并发组件之一:ThreadLocal线程本地变量
一、概述
ThreadLocal从字面上进行理解很容易被大部分人认为是本地线程,这是一个错误的理解。ThreadLocal可以理解为Thread局部变量ThreadLocalMap中的key值。
很多文章都会把ThreadLocal当作是解决高并发下线程不安全的一种做法,然而ThreadLocal并不是为了解决并发安全甚至可以这么说,它与真正的并发安全背道而驰。并发安全是指多个线程对同一个对象进行操作而导致的不安全,但是ThreadLocal在每个线程内部保存了一份该对象,使得每个线程都操作自己内部的对象而不影响其它线程。概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而 ThreadLocal 采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
ThreadLocal的主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个ThreadLocal,里面存用户基本信息等,在很多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联系而且当前线程也可以及时获取想要的数据。
二、简单示例
下面是一个使用 ThreadLocal 的例子,每个线程产生自己独立的序列号。
1 public class SequenceNumber {
2 // 创建ThreadLocal的变量
3 private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
4 // 覆盖初始化方法将初始化值定义为0
5 public Integer initialValue() {
6 return 0;
7 }
8 };
9 // 下一个序列号
10 public int getNextNum() {
11 seqNum.set(seqNum.get() + 1);//往ThreadLocalMap中设置值
12 return seqNum.get();
13 }
14 private static class TestClient extends Thread {
15 private SequenceNumber sn;
16 public TestClient(SequenceNumber sn) {
17 this.sn = sn;
18 }
19 // 线程产生序列号
20 public void run() {
21 for (int i = 0; i < 3; i++) {
22 System.out.println("thread[" + Thread.currentThread().getName() + "] sn[" + sn.getNextNum() + "]");
23 }
24 }
25 }
29 public static void main(String[] args) {
30 SequenceNumber sn = new SequenceNumber();
31 // 三个线程产生各自的序列号
32 TestClient t1 = new TestClient(sn);
33 TestClient t2 = new TestClient(sn);
34 TestClient t3 = new TestClient(sn);
35 t1.start();
36 t2.start();
37 t3.start();
38 }
39 }
程序输出结果如下:
1 thread[Thread-1] sn[1]
2 thread[Thread-1] sn[2]
3 thread[Thread-1] sn[3]
4 thread[Thread-2] sn[1]
5 thread[Thread-2] sn[2]
6 thread[Thread-2] sn[3]
7 thread[Thread-0] sn[1]
8 thread[Thread-0] sn[2]
9 thread[Thread-0] sn[3]
从运行结果可以看出,使用了 ThreadLocal 后,每个线程产生了独立的序列号,没有相互干扰。这也就是ThreadLocal的本质:在每个线程内部创建了一个属于该线程的局部变量保存值,线程之间不会相互受到影响。
三、实现原理
首先可以看一下ThreadLocal的类结构:
通过表格的方式我们看看ThreadLocal中到底有些啥:
ThreadLocalMap | ThreadLocal内部类,是Thread中的真正的局部变量保存在Thread中。和Map对象很像同样是key-value格式的数据 |
nextHashCode | ThreadLocalMap中会用到,HashMap中是通过链表发解决冲突的,而ThreadLocalMap是通过开地址法解决。其值是Atomic类型的通过getAndAdd方法直接赋值 |
HASH_INCREAMENT | hash值的增量。这个数字很有趣,是为了优化开地址法解决冲突的查找元素提高效率使用 |
下面的就是ThreadLocal、Thread、ThreadLocalMap之间的关系图:
从图中可以看出ThreadLocalMap并不是从ThreadLocal中取出的而只是定义在ThreadLocal中的一个内部类而真正存在是在Thread中的。了解完ThreadLocal的组成可以看看ThreadLocal中一些关键的方法:
get():T | 从ThreadLocal中获取值操作,调用的是内部类ThreadLocalMap中的getEntry()方法 |
set(T):void | 给ThreadLocal设置值,底层调用的是ThreadLocalMap中的set(ThreadLocal<?>, Object)方法 |
remove():void | 从ThreadLocal中移除元素方法,调用底层ThreadLocalMap中的remove(ThreadLocal<?>)方法 |
下面就是具体的对应方法的源码分析:
get():从ThreadLocal中获取值
1 public T get() {
2 Thread t = Thread.currentThread();//获取当前线程
3 ThreadLocalMap map = getMap(t);//获取ThreadLocalMap对象,这个对象是从Thread中进行获取的而不是ThreadLocal对象中,从getMap方法中就可以看出
4 if (map != null) {
5 ThreadLocalMap.Entry e = map.getEntry(this);//获取ThreadLocalMap中的entry对象
6 if (e != null)//如果存在entry对象
7 return (T)e.value;//返回值
8 }
9 return setInitialValue();//返回初始化的值
10 }
getMap(Thread thread):通过线程获取ThreadLocalMap对象
1 ThreadLocalMap getMap(Thread t) {
2 return t.threadLocals;//返回线程中的局部变量
3 }
getEntry(ThreadLocal key):从ThreadLocalMap中获取相应的value值,ThreadLocalMap中的方法
1 private Entry getEntry(ThreadLocal key) {
2 int i = key.threadLocalHashCode & (table.length - 1);//通过hash取模获取数组中具体的位置
3 Entry e = table[i];//获取该数组中的Entry节点
4 if (e != null && e.get() == key)//如果entry节点不为null并且获取的key是当前的ThreadLocal引用
5 return e;//返回节点
6 else
7 return getEntryAfterMiss(key, i, e);//当获取到的key为null或者不是当前线程的key就执行这一步清理
8 }
getEntryAfterMiss(ThreadLocal key,int i,Entry e):当找不到key的时候就会调用这个方法,ThreadLocalMap中的方法
1 private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
2 Entry[] tab = table;//获取table
3 int len = tab.length;//数组长度
4
5 while (e != null) {
6 ThreadLocal k = e.get();//获取ThreadLocal引用
7 if (k == key)//如果是当前Threadlocal对象就返回这个Entry对象
8 return e;
9 if (k == null)//如果key为null
10 expungeStaleEntry(i);//进行重新hash来删除陈旧的条目
11 else
12 i = nextIndex(i, len);//不停的往下一个进行找
13 e = tab[i];//将该位置的元素赋值给e再重复以上的操作
14 }
15 return null;
16 }
expungeStaleEntry(int i):进行重新hash来删除旧的条目,ThreadLocalMap中的方法
1 private int expungeStaleEntry(int staleSlot) {
2 Entry[] tab = table;//Entry数组
3 int len = tab.length;//数组长度长度
4
6 tab[staleSlot].value = null;//该位置的Entry的值为null
7 tab[staleSlot] = null;//该entry节点为null,全部为null有助于GC操作
8 size--;//长度自减
11 Entry e;
12 int i;
13 for (i = nextIndex(staleSlot, len);//从当前i的后一个节点开始进行循环直到下一个数组中的节点元素为null
14 (e = tab[i]) != null;
15 i = nextIndex(i, len)) {//这个过程其实是一个无限循环的过程,终止的条件就是某一个节点为null
16 ThreadLocal k = e.get();//获取ThreadLocal引用
17 if (k == null) {//删除key为null的值
18 e.value = null;//赋值null
19 tab[i] = null;//赋值null为了方便GC回收
20 size--;
21 } else {
22 int h = k.threadLocalHashCode & (len - 1);//取模获取key所在的数组的下标
23 if (h != i) {//如果不相等
24 tab[i] = null;//该位置节点为null
28 while (tab[h] != null)//节点如果不为null
29 h = nextIndex(h, len);//说明有冲突就查看下一个节点,直到h位置上为null时
30 tab[h] = e;//进行一次赋值
31 }
32 }
33 }
34 return i;
35 }
setInitValue():设置初始值方法,ThreadLocalMap中的方法
1 private T setInitialValue() {
2 T value = initialValue();//获取初始值,初始值为null,ThreadLocal中提供了获取初始值方法返回的是一个null
3 Thread t = Thread.currentThread();//获取当前线程
4 ThreadLocalMap map = getMap(t);//从当前线程中获取ThreadLocalMap对象
5 if (map != null)//存在就赋值
6 map.set(this, value);//通过调用ThreadLocalMap进行设置值操作,这个放下面set方法中去讲
7 else
8 createMap(t, value);//创建一个ThreadLocalMap出来
9 return value;//第一个值是null
10 }
createMap(Thread thread,T firstValue):创建一个新的ThreadLocalMap对象
1 void createMap(Thread t, T firstValue) {
2 t.threadLocals = new ThreadLocalMap(this, firstValue);
3 }
get()方法是从ThreadLocal对象中获取属于每个线程自己的局部变量方法,经历了上面这么多方法可以总结一下get()方法的流程:
调用get()方法,通过当前线程获取到线程内部ThreadLocalMap线程局部变量。
判断当前是否存在ThreadLocalMap对象,如果存在就通过ThreadLocal对象获取值。
如果通过ThreadLocal对象找到了符合数组中的位置并且存在就返回值。
如果通过ThreadLocal对象没找到或者找到了也是不是当前的ThreadLocal对象此时就会对ThreadLocalMap进行一次清理,然后进行重新的hash排放元素
如果不存在就创建一个新的ThreadLocalMap对象并赋予初始值返回设定的初始值。
set(T value):给ThreadLocal设置值操作
1 public void set(T value) {
2 Thread t = Thread.currentThread();//获取当前线程
3 ThreadLocalMap map = getMap(t);//获取线程中的局部变量
4 if (map != null)//如果不为null直接设置值
5 map.set(this, value);//ThreadLocalMap中的设置方法
6 else
7 createMap(t, value);//创建一个新的ThreadLocalMap
8 }
设置值方法很简单,就是判断当前是否存在符合的ThreadLocalMap对象如果存在就在设置值,如果不存在就创建一个新的ThreadLocalMap并设置值。
set(ThreadLocal<?> key,Object value):通过给定的key和value设置值,ThreadLocalMap中的方法
1 private void set(ThreadLocal<?> key, Object value) {
2
4 Entry[] tab = table;//获取表
5 int len = tab.length;//表长度
6 int i = key.threadLocalHashCode & (len-1);//取模获取的值
7
8 for (Entry e = tab[i];
9 e != null;
10 e = tab[i = nextIndex(i, len)]) {//从当前位置开始,判断条件是不为null,自增开始找循环的到了末尾就从头开始
11 ThreadLocal<?> k = e.get();//获取ThreadLocal对象
12
13 if (k == key) {//如果Key相等代表找到
14 e.value = value;//将值赋值一下
15 return;
16 }
17
18 if (k == null) {//如果key不存在
19 replaceStaleEntry(key, value, i);//进行清理操作,会清理key被回收的节点
20 return;
21 }
22 }
23
24 tab[i] = new Entry(key, value);//在该位置上进行一个新的处理
25 int sz = ++size;//长度自增
26 if (!cleanSomeSlots(i, sz) && sz >= threshold)//如果超过了容量
27 rehash();//扩容操作
28 }
rehash():对当前的ThreadLocalMap进行扩容操作
1 private void rehash(){
3 expungeStaleEntries();//整体清理一下表
5 if(size >= threshold - threshold/4){//当长度大于原长度的四分之三时进行扩容
7 resize();//真正扩容的操作
9 }
11 }
resize():真正执行扩容的方法
1 private void resize(){
2 Entry[] oldTab = table;
4 int oldLen = oldTab.length;//原来的长度
6 int newLen = oldLen * 2;//扩容两倍
8 Entry[] newTab = new Entry[newLen];//新长度数组
10 int count = 0;
12 for(int j = 0;j<oldLen; ++j){//遍历老的数组
14 Entry e = oldTab[j];
16 if(e != null){
18 ThreadLocal k = e.get();//获取key值
20 if(k == null){
22 e.value = null;//把value置为null是为了方便GC回收
24 }else{
26 int h = k.threadLocalHashCode & (newLen -1);//映射到新数组的位置
28 while(newTab[h] != null){
30 h = newIndex(h,newLen);//往下一个位置进行查找
32 }
34 newTab[h] = e;//这一步就是解决冲突的地方,当key存在相同的hash值就往下一位找直到不为null就插入进去
36 count++;
38 }
40 }
42 }
44 setThreshold(newLen);//设置新的长度
46 size = count;
48 table = newTab;//将新数组赋值给老的
50
51 }
整个扩容的流程不难,就是通过遍历原来的数组将值一个个拿出来当发现存在key为null的就先清理掉,然后将原本的key进行再次定位到新数组中的位置,这里存在一个冲突的可能性,就通过线性探测法查看下一个是否为null直到找到一个为null的就把key-value存放进去。
四、ThreadLocal存在的一些问题
ThreadLocal实现方面并不是很难,通过源码你也会发现在源码层面大量的做了相关key为null时候的处理,那么为什么key会无缘无故为null呢,明明存了ThreadLocal对象进去的。很多地方都会称这个为现象为内存泄漏,当长时间以后整个ThreadLocalMap中都会出现大量的key为null的节点。而由于vlaue又是强引用导致这个节点无法被删除,最终ThreadLocalMap无限变大撑爆了内存。
经过上面的了解我们知道存放在ThreadLocalMap中的key是一个弱引用类型的ThreadLocal对象,弱引用对象是指在系统进行第二次垃圾回收的时候一般就会被回收掉导致成为null。如果不对key为null的Entry节点做处理,就会导致ThreadLocalMap中存在很多无法被外界使用的元素而且都是key为null的值,这样就会很浪费存储空间。因此在ThreadLocal中每次进行set操作都会进行查看是否需要进行清理。当然一般程序中可以通过static关键字进行修饰这样就不会导致这些问题的出现。
五、总结
上面分析了这么多源码,是比较细节地来看ThreadLocal了。对这些内容做一个总结,ThreadLocal的原理简单说应该是这样的:
ThreadLocal不需要key,因为线程里面自己的ThreadLocal.ThreadLocalMap不是通过链表法实现的,而是通过开地址法实现的
每次set的时候往线程里面的ThreadLocal.ThreadLocalMap中的table数组某一个位置塞一个值,这个位置由ThreadLocal中的threadLocaltHashCode取模得到,如果位置上有数据了,就往后找一个没有数据的位置
每次get的时候也一样,根据ThreadLocal中的threadLocalHashCode取模,取得线程中的ThreadLocal.ThreadLocalMap中的table的一个位置,看一下有没有数据,没有就往下一个位置找
既然ThreadLocal没有key,那么一个ThreadLocal只能塞一种特定数据。如果想要往线程里面的ThreadLocal.ThreadLocalMap里的table不同位置塞数据 ,比方说想塞三种String、一个Integer、两个Double、一个Date,请定义多个ThreadLocal。
==================================================================================
不管岁月里经历多少辛酸和艰难,告诉自己风雨本身就是一种内涵,努力的面对,不过就是一场命运的漂流,既然在路上,那么目的地必然也就是前方。
==================================================================================