ThreadLocal从变量副本的角度解决多线程并发安全问题
之前我们讲的高并发场景下的线程安全问题,可以使用Synchronized同步关键字、Lock手动加锁的方式去解决,什么轻量级锁、偏向锁、重量级锁、可重入锁等等,实际上本质都是控制线程,使得多个线程同步的去访问共享资源。之所以多线程存在线程安全问题,就是因为多个线程访问同一个共享资源导致的,多个线程之间属于竞争关系,很容易就会导致数据的不安全。
我们说了加锁实际上保证了各个线程之间同步、有序的去访问共享资源,难道不加锁就没有办法解决多线程安全问题了吗?我们要抓主要矛盾,之所以存在并发安全问题,是因为共享资源只有一个,多线程会竞争获取共享资源,如果同一类共享资源有多个,或者说有多个副本给每一个线程使用呢?这样不就不用加锁了,每一个线程只需要存在自己的那个数据的副本即可,因此也就不存在资源竞争问题,也就保证了多线程下数据的安全。而ThreadLocal
类就是给每个线程绑定了变量的本地副本,从而避免线程安全。
接下来我们从源码来看看ThreadLocal
类是如何给每一个线程保存变量的本地副本的。
首先我们看看Thread线程类,线程类里面有一个ThreadLocalMap
类型的成员变量threadLocals
,用来存放当前线程的本地变量
实际上ThreadLocal
类有一个ThreadLocalMap
内部类,这个内部类你可以认为是一个专门用来维护线程本地变量HashMap集合。
ThreadLocalMap
这个类的数据结构是一个Entry
类型的数组,用来保存一个个的Entry
节点,Entry
节点封装了以ThreadLocal
实例对象为key
,Object
对象为value
的键值对,保存在ThreadLocalMap
中。
ThreadLocal
类实际上是对ThreadLocalMap
这个内部类的封装,本地变量值最终是存放在ThreadLocalMap
中的,ThreadLocal
类提供了set()
、get()
等其他方法,来操作ThreadLocalMap
中保存的数据。
ThreadLocal
调用set()
方法保存本地变量时,首先获取到当前线程,然后调用getMap()
方法得到当前线程的ThreadLocalMap
,底层实际上是调用ThreadLocalMap
的set()
方法向这个Map集合中保存数据的
当第一次调用set()
方法时,会先调用createMap()
方法创建出ThreadLocalMap
对象,因此是懒加载的
接着会使用构造方法创建出ThreadLocalMap
对象
我们之前说过ThreadLocalMap
是一个HashMap
集合,因此也有初始容量、加载因子、阈值、散列函数、hashcode值。
ThreadLocalMap
的初始容量默认为16
,阈值为容量的2/3
,利用ThreadLocal
的hashcode
值,对容量进行取模运算,计算出Entry数组中对应的索引位置。
如果ThreadLocalMap
之前已经创建出来了,就会调用set()
方法向ThreadLocalMap
中添加元素。根据hashcode值计算出数组中对应的索引位置,然后遍历这个map所有的Entry,如果key存在了就进行替换,没有的话就将键值对保存到ThreadLocalMap中。同时在遍历的过程中发现key为null,就会清除掉这个数据,并将新的数据存放到这个索引位置。这主要是释放了内存空间,防止内存泄漏。
如果既没有发生替换,也没有发生可以清除掉的key,那么就会创建一个Entry,保存到计算出的对应的索引位置。
我们说过ThreadLocalMap初始容量默认为16,阈值默认为容量的2/3。在向map中添加完数据时,最后还会进行一次清理工作,如果清理后发现当前map的大小还是大于等于阈值,就会触发扩容机制
ThreadLocalMap扩容机制和HashMap差不多,也是扩容为原来的2倍,然后进行扩容后在再散列,并设置新的阈值。
讲完了set()
方法再来讲一讲get()
方法。
get()
方法首先获取到当前线程,然后调用getMap()
方法得到当前线程对应的ThreadLocalMap
。如果这个map不为null,就根据key得到对应Entry,并返回对应的value值。
如果map为null,比如在第一次调用get()
方法,这个map可能还没创建出来。此时会调用setInitialValue()
方法来设置初始值,
并返回这个value,value的默认初始值为null。
我们讲完了ThreadLocal
类的set()
、get()
方法,实际上都是一直在操作ThreadLocalMap
这个map集合。而ThreadLocalMap中存放的都是一个个的Entry,Entry的key为ThreadLocal对象,value为对应的本地变量。实际上这个Entry是WeakReference
弱引用的子类,这是为了在JVM进行垃圾回收时,能够自动进行回收,防止内存溢出。真正存储数据备份其实就是这个Entry
。
ThreadLocal
类本质实际上是以线程作为key,通过数据备份的方式,实现了线程间的数据隔离!
既然进行的数据备份,那么肯定就会造成数据冗余,并且随着线程的存活时间增长,存储的备份数据也会越来越多,即使线程结束了生命周期,这些备份数据也很有可能仍然存在。这样就可能造成内存泄漏,进而导致OOM!
ThreadLocal为了解决内存泄漏的问题,也进行了一些相应的处理,比如将存储备份数据的Entry类设置为弱引用类型,这是为了方便在GC时自动回收。而且在set()
、get()
方法中增加了数据检查,及时清除掉那些key为null的没用的备份数据。