ThreadLocal从变量副本的角度解决多线程并发安全问题

ThreadLocal从变量副本的角度解决多线程并发安全问题

之前我们讲的高并发场景下的线程安全问题,可以使用Synchronized同步关键字、Lock手动加锁的方式去解决,什么轻量级锁、偏向锁、重量级锁、可重入锁等等,实际上本质都是控制线程,使得多个线程同步的去访问共享资源。之所以多线程存在线程安全问题,就是因为多个线程访问同一个共享资源导致的,多个线程之间属于竞争关系,很容易就会导致数据的不安全。

ThreadLocal从变量副本的角度解决多线程并发安全问题

我们说了加锁实际上保证了各个线程之间同步、有序的去访问共享资源,难道不加锁就没有办法解决多线程安全问题了吗?我们要抓主要矛盾,之所以存在并发安全问题,是因为共享资源只有一个,多线程会竞争获取共享资源,如果同一类共享资源有多个,或者说有多个副本给每一个线程使用呢?这样不就不用加锁了,每一个线程只需要存在自己的那个数据的副本即可,因此也就不存在资源竞争问题,也就保证了多线程下数据的安全。而ThreadLocal类就是给每个线程绑定了变量的本地副本,从而避免线程安全。

ThreadLocal从变量副本的角度解决多线程并发安全问题

接下来我们从源码来看看ThreadLocal类是如何给每一个线程保存变量的本地副本的。

首先我们看看Thread线程类,线程类里面有一个ThreadLocalMap类型的成员变量threadLocals,用来存放当前线程的本地变量

ThreadLocal从变量副本的角度解决多线程并发安全问题

实际上ThreadLocal类有一个ThreadLocalMap内部类,这个内部类你可以认为是一个专门用来维护线程本地变量HashMap集合

ThreadLocalMap这个类的数据结构是一个Entry类型的数组,用来保存一个个的Entry节点,Entry节点封装了以ThreadLocal实例对象为keyObject对象为value的键值对,保存在ThreadLocalMap

ThreadLocal从变量副本的角度解决多线程并发安全问题

ThreadLocal类实际上是对ThreadLocalMap这个内部类的封装本地变量值最终是存放在ThreadLocalMap中的ThreadLocal类提供了set()get()等其他方法,来操作ThreadLocalMap中保存的数据。

ThreadLocal调用set()方法保存本地变量时,首先获取到当前线程,然后调用getMap()方法得到当前线程的ThreadLocalMap,底层实际上是调用ThreadLocalMapset()方法向这个Map集合中保存数据的ThreadLocal从变量副本的角度解决多线程并发安全问题

当第一次调用set()方法时,会先调用createMap()方法创建出ThreadLocalMap对象,因此是懒加载的
ThreadLocal从变量副本的角度解决多线程并发安全问题

接着会使用构造方法创建出ThreadLocalMap对象

ThreadLocal从变量副本的角度解决多线程并发安全问题

我们之前说过ThreadLocalMap是一个HashMap集合,因此也有初始容量、加载因子、阈值、散列函数、hashcode值。

ThreadLocalMap的初始容量默认为16,阈值为容量的2/3,利用ThreadLocalhashcode值,对容量进行取模运算,计算出Entry数组中对应的索引位置

ThreadLocal从变量副本的角度解决多线程并发安全问题
ThreadLocal从变量副本的角度解决多线程并发安全问题

如果ThreadLocalMap之前已经创建出来了,就会调用set()方法向ThreadLocalMap中添加元素。根据hashcode值计算出数组中对应的索引位置,然后遍历这个map所有的Entry,如果key存在了就进行替换,没有的话就将键值对保存到ThreadLocalMap中。同时在遍历的过程中发现key为null,就会清除掉这个数据,并将新的数据存放到这个索引位置。这主要是释放了内存空间,防止内存泄漏

如果既没有发生替换,也没有发生可以清除掉的key,那么就会创建一个Entry,保存到计算出的对应的索引位置。

ThreadLocal从变量副本的角度解决多线程并发安全问题

我们说过ThreadLocalMap初始容量默认为16,阈值默认为容量的2/3。在向map中添加完数据时,最后还会进行一次清理工作,如果清理后发现当前map的大小还是大于等于阈值,就会触发扩容机制

ThreadLocal从变量副本的角度解决多线程并发安全问题
ThreadLocal从变量副本的角度解决多线程并发安全问题

ThreadLocalMap扩容机制和HashMap差不多,也是扩容为原来的2倍,然后进行扩容后在再散列,并设置新的阈值。
ThreadLocal从变量副本的角度解决多线程并发安全问题

讲完了set()方法再来讲一讲get()方法。

get()方法首先获取到当前线程,然后调用getMap()方法得到当前线程对应的ThreadLocalMap。如果这个map不为null,就根据key得到对应Entry,并返回对应的value值。

ThreadLocal从变量副本的角度解决多线程并发安全问题

如果map为null,比如在第一次调用get()方法,这个map可能还没创建出来。此时会调用setInitialValue()方法来设置初始值,

并返回这个value,value的默认初始值为null。
ThreadLocal从变量副本的角度解决多线程并发安全问题

我们讲完了ThreadLocal类的set()get()方法,实际上都是一直在操作ThreadLocalMap这个map集合。而ThreadLocalMap中存放的都是一个个的Entry,Entry的key为ThreadLocal对象,value为对应的本地变量。实际上这个EntryWeakReference弱引用的子类,这是为了在JVM进行垃圾回收时,能够自动进行回收,防止内存溢出。真正存储数据备份其实就是这个Entry
ThreadLocal从变量副本的角度解决多线程并发安全问题

ThreadLocal类本质实际上是以线程作为key,通过数据备份的方式,实现了线程间的数据隔离!

既然进行的数据备份,那么肯定就会造成数据冗余,并且随着线程的存活时间增长,存储的备份数据也会越来越多,即使线程结束了生命周期,这些备份数据也很有可能仍然存在。这样就可能造成内存泄漏,进而导致OOM!

ThreadLocal为了解决内存泄漏的问题,也进行了一些相应的处理,比如将存储备份数据的Entry类设置为弱引用类型,这是为了方便在GC时自动回收。而且在set()get()方法中增加了数据检查,及时清除掉那些key为null的没用的备份数据。

上一篇:遍历map的四种方法及Map.entry详解


下一篇:面试官:你知道 LRU算法 —— 缓存淘汰算法吗?