并发组件之一:ThreadLocal线程本地变量
Posted wait-pigblog
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发组件之一:ThreadLocal线程本地变量相关的知识,希望对你有一定的参考价值。
一、简介
ThreadLocal从字面上进行理解很容易被大部分人认为是本地线程,然而ThreadLocal并不是一个Thread,可以说它只是一个容器,而它装的内容又是Thread的局部变量。很多文章都会把ThreadLocal当作是解决高并发下线程不安全的一种做法,然而ThreadLocal并不是为了解决并发安全甚至可以这么说,它与真正的并发安全背道而驰。并发安全是指多个线程对同一个对象进行操作而导致的不安全,但是ThreadLocal在每个线程内部保存了一份该对象,使得每个线程都操作自己内部的对象而不影响其它线程。概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而 ThreadLocal 采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
二、简单示例
下面是一个使用 ThreadLocal 的例子,每个线程产生自己独立的序列号。就是使用ThreadLocal存储每个线程独立的序列号,线程之间互不干扰。
1 public class SequenceNumber {
2 // 定义匿名子类创建ThreadLocal的变量
3 private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
4 // 覆盖初始化方法
5 public Integer initialValue() {
6 return 0;
7 }
8 };
9 // 下一个序列号
10 public int getNextNum() {
11 seqNum.set(seqNum.get() + 1);
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,ThreadLocalMap跟Map有点像都是key-value的内部结构,通过key获取值。只不过ThreadLocalMap中的key是一个弱引用类型的ThreadLocal对象。除了一个ThreadLocalMap静态内部类,ThreadLocal对象还定义了其常用的几个方法,接下来就可以通过源码看看ThreadLocal的实现原理。在讲解源码之前,必须了解几个内部变量:
threadLocalHashCode:ThreadLocal的hash值,每次都会进行一个自增的操作。(为什么这样后面会进行研究)
HASH_INCREMENT:hash值的增量也就是每次增加的hash值。
get():从ThreadLocal中获取值
1 public T get() {
2 Thread t = Thread.currentThread();//获取当前线程
3 ThreadLocalMap map = getMap(t);//获取ThreadLocalMap对象,这个对象是从Thread中进行获取的而不是ThreadLocal对象中
4 if (map != null) {
5 ThreadLocalMap.Entry e = map.getEntry(this);//获取ThreadLocalMap中的entry对象
6 if (e != null)
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值
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的时候就会调用这个方法
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对象就返回
8 return e;
9 if (k == null)//如果key为null
10 expungeStaleEntry(i);//进行重新hash来删除陈旧的条目
11 else
12 i = nextIndex(i, len);//不停的往下一个进行找,清理整个ThreadLocalMap
13 e = tab[i];
14 }
15 return null;
16 }
expungeStaleEntry(int i):进行重新hash来删除旧的条目
1 private int expungeStaleEntry(int staleSlot) {
2 Entry[] tab = table;//Entry数组
3 int len = tab.length;//数组长度长度
4
6 tab[staleSlot].value = null;//该值为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();
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);//往下找直到为null
30 tab[h] = e;//进行一次赋值
31 }
32 }
33 }
34 return i;
35 }
setInitValue():设置初始值方法
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进行设置值操作
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进行一次清理。
如果不存在就创建一个新的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);
6 else
7 createMap(t, value);//创建一个新的ThreadLocalMap
8 }
设置值方法很简单,就是判断当前是否存在符合的ThreadLocalMap对象如果存在就在设置值,如果不存在就创建一个新的ThreadLocalMap并设置值。
既然ThreadLocalMap类似与Map,它也有扩容的操作,只不过和Map的不同在于处理key的hash相同与否方面有些不一样。
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对象进去的。这就是ThreadLocal存在的一个问题。经过上面的了解我们知道存放在ThreadLocalMap中的key是一个弱引用类型的ThreadLocal对象,弱引用对象是指在系统进行第二次垃圾回收的时候一般就会被回收掉导致成为null。如果不对key为null的Entry节点做处理,就会导致ThreadLocalMap中存在很多无法被外界使用的元素而且都是key为null的值,这样就会很浪费存储空间。因此在ThreadLocal中每次进行set操作都会进行查看是否需要进行清理。当然一般程序中可以通过static关键字进行修饰这样就不会导致这些问题的出现。
五、总结
ThradLocal主要是为了让每个线程都操作自己内部的变量而不会影响其它的线程,很适合那些可以每个线程都存放一份属于自己变量的操作。ThreadLocal底层实现也不是很难,主要就是对ThreadLocalMap的操作,类似于Map的操作都是存放key-value的格式。
==================================================================================
不管岁月里经历多少辛酸和艰难,告诉自己风雨本身就是一种内涵,努力的面对,不过就是一场命运的漂流,既然在路上,那么目的地必然也就是前方。
==================================================================================
以上是关于并发组件之一:ThreadLocal线程本地变量的主要内容,如果未能解决你的问题,请参考以下文章
Java并发机制--ThreadLocal线程本地变量(转)