您当前的位置: 首页 > 

Dongguo丶

暂无认证

  • 1浏览

    0关注

    472博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

ThreadLocal

Dongguo丶 发布时间:2021-09-25 14:14:05 ,浏览量:1

ThreadLocal概述 是什么

官网:

image-20210908064936399

稍微翻译一下: ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态(private static)字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。

能干嘛

实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份), 主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。

image-20210908065026241

解决线程安全问题:

从悲观锁串行化 是 1:1 加入synchronized或者Lock控制资源的访问顺序

到cas乐观锁是 N:1 自旋

到threadLocal是 N:N 人手一份,大家各自安好,没必要抢夺

api介绍

image-20210908065046559

案例 按照总销售额统计,方便集团公司做计划统计

例如经典案例卖票 ,三个售票员卖出30张票使用synchronized

package com.dongguo.threadlocal;

/**
 * @author Dongguo
 * @date 2021/9/8 0008-7:08
 * @description:
 */
class Ticket{
    private int number = 30;

     public synchronized void saleTicket() {
         if (number>0){
             System.out.println(Thread.currentThread().getName()+"买票成功,还剩"+number--+"张");
         }else {
             System.out.println(Thread.currentThread().getName()+"票卖完了");
         }
     }
}
public class ThreadLocalDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        for (int i = 1; i {
                for (int j = 0; j  0);

    public ThreadLocal getThreadLocal() {
        return threadLocal;
    }

    public void saleHouse() {
        Integer value = threadLocal.get();
        value++;
        threadLocal.set(value);
    }
}

public class ThreadLocalDemo {
    public static void main(String[] args) {
        House house = new House();
        new Thread(() -> {
            try {
                for (int i = 1; i  {
            try {
                for (int i = 1; i  {
            try {
                for (int i = 1; i  field[YEAR];
    if (weekDate && !cal.isWeekDateSupported()) {
        // Use YEAR instead
        if (!isSet(YEAR)) {
            set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
        }
        weekDate = false;
    }

    cal.clear();//清除Calendar
   ...
}

calendar使用完一次就清除,如果在并发条件下,一个线程正在使用calendar引用时,另一个线程把calendar clear清楚了,会出现并发安全问题

sdf.format(date)

@Override
public StringBuffer format(Date date, StringBuffer toAppendTo,
                           FieldPosition pos)
{
    pos.beginIndex = pos.endIndex = 0;
    return format(date, toAppendTo, pos.getFieldDelegate());
}

并发条件下calendar.setTime(date);多个线程同时使用一个calendar设置时间,会出现并发安全问题

private StringBuffer format(Date date, StringBuffer toAppendTo,
                            FieldDelegate delegate) {
    // Convert input date to time field list
    calendar.setTime(date);//

    boolean useDateFormatSymbols = useDateFormatSymbols();

    for (int i = 0; i >> 8;
        int count = compiledPattern[i++] & 0xff;
        if (count == 255) {
            count = compiledPattern[i++] Entry->ThreadLocal

image-20210908105543845

一个Thread最多只有一个ThreadLocalMap,ThreadLocalMap底层是一个Entry数组,

但是一个Thread可以有多个ThreadLocal,一个ThreadLocal对应一个变量数据,封装成Entry存到ThreadLocalMap中,所以就有多个Entry。

image-20210925125127713

ThreadLocal内存泄露问题

不再被使用的对象或者不再被变量占用的内存不能被回收,就是内存泄露。

为什么会出现内存泄漏

image-20210908111735622

ThreadLocalMap与WeakReference ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以它为Key),不过是经过了两层包装的ThreadLocal对象: (1)第一层包装是使用 WeakReference>:

image-20210908152402197

image-20210908152412615

每个Thread对象维护着一个ThreadLocalMap的强引用 ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象 调用ThreadLocal的get()方法时,实际上就是从ThreadLocalMap获取值,key是ThreadLocal对象 ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响

为什么要用弱引用?不用如何?
public void function01()
{
    ThreadLocal tl = new ThreadLocal();    //line1
    tl.set(2021);                                   //line2
    tl.get();                                       //line3
}

line1新建了一个ThreadLocal对象,t1 是强引用指向这个对象; line2调用set()方法后新建一个Entry,通过源码可知Entry对象里的k是弱引用指向这个对象。

image-20210908152746626

为什么源代码用弱引用? 当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象 若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏; 若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的问题)。

使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收 ,此时Entry的key引用就指向为null。

因为map是允许存在空key的,那如何回收这些entry呢?

此后我们调用get,set或remove方法时,就会尝试删除key为null的entry,可以释放value对象所占用的内存。

image-20210908153438217

1 当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(tl=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,如果value占用内存非常大 ,即key=null,value为大对象很容易造成内存泄漏。

2当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。

3 但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心

key为null的entry,原理解析

image-20210908153525184

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链。

ThreadLocalMap里面的Entry 内部类中的key 是弱引用,value 是强引用

虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的任务就有可能获取到上个任务遗留下来的value值,造成bug。

set、get、remove方法会去检查所有键为null的Entry对象

set()

private void set(ThreadLocal key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }
		//检查所有键为null的Entry对象 
        //当key为null时
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
private void replaceStaleEntry(ThreadLocal key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
      //这里采用的是从当前的staleSlot 位置向前面遍历,i--
      //这样的话是为了把前面所有的key为null的entry也一起释放空间出来
      //(注意这里只是key 被回收,value还没被回收,entry更加没回收,所以需要让他们回收),
       //同时也避免这样存在很多过期的对象的占用,导致这个时候刚好来了一个新的元素达到阀值而触发一次新的rehash
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            //
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            进行清理过期数据
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);//cleanSomeSlots、expungeStaleEntry方法
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
         // 如果我们在第一个for 循环(i--) 向前遍历的时候没有找到任何过期的对象
                // 那么我们需要把slotToExpunge 设置为向后遍历(i++) 的第一个过期对象的位置
                // 因为如果整个数组都没有找到要设置的key 的时候,该key 会设置在该staleSlot的位置上
                //如果数组中存在要设置的key,那么上面也会通过交换位置的时候把有效值移到staleSlot位置上
                //综上所述,staleSlot位置上不管怎么样,存放的都是有效的值,所以不需要清理的
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
     // 如果key 在数组中没有存在,那么直接新建一个新的放进去就可以
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
    // 如果有其他已经过期的对象,那么需要清理他
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
//主要作用:从当前位置开始,往后再找一段,碰到脏entry进行清理,碰到null结束
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    // 1. 将当前的脏entry 置为null,value 置为 null, 方便GC回收,  size,即entry[] 的数量 减一
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
     // 依次循环的使index往后移,直到找到一个 entry = null (遍历结束),则退出,并返回这个 index
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal k = e.get();
        // 在这个过程中,发现脏entry就清除掉
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
           //这里主要的作用是由于采用了开放地址法,所以删除的元素是多个冲突元素中的一个,需要对后面的元素作
                //处理,可以简单理解就是让后面的元素往前面移动
                //为什么要这样做呢?主要是开放地址寻找元素的时候,遇到null 就停止寻找了,你前面k==null
                //的时候已经设置entry为null了,不移动的话,那么后面的元素就永远访问不了了,下面会画图进行解释说明
            int h = k.threadLocalHashCode & (len - 1);
            //他们不相等,说明是经过hash 是有冲突的
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

扩展

为什么ThreadLocalMap 采用开放地址法来解决哈希冲突? jdk 中大多数的类都是采用了链地址法来解决hash 冲突,为什么ThreadLocalMap 采用开放地址法来解决哈希冲突呢?首先我们来看看这两种不同的方式

链地址法

这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。列如对于关键字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我们用前面同样的12为除数,进行除留余数法:

img

开放地址法

这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入。

比如说,我们的关键字集合为{12,33,4,5,15,25},表长为10。 我们用散列函数f(key) = key mod l0。 当计算前S个数{12,33,4,5}时,都是没有冲突的散列地址,直接存入(蓝色代表为空的,可以存放数据):

img

计算key = 15时,发现f(15) = 5,此时就与5所在的位置冲突。

于是我们应用上面的公式f(15) = (f(15)+1) mod 10 =6。于是将15存入下标为6的位置。这其实就是房子被人买了于是买下一间的作法:

img

链地址法和开放地址法的优缺点

开放地址法:

容易产生堆积问题,不适于大规模的数据存储。 散列函数的设计对冲突会有很大的影响,插入时可能会出现多次冲突的现象。 删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。 链地址法:

处理冲突简单,且无堆积现象,平均查找长度短。 链表中的结点是动态申请的,适合构造表不能确定长度的情况。 删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。 指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间。 ThreadLocalMap 采用开放地址法原因

ThreadLocal 中看到一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table,关于这个神奇的数字google 有很多解析,这里就不重复说了 ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低 参考:

被大厂面试官连环炮轰炸的ThreadLocal (吃透源码的每一个细节和设计原理)

get()

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);//
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
private Entry getEntry(ThreadLocal key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);//
}
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal k = e.get();
        if (k == key)
            return e;
        if (k == null)//当key为null时 expungeStaleEntry方法
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

remove()

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);//
}
private void remove(ThreadLocal key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);//expungeStaleEntry方法
            return;
        }
    }
}

从前面的set,get,remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题, 都会通过replaceStaleEntry()、cleanSomeSlots()、expungeStaleEntry(),这三个方法(最终)清理掉key为null的无用entry。

ThreadLocal最佳实践场景

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OzQMwOal-1632550421493)(E:\笔记\图片\image\18-1ThreadLocal.assets\image-20210908153926026.png)]

image-20210908153942703

用完记得手动remove

总结

ThreadLocal 并不解决线程间共享数据的问题

ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景

ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题

每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题

ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题

都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为 null 的 Entry 对象的值value(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法

关注
打赏
1638062488
查看更多评论
0.0522s