写在前面
最近在研究JUC下面的并发工具类的源码,看了ThreadLocal的源码与实现原理,虽然我在网上也看到很多了ThreadLocal解析的文章,但是总感觉讲的太浅或者有些讲的不对,就自己写了一篇。笔者水平有限,文中如有疏漏欢迎各位读者批评指正。
ThreadLocal的作用
ThreadLocal是一种用线程本地存储来实现线程安全的方式,它的作用是将线程共享的变量在每个线程本地存储一份副本,每一个线程都可以独立地改变和获取自己的副本,而不会和其它线程的副本冲突。每一个ThreadLocal对象都能创建一个可以被多线程进行本地存储的共享变量,ThreadLocal对象和共享变量是一一对应的关系。
ThreadLocal提供了set()与get()等方法,用来设置和获取当前调用此方法的线程中的存储的变量副本。
例如以下代码为ThreadLocal的简单使用方式:
public class ThreadLocalTest {
//创建一个线程共享的变量
static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal1.set("localVarMain");
threadLocal2.set(100);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程1中本地变量的值
threadLocal1.set("localVar1");
threadLocal2.set(200);
System.out.println("thread1中的本地变量的值是:"+threadLocal1.get());
System.out.println("thread1中的本地变量的值是:"+threadLocal2.get());
}
},"thread1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程1中本地变量的值
threadLocal1.set("localVar2");
threadLocal2.set(300);
System.out.println("thread2中的本地变量的值是:"+threadLocal1.get());
System.out.println("thread2中的本地变量的值是:"+threadLocal2.get());
}
},"thread2");
t1.start();
t2.start();
System.out.println("主线程中的本地变量的值是:"+threadLocal1.get());
System.out.println("主线程中的本地变量的值是:"+threadLocal2.get());
}
}
最后输出的结果为:
可以看到每个线程对共享变量的修改互不影响,因为只是修改当前线程本地存储的副本。
ThreadLocal源码与实现原理
ThreadLocal的数据结构
一、变量副本的存储方式
ThreadLocal的作用是让线程在本地存储共享变量的副本,副本是存储在线程Thread对象的属性中,因此确切的说ThreadLocal本身不具备数据结构,但是线程对象用来存储共享变量副本的属性是有数据结构的。因为每个线程对象可能需要存储不止一个共享变量的副本,所以该属性必须是一个集合、数组、哈希表等可以包含多个元素的类型。
在当前的JDK中,Thread采用了哈希表来存储共享变量的副本。
二、关于哈希表
散列表也叫做Hash表,是根据键值来计算出关键字在表中地址的一种数据结构,而这种计算方式被称为散列函数。也就是说,Hash表建立了键值与存储地址之间的一种直接映射关系。
这么说可能过于学术化,更加直白的表述就是通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录(哈希表使用数组为存储方式,这里的位置可以理解为数组的下标),这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
哈希表的存储方式是数组,但是哈希表和数组不是一个概念。数组是编程语言提供的一种数据类型,即用一组连续的内存空间来存放数据,可以通过一个首地址和一个数组下标,直接访问这组内存空间中的任意位置。哈希表则是一种数据结构,是以数组为存储方式,实现的一种可以快速查找数据的数据结构。它是将数据的值通过一个映射函数,求出一个散列结果,然后把数据放在这个结果对应的数组下标的位置。哈希表可以说数组+链表,也可以是数组+二叉树等结构。
如下图所示:每个线程都有一个哈希表,里面存储着共享变量的本地副本value1、value2等。
ThreadLocal源码
一、共享变量的键值与散列函数
既然线程中使用哈希表来存储共享变量的本地副本,那么每个变量就需要一个键值来映射自己在哈希表中的位置,这个键值就保存在创建该共享变量的ThreadLocal对象的属性里面。
ThreadLocal中定义了一个原子操作类AtomicInteger型的变量nextHashCode,然后每创建一个新的ThreadLocal对象,都会在nextHashCode上增加一个数值0x61c88647,将增加后的值作为该ThreadLocal对象所要创建的共享变量的键值,使用threadLocalHashCode表示。
//共享变量的键值,调用nextHashCode()得到
private final int threadLocalHashCode = nextHashCode();
//定义一个静态原子变量,通过每次对该变量增加一个HASH_INCREMENT值来
//得到新的threadLocalHashCode
private static AtomicInteger nextHashCode =
new AtomicInteger();
//计算新的threadLocalHashCode每次需要对nextHashCode增加的数值
private static final int HASH_INCREMENT = 0x61c88647;
//使用AtomicInteger的nextHashCode()方法对nextHashCode进行增加
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
二、共享变量本地副本在线程中的存储方式
1、哈希表中节点
在Thread类的源码中,定义了一个ThreadLocalMap类型的属性threadLocals,用来存储线程本地的共享变量副本的集合。ThreadLocalMap内部封装了一个Entry类型的哈希表,每个Entry实例中存储了共享变量在本地的副本。
但是JDK源码中为什么不直接在哈希表中存放共享变量的值,而要存放Entry类型的节点呢?且看下面的Entry类型的源码:
//哈希表中的节点,一个节点用来存储一个共享变量的本地副本
static class Entry extends WeakReference<ThreadLocal<?>> {
//共享变量的本地副本
Object value;
//使用软引用连接创建此共享变量的ThreadLocal对象
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
首先为什么不能在哈希表中直接存储共享变量的值,而是采用Entry类进行封装,是因为以下两点:
1、与哈希冲突有关
虽然ThreadLocal对象中存储着共享变量的键值 threadLocalHashCode,可以使用键值通过散列函数就可以知道其创建的共享变量在哈希表中的位置。但是哈希表存在一种叫做哈希冲突的情况,即多个键值通过散列函数计算而来的散列值是一样的。
ThreadLocalMap使用线性探测法来解决哈希冲突,即发生哈希冲突时,依次从冲突的位置开始一个个的探测哈希表的下一个位置是否为空,找到空位置就插入。鉴于ThreadLocalMap使用线性探测法来解决哈希冲突,只用ThreadLocal对象中存储的键值通过散列函数一次定位在哈希表共享变量的位置是不行的,因为散列函数计算出的位置并不一定是共享变量在哈希表中的位置。最好的方法就是哈希表中不直接存放共享变量的副本值,而是用Entry类封装共享变量的副本,同时Entry里面包含指向ThreadLocal对象的引用,以便在使用ThreadLocal对象查询哈希表中的变量值时直接比对。
因此,如果调用ThreadLocal的get()方法在当前线程中的ThreadLocalMap中查询一个ThreadLocal对象创建的共享变量本地副本的话,必须要在哈希表的节点中也存放着指向ThreadLocal对象的引用。
2、方便垃圾回收
如果一个共享变量已经不需要使用了,创建它的ThreadLocal对象已经被垃圾回收,但是其在各个线程中存放的副本依然存在。这个时候每个线程则可以通过判断ThreadLocal对象是否还存在,来决定是否删除该ThreadLocal对象创建的变量副本是否需要从哈希表中删除。
其次,为什么需要使用弱引用指向ThreadLocal对象?
我们先来看,在Java中有四种引用类型:
①强引用
在 Java 中最常见的就是强引用, 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
②软引用
软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
③弱引用
它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
④虚引用
最弱的一种引用关系,不会对生存时间造成影响,也无法通过虚引用获取对象,设置虚引用的唯一目的是在对象被回收时收到一个系统通知。
弱引用的作用是让自己知道所引用的目标,但是不会因为自己的引用影响垃圾回收。为什么要这样做呢?其实也和垃圾回收有关。如果使用强引用的话,直接在Entry中定义threadLocal属性指向ThreadLocal对象。假如某个时候某个共享变量不需要使用了,程序员只是threadLocal=null在主线程中将ThreadLocal对象的引用清空,但是没有使用相关方法(remove()方法)在各个已经存储了共享变量副本的线程中将哈希表中Entry对象中的threadLocal属性置为null,那么该ThreadLocal对象仍然在GC Roots引用链上,不会被垃圾回收。而弱引用的效果就是,可以通过弱引用来获取对象,但是弱引用不影响垃圾回收。
三、ThreadLocalMap类
ThreadLocalMap类封装了用来存储Entry节点的哈希表,以及哈希表中元素个数、哈希表的扩容阈值等。
此处的代码应该注意以下几点:
1、计算共享变量在哈希表中位置的散列函数是:threadLocalHashCode & (len-1),其中threadLocalHashCode 是存储在ThreadLocal对象中的键值,len为哈希表的长度。
2、当两个以上共享变量经过散列函数计算得到的位置相同,即发生了哈希冲突时,ThreadLocalMap采用线性探测法解决哈希冲突。即如果valueX的散列值是3,应该存放在哈希表中第3个位置上,但是3位置非空,就从3位置开始往后遍历哈希表,直到找到一个空位置就把valueX放进去。
3、假如某个时候某个共享变量不需要使用了,程序员只是threadLocal=null在主线程中将ThreadLocal对象的引用清空,但是每个线程的哈希表中的Entry对象仍然存在,这就造成哈希表中存在垃圾对象。为了防止内存泄露,ThreadLocalMap在每次调用remove()等方法时,都会调用expungeStaleEntry(int staleSlot)方法对哈希表进行一次清理。清理的流程为:从staleSlot位置开始检查哈希表,如果找到节点指向的ThreadLocal对象已经为空,就删除节点。否则检查节点通过散列函数计算的散列值 h 和节点当前位置 i 是否相同,不相同的话则将节点移到哈希表中离 h 最近的一个空位置上去,相当于让节点在哈希表中的排布更加紧密。
static class ThreadLocalMap {
//哈希表中的节点,一个节点用来存储一个共享变量的本地副本
static class Entry extends WeakReference<ThreadLocal<?>> {
//共享变量的本地副本
Object value;
//使用软引用连接创建此共享变量的ThreadLocal对象
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//哈希表的初始容量
private static final int INITIAL_CAPACITY = 16;
//哈希表
private Entry[] table;
//表中元素个数
private int size = 0;
//扩容阈值
private int threshold; // Default to 0
//重置扩容阈值
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
//表中的下一个位置索引
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
//表中的上一个位置索引
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
//初始化ThreadLocalMap的构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建哈希表
table = new Entry[INITIAL_CAPACITY];
//通过散列函数计算Entry节点在哈希表中的位置,计算的键值是threadLocalHashCode
//散列函数是threadLocalHashCode & (INITIAL_CAPACITY - 1)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//Entry节点存入哈希表中
table[i] = new Entry(firstKey, firstValue);
//哈希表中元素数量变为1
size = 1;
//重置扩容阈值
setThreshold(INITIAL_CAPACITY);
}
//如果使用的是InheritableThreadLocal,需要把父线程的共享变量拷贝给子线程
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
//获得一个ThreadLocal对象极其创建的共享变量在当前线程中的副本
private Entry getEntry(ThreadLocal<?> key) {
//通过散列函数计算节点在哈希表中的初始位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//如果该位置的Entry节点确实是ThreadLocal对象创建的
if (e != null && e.get() == key)
return e;
else
//说明发生了哈希冲突,在哈希表中从i位置开始,
//从前向后继续寻找变量所在的节点
return getEntryAfterMiss(key, i, e);
}
//在哈希表中从第i个位置开始,从前到后遍历寻找变量所在的节点
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//先看第i个节点,取出其指向的ThreadLocal对象进行比较
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
//如果key=null,说明ThreadLocal对象对象被回收了,此时说明哈希表中存在
//已经不需要的变量值,需要调用expungeStaleEntry方法进行清理
if (k == null)
expungeStaleEntry(i);
else
//循环遍历下一个节点
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
//更新将哈希表中的某个ThreadLocal对象创建的变量值
private void set(ThreadLocal<?> key, Object value) {
//先通过散列函数找到初始位置
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//如果初始位置的Entry节点指向的ThreadLocal对象就是需要查询的,
//直接设置value值,否则说明插入时发生了哈希冲突,则遍历哈希表
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();
}
//删除哈希表中节点的方法
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);
return;
}
}
}
//从哈希表中删除节点,并且对后面的节点进行一次检查,
//如果找到节点指向的ThreadLocal对象已经为空,就删除节点
//否则检查节点的散列值h和位置i是否相同,不相同的话则将节点移到
//离h最近的一个空位上去,相当于让节点排布更加紧密
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//删除节点
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
//整理哈希表中节点排布,
Entry e;
int i;
//从下一个节点开始,遍历哈希表
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//发现指向的ThreadLocal对象已经不在,删除节点
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//重排节点的位置,先计算散列值h,表示未发生哈希冲突时,
//节点应该在哈希表中的位置
int h = k.threadLocalHashCode & (len - 1);
//如果发现位置偏离,说明节点插入哈希表时发生了哈希冲突,被插入到了后面
if (h != i) {
//先把节点从此处移走,即把哈希表i位置清空
tab[i] = null;
//从h位置开始遍历哈希表,直到找到一个空位置
while (tab[h] != null)
h = nextIndex(h, len);
//将节点放入离h最近的空位置
tab[h] = e;
}
}
}
return i;
}
}
四、共享变量在线程中本地副本值的获取和更新
ThreadLocal类提供了get()方法来获取共享变量在线程中的副本值,其源码为:
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程中存储共享变量副本的哈希表
ThreadLocalMap map = getMap(t);
//如果哈希表不为null
if (map != null) {
//使用当前ThreadLocal对象从哈希表中找到变量副本值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果线程中的哈希表为空,初始化哈希表
return setInitialValue();
}
其中使用的getMap(t)方法就是从线程对象中取出其ThreadLocalMap属性:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocal类提供了set()方法来获取共享变量在线程中的副本值,其源码为
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//取出当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果ThreadLocalMap对象不为空
if (map != null)
//存入value值,其中的实参this是当前ThreadLocal对象
map.set(this, value);
else
//否则创建ThreadLocalMap对象
createMap(t, value);
}
由此可见,ThreadLocal的get方法和set方法都是先调用Thread.currentThread()方法获取当前线程Thread,然后通过线程对象获取线程的ThreadLocalMap,每个线程都有一个自己的ThreadLocalMap,ThreadLocalMap里就保存着所有的ThreadLocal变量。最后在线程的ThreadLocalMap中对变量进行查询修改等操作,因此操作的都是每个线程在本地的副本。