ThreadLocal详解

216 阅读9分钟

ThreadLocal详解

提供线程内的局部变量,不同线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

其有三个特点:

  • 线程并发——在多线程开发的场景下
  • 传递数据——通过ThreadLocal在同一线程,不同组件中传递公共变量
  • 线程隔离——每个线程的变量都是独立的,不会互相影响

⭐基本使用

常用方法

方法描述
ThreadLocal()创建ThreadLocal对象
public void set(T value)设置当前线程绑定的局部变量
public get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量

🌰举例

我们现在要创建两个线程,要求每个线程之间的变量是相互独立的

我们先来看看如果不隔离会出现什么情况:

//test类
//一会儿变量要将数据存入content中
private String content;
​
public String getContent() {
    return content
}
​
public void setContent(content) {
    this.content = content
}
​
public static void main(String[] args) {
    //先创建本类对象
    Test test = new Test();
    
    //创建第一个线程
    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            //往类中存自己的数据
            test.setContent(Thread.currentThread().getName() + "的数据");
            //从类中读取自己刚刚存进去的数据
            System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
        }
    })
    
    thread1.setName("线程一");
    //执行线程1
    thread1.start();
    
    //创建第二个线程
    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            //往类中存自己的数据
            test.setContent(Thread.currentThread().getName() + "的数据");
            //从类中读取自己刚刚存进去的数据
            System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
        }
    })
    
    thread1.setName("线程二");
    //执行线程2
    thread2.start();
}

以上两个线程均被运行,共同工作,都向本类的content变量中存储数据,我们知道线程的运行是抢占式的,线程一抢到了就他运行一定时间,时间到后线程二再运行,这样反复抢占,各个线程的执行顺序是不一定的,运气好的话,线程一先执行,存储完自己的数据后再进行读取,然后轮线程二存储取出。 但运气不好的话,线程一先存入了自己的数据,时间到了,轮到线程二来执行,线程二也存入自己的数据,但这里就遇到了问题,两个线程使用的是同一个变量,所以这里线程二存入的数据就覆盖了线程一的数据,再轮到线程一执行时,他取到的就是线程二的数据了:

线程一--->线程二的数据

这到了真实开发环境中是很要命的,数据乱存乱放可不行,我们就要使用ThreadLocal来解决问题了:

  1. 使用set方法将变量绑定到当前线程中
  2. 再使用get方法获取当前变量绑定的线程

🌰案例改进

我们在类中先创建ThreadLocal对象:

ThreadLocal<String> t1 = new ThreadLocal<>();

然后再改写content变量的setter和getter:

public String getContent() {
    //从t1中读取变量
    return t1.get();
}
​
public void setContent(String content) {
    //将content绑定到当前线程中,注意是当前!也就是说哪个线程在执行,它绑定的就是哪个线程
    t1.set(content);
}

这样改进后再去执行,就能保证每个线程读取到的变量都是自己设置的那个,就不会乱套啦!

不过这样设置后,类中原本有的content就可以休息啦,线程们都不会再去使用这个公共的变量,而是创建了多个它的替身供各个线程私有使用。

有些小伙伴可能会拿synchronized和它对比一下,使用synchronized包裹也能实现同样的效果:

synchronized(Test.class){
    //往类中存自己的数据
    test.setContent(Thread.currentThread().getName() + "的数据");
    //从类中读取自己刚刚存进去的数据
    System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
}

但二者还是有些区别的:

  • synchronized加锁是让一个线程同一时间读写,也就是保证读写前后不会被其他进程抢占,保证了自己放完自己取,但前后用的还是一个content,只是下一个进程就把上一个进程覆盖掉了。(时间换空间)
  • ThreadLocal更像是把内存中的变量Copy到了线程自己的工作内存中,每个变量操作的都是自己的变量。(空间换时间)

在我们经典的转账案例中,同样也可以使用ThreadLocal

思路:

  1. 转账是通过service层调用dao层再去数据库中进行操作

  2. 而如果其中一环操作异常,就需要进行事务回滚,让钱回到原位

  3. 但事务有两个需要注意的地方:

    1. 为了保证所有的操作在一个事务中,service层开启的事务的connection需要和dao层访问数据库的connection保持一致
    2. 在线程并发的情况下,每个线程只能操作各自的connection,这里就用到了线程隔离

方案一: 将service的connection直接传到dao层中,而在dao层中不关闭连接,最后让service层负责关闭连接

方案二: 创建一个获取连接方法,在方法中直接获取当前线程绑定的连接对象,如果连接对象为空,则去连接池中获取,获取到连接对象后,将此对象跟当前线程进行绑定后返回。

🌙使用ThreadLocal的优点

  1. 传递数据——保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题
  2. 线程隔离——各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失

⭐ThreadLocal的内部结构

🌙早期设计

早期JDK中,ThreadLocal设计为如下形式:

earlyInJDK.png

每个ThreadLocal都创建一个map,然后用线程作为map的key,要存储的局部变量作为map的value,这样就能达到各个线程的局部变量隔离的效果。

不过现在已经不是这种设计形式了

🌙现在的设计

now.png

改为每个Thread都创建一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值Object,ThreadLocal负责向map获取和设置线程的变量值。这样,对于不同的线程,每次获取副本值时,并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

交由Thread进行map管理的好处:

  1. 每个Map存储的Entry数量变少
  2. 当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存的使用

🌙核心方法设计

其各方法核心设计思路也很简单:

  • set方法

    • 首先获取当前线程,并根据当前线程获取一个Map
    • 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
    • 如果Map为空,则给该线程创建Map,并设置初始值
  • get方法

    • 首先获取当前线程,根据当前线程获取一个Map
    • 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到下面最后一步
    • 如果e不为空,则返回e.value,否则转到下一步
    • Map为空或者e为空的情况下,则会通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

    get方法总结:先获取当前线程的ThreadLocalMap变量,如果存在则返回值,如果不存在就创建并返回初始值

  • remove方法

    • 首先获取当前线程,根据当前线程获取一个Map
    • 如果获取到的Map不为空,则移除当前的ThreadLocal对象对应的entry
  • initialValue方法

    • 这个方法是一个延迟调用方法,在set方法还未调用而先调用了get方法时才执行,并且仅执行一次
    • 这个方法缺省实现直接返回一个null
    • 如果想要一个除null之外的初始值,可以重写此方法(该方法定位protected,显然就是为了让子类覆盖而设计的)

🌙ThreadLocalMap内部分析

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现

其类图设计如下:

UML.png

它的成员变量:

        /**
         * The initial capacity -- MUST be a power of two.
         * 初始化容量,必须是2的整数次幂
         */
        private static final int INITIAL_CAPACITY = 16;
​
        /**
         * 存放数据的table, 同样数组长度必须是2的整数次幂
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
​
        /**
         * 数组里entrys的个数,可以判断table是否超过阈值 (存储的格式)
         * The number of entries in the table.
         */
        private int size = 0;
​
        /**
         * 阈值 进行扩容的阈值,表使用大于他的时候,进行扩容
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

存储结构:

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         翻译:
         * Entry继承WeakReference, 并且用ThreadLocal作为key
         * 如果key为null(entry.get() == null)意味着key不在被引用,因此这时候entry也可以从table中清除(被垃圾回收器回收) 
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
​
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
​

可以注意到,Entry继承自WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。

⭐内存泄露问题

内存泄露(Memory leak) ——是指程序中已动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄露的堆积终将导致内存溢出。

🌙ThreadLocalMap中的key为强引用时

strongReference.png

当ThreadLocalMap中使用了强引用时,若ThreadLocal Ref被回收了,若线程仍在运行,仍有Entry的强引用指向ThreadLocal,而导致ThreadLocal无法被回收,且Enter中Value指向的内存也无法被回收,造成了内存泄露。

所以这时就需要我们手动去删除Entry来避免内存泄露

🌙ThreadLocalMap中的key为弱引用时

weakReference.png

这里由于ThreadLocalMap只持有对ThreadLocal的弱引用,所以若ThreadLocal Ref被回收,ThreadLocal就可以顺利被gc回收,此时Entry中的key就为null

但如果线程仍在运行,且我们没有删除Entry的情况下,依然有从thread Ref -> currentThread -> ThreadLocalMap -> entry -> value的强引用,导致value不会被回收,最终造成value内存泄露。

所以,即使ThreadLocalMap中的key使用了弱引用形式,也可能会造成内存泄露,只不过少了ThreadLocal的内存泄露而已

所以内存泄露有两个前提:

  1. 没有手动删除Entry
  2. 线程仍在运行

我们想避免内存泄露的话:

  1. 使用完ThreadLocal,调用其remove方法删除对应的Entry
  2. 使用完ThreadLocal,当前Thread也随之运行结束

相对第一种方式,第二种方式显然不好控制,尤其是使用线程池的时候,线程结束是不会销毁的。

也就是说,想要避免内存泄露,只要记得在使用完ThreadLocal后及时的调用remove,无论key是强引用和弱引用都不会有问题

那ThreadLocalMap的key为什么要设置为弱引用呢?

事实上,在ThreadLocalMap中的set/setEntry方法中,会对key为null(即ThreadLocal为null)进行判断,如果为null的话,那么会对value设为null。

这就意味着当我们使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障,保障弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set、get、remove中的任一方法时会被清除,从而避免内存泄露。