JUC并发编程10——ThreadLocal

本文详细介绍了JavaThreadLocal的作用、使用方法,包括set、get和remove操作,以及为何使用弱引用。还对比了ThreadLocalMap和HashMap的区别,并强调了ThreadLocal变量的不传递性和InheritableThreadLocal的使用场景。
摘要由CSDN通过智能技术生成

目录

1. ThreadLocal是什么?

2. ThreadLocal怎么用?

3. ThreadLocal源码分析

3.1set方法

3.2get()方法

3.3remove()方法

4.为什么key使用弱引用?

5.ThreadLocalMap 和 HashMap 区别

6.ThreadLocal变量不具有传递性

7.InheritableThreadLocal使用示例


我们都知道,在多线程环境下访问同一个共享变量,可能会出现线程安全的问题,为了保证线程安全,我们往往会在访问这个共享变量的时候加锁,以达到同步的效果,如下图所示。

对共享变量加锁虽然能够保证线程的安全,但是却增加了开发人员对锁的使用技能,如果锁使用不当,则会导致死锁的问题。而ThreadLocal能够做到在创建变量后,每个线程对变量访问时访问的是线程自己的本地变量

1. ThreadLocal是什么?

从名字我们就可以看到 ThreadLocal 叫做本地线程变量,意思是说,ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。

2. ThreadLocal怎么用?

public class ThreadLocalDemo {
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args){
        //创建第一个线程
        Thread threadA = new Thread(()->{
            threadLocal.set("ThreadA:" + Thread.currentThread().getName());
            System.out.println("线程A本地变量中的值为:" + threadLocal.get());
            threadLocal.remove();
            System.out.println("线程A删除本地变量后ThreadLocal中的值为:" + threadLocal.get());
        });
        //创建第二个线程
        Thread threadB = new Thread(()->{
            threadLocal.set("ThreadB:" + Thread.currentThread().getName());
            System.out.println("线程B本地变量中的值为:" + threadLocal.get());
            System.out.println("线程B没有删除本地变量:" + threadLocal.get());
        });
        //启动线程A和线程B
        threadA.start();
        threadB.start();
    }
}

ThreadLocal通常被声明为private static:

  • 私有性(Private):使用private修饰符可以确保ThreadLocal实例不会被外部类访问,从而防止了其值被意外修改,增强了封装性。当然是否使用private修饰是一个普遍的问题而不是与ThreadLocal有关的一个具体问题。
  • 静态性(Static):ThreadLocal通常会被声明为static,这样做的好处是可以避免重复创建与线程相关的变量(Thread Specific Object,TSO)。如果ThreadLocal被声明为某个类的实例变量(而不是静态变量),那么每创建一个该类的实例就会导致一个新的TSO实例被创建。这样,同一个线程可能会访问到同一个TSO(指类)的不同实例,这即便不会导致错误,也会导致浪费(重复创建等同的对象)

3. ThreadLocal源码分析

首先,我们看下Thread类的源码,如下所示:

由Thread类的源码可以看出,在Thread类中存在成员变量threadLocals和inheritableThreadLocals,这两个成员变量都是ThreadLocalMap类型的变量,而且二者的初始值都为null。只有当前线程第一次调用ThreadLocal的set()方法或者get()方法时才会实例化变量。

这里需要注意的是:每个线程的本地变量不是存放在ThreadLocal实例里面的,而是存放在调用线程Thread对象的threadLocals变量里面的。

也就是说,调用ThreadLocal的set()方法存储的本地变量是存放在具体线程的内存空间中的,而ThreadLocal类只是提供了set()和get()方法来存储和读取本地变量的值,当调用ThreadLocal类的set()方法时,把要存储的值放入调用线程Thread对象的threadLocals中存储起来,当调用ThreadLocal类的get()方法时,从当前线程的threadLocals变量中将存储的值取出来。

接下来,我们分析下ThreadLocal类的set()、get()和remove()方法的实现逻辑。

3.1set方法

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取Thread对象中的ThreadLocalMap成员属性
    ThreadLocalMap map = getMap(t);
    //获取的ThreadLocalMap对象不为空
    if (map != null)
        //设置value的值
        map.set(this, value);
    else
        //获取的ThreadLocalMap对象为空,创建Thread类中的threadLocals变量
        createMap(t, value);
}

如果调用getMap(t)方法返回的对象为空,则程序调用createMap(t, value)方法来实例化Thread类的threadLocals成员变量。

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

也就是创建当前线程的threadLocals变量,并且存了一个key为Threadlocal对象,value为firstValue的键值对。

因为每个线程Thread对象的ThreadLocal.ThreadLocalMap threadLocals不同,所以即便是使用同一个key,但Map都不同了,拿出来的value自然也是相互隔离的。

我们再仔细看看 ThreadLocalMap 的 set() 方法:

private void set(ThreadLocal<?> key, Object value) {

            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;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

第一种情况: 通过hash计算后的槽位对应的Entry数据为空,直接将数据放到该槽位

第二种情况: 槽位数据不为空,key值与当前ThreadLocal通过hash计算获取的key值一致,直接更新该槽位的数据

第三种情况: 槽位数据不为空,往后遍历过程中,在找到Entry为null的槽位之前,没有遇到key过期的Entry

遍历散列数组,线性往后查找,如果找到Entry为null的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了key 值相等的数据,直接更新即可。

第四种情况: 槽位数据不为空,往后遍历过程中,在找到Entry为null的槽位之前,遇到key过期的Entry,如下图,往后遍历过程中,遇到了index=7的槽位数据Entry的key=null

散列数组下标为 7 位置对应的Entry数据key为null,表明此数据key值已经被垃圾回收掉了,此时就会执行replaceStaleEntry()方法,该方法含义是替换过期数据的逻辑,以index=7位起点开始遍历,进行探测式数据清理工作。

3.2get()方法

public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的threadLocals成员变量
    ThreadLocalMap map = getMap(t);
    //获取的threadLocals变量不为空
    if (map != null) {
        //返回本地变量对应的值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //初始化threadLocals成员变量的值
    return setInitialValue();
}

通过当前线程来获取threadLocals成员变量,如果threadLocals成员变量不为空,则直接返回当前线程绑定的本地变量,否则调用setInitialValue()方法初始化threadLocals成员变量的值。

private T setInitialValue() {
    //调用初始化Value的方法
    T value = initialValue();
    Thread t = Thread.currentThread();
    //根据当前线程获取threadLocals成员变量
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //threadLocals不为空,则设置value值
        map.set(this, value);
    else
        //threadLocals为空,创建threadLocals变量
        createMap(t, value);
    return value;
}

其中,initialValue()方法的源码如下所示

protected T initialValue() {
    return null;
}

通过initialValue()方法的源码可以看出,这个方法可以由子类覆写,在ThreadLocal类中,这个方法直接返回null。

3.3remove()方法

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
}

可以看到,在remove()方法中,首先根据当前线程获取ThreadLocalMap类型的m对象,不为空,则直接调用m对象的有参remove()方法移除value的值。

我们继续往里面看

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.refersTo(key)) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

可以看到,在有参remove()方法中,会通过threadLocalHashCode计算出Entry对象在Entry数组中的位置,并获取出对应的Entry对象,如果Entry对象不为空,并且Entry对象中的Key等于传入的ThreadLocal对象,则清除对应的Key,并且调用expungeStaleEntry()方法

接下来,我们再分析下expungeStaleEntry()方法

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    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;
        }

可以看到,在expungeStaleEntry()方法中,会将key为null(即ThreadLocal为null)对应的value设置为null,同时会把对应的Entry对象也设置为null,并且会将所有ThreadLocal对应的value为null的Entry对象设置为null,这样就去除了强引用,便于后续的GC进行自动垃圾回收,也就避免了内存泄露的问题。

注意:在ThreadLocal中,不仅仅是remove()方法会调用expungeStaleEntry()方法,在set()方法和get()方法中也可能会调用expungeStaleEntry()方法来清理数据。

ThreadLocal虽然提供了避免内存泄露的方法,但是ThreadLocal不会主动去执行这些方法,需要我们在使用完ThreadLocal对象中保存的数据后,在finally{}代码块中调用ThreadLocal的remove()方法,加快GC自动垃圾回收,避免内存泄露。

4.为什么key使用弱引用?

如果使用强引用,当ThreadLocal 对象的引用(强引用)被回收了,ThreadLocalMap本身(Entry数组)依然还持有ThreadLocal对象的强引用,如果没有手动删除这个key ,则ThreadLocal对象不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收, 可以认为这导致Entry内存泄漏。

如果使用弱引用,那指向ThreadLocal对象的引用就两个:ThreadLocalRef强引用和ThreadLocalMap中Entry的弱引用。一旦ThreadLocalRef强引用被回收,则指向ThreadLocal的就只有弱引用了,在下次gc的时候,这个ThreadLocal对象就会被回收。而这个弱引用Key也将置为null。

此时,我们可以看到,Entry对象中的Key,也就是ThreadLocal对象可以被GC自动回收,但是对应的value还在被引用,并且value是强引用,所以,value是不能被GC自动回收的,这种情况下就会存在内存泄露的风险。

使用了线程池,可以达到“线程复用”的效果。但是归还线程之前记得清除ThreadLocalMap,要不然再取出该线程的时候,ThreadLocal变量还会存在。这就不仅仅是内存泄露的问题了,整个业务逻辑都可能会出错。

5.ThreadLocalMap 和 HashMap 区别

ThreadLocalMap 和HashMap的功能类似,但是实现上却有很大的不同:

  • HashMap 的数据结构是数组+链表,HashMap 是通过链地址法解决hash 冲突的问题,HashMap 里面的Entry 内部类的引用都是强引用。
  • ThreadLocalMap的数据结构仅仅是数组,ThreadLocalMap 是通过开放定址法——线性探测来解决hash 冲突的问题,ThreadLocalMap里面的Entry 内部类中的key 是弱引用,value 是强引用。

如上图所示,如果我们插入一个value=27的数据,通过 hash 计算后应该落入槽位 4 中,而槽位 4 已经有了 Entry 数据。此时就会线性向后查找,一直找到 Entry 为 null 的槽位才会停止查找,将当前元素放入此槽位中。

6.ThreadLocal变量不具有传递性

使用ThreadLocal存储本地变量不具有传递性,也就是说,同一个ThreadLocal在父线程中设置值后,在子线程中是无法获取到这个值的,这个现象说明ThreadLocal中存储的本地变量不具有传递性。

代码示例:

那有没有办法在子线程中获取到主线程设置的值呢?此时,我们可以使用InheritableThreadLocal来解决这个问题。

7.InheritableThreadLocal使用示例

InheritableThreadLocal类继承自ThreadLocal类,它能够让子线程访问到在父线程中设置的本地变量的值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值