您现在的位置是:首页 > 文章详情

java源码-ThreadLocal

日期:2018-08-10点击:470

开篇

ThreadLocal主要解决的问题是给每个线程绑定自己的值,这个值是和线程绑定的,是线程的局部变量,是其他线程没法访问的。

ThreadLocal的源码的核心知识点在于ThreadLocal变量如何跟线程绑定ThreadLocal如何实现gc垃圾回收,这篇文章希望能够讲解清楚这两个知识点。

ThreadLocal的实现原理:每一个线程在使用ThreadLocal的时候实际上是以ThreadLocal对象作为key值共享的对象为value值保存在Thread.threadLocalMaps变量中也就是ThreadLocalMap实例,因此ThreadLocal仅仅是作为一个key值来保存,多线程在使用同一个ThreadLocal或者不同的ThreadLocal但是保存相同的共享对象时他们的threadLocalMaps值是相同的,因此如果有共享资源发生冲突问题,ThreadLocal并不能解决!如果要解决并发冲突的问题要么使用安全对象,要么使用上锁机制来保证多线程顺序访问!

ThreadLocal的案例

通过下面例子可以对ThreadLocal的有一个直观的了解。

  • 案例一:主线程main当中存在对象StudentInfo而子线程Thread-0当中不存在该变量,因为主线程通过set设置对象。
  • 案例二:主线程main当中的对象StudentInfo通过set设置而子线程Thread-0当中的对象是初始化函数提供的,两者也不相同。
 /** 创建一个ThradLocal实例 */ private static ThreadLocal<StudentInfo> threadLocal = new ThreadLocal<StudentInfo>(); public static void main(String[] args) { StudentInfo info = new StudentInfo("sdew23", "张三", "男"); // 为主线程保存一个副本StudentInfo对象 threadLocal.set(info); System.out.println(threadLocal.get()); System.out.println(Thread.currentThread().getName()); // 开启子线程 new Thread(new Runnable() { @Override public void run() { // threadLocal.set(info); System.out.println(threadLocal.get()); System.out.println(Thread.currentThread().getName()); } }).start(); } 
img_407e58fb0d7d06f792205b2ea0300572.png




 /** 创建一个ThradLocal实例 */ private static ThreadLocal<StudentInfo> threadLocal = new ThreadLocal<StudentInfo>() { @Override public StudentInfo initialValue() { return new StudentInfo("sss", "小西", "女"); } }; public static void main(String[] args) { StudentInfo info = new StudentInfo("sdew23", "张三", "男"); // 为主线程保存一个副本StudentInfo对象 threadLocal.set(info); System.out.println(threadLocal.get()); System.out.println(Thread.currentThread().getName()); // 开启子线程 new Thread(new Runnable() { @Override public void run() { // threadLocal.set(info); System.out.println(threadLocal.get()); System.out.println(Thread.currentThread().getName()); } }).start(); } 
img_5238a50c872c48a46e3895d7854c4d82.png


ThreadLocal线程隔离的原理

ThreadLocal之所以能够实现线程隔离主要是因为在Thread的类变量当中存在一个ThreadLocal.ThreadLocalMap threadLocals类型对象,这样保证了每个线程能够单独维护一份ThreadLocal对象的map,也就自然而然实现了线程隔离。

ThreadLocal对象的set操作获取当前线程的ThreadLocal.ThreadLocalMap对象,以ThreadLocal对象作为key,以ThreadLocal保存的对象如下例中的StudentInfo为value,保存到ThreadLocalMap当中,每个线程执行set操作都是把对象保存至对应的ThreadLocalMap,所以也就解释了为什么能够隔离了。

ThreadLocal.ThreadLocalMap采用环形数组实现,也就是Entry[] table,set操作就是创建一个Entry对象然后放到环形数组table当中,通过线性探测方法解决冲突问题

// 每个线程包含一个ThreadLocal.ThreadLocalMap对象 public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null; } { private static ThreadLocal<StudentInfo> threadLocal = new ThreadLocal<StudentInfo>(); StudentInfo info = new StudentInfo("sdew23", "张三", "男"); threadLocal.set(info); } public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } public class ThreadLocal<T> { static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private Entry[] table; private void set(ThreadLocal<?> key, Object value) { 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)]) { 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(); } } } 
img_da00f59d2b85f81d9ad57c78a4d6ccb2.png
环形数组


ThreadLocal的get操作

ThreadLocal的get操作从当前Thread获取ThreadLocal.ThreadLocalMap threadLocals对象,在ThreadLocalMap中以ThreadLocal对象作为key返回Entry也即ThreadLocal当中保存的value。

ThreadLocal的get操作内部考虑线性探测方法来解决存储时候的冲突问题,第一次以hash值取值成功则直接返回,如果未取到那么就通过线性探测法继续查找直到null值为止。

 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); } private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; } 


ThreadLocal的gc清理

ThreadLocal的key回收

ThreadLocal的垃圾回收机制是它的一大设计亮点,也是面试当中经常会被问到的问题,这里我们从两个维度进行说明。

  • ThreadLocal当中的Entry的key实现了WeakReference弱引用,所以gc机制可以对key进行回收
  • ThreadLocal当中的Entry的value由于没实现WeakReference弱引用,所以只有程序主动进行回收,在set/get操作中实现回收。
  • 关于回收的细节可以参考文章《一篇文章,从源码深入详解ThreadLocal内存泄漏问题》,作者讲解的非常清楚给我很大启发。
 // 关于key的弱引用实现机制 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } 


ThreadLocal的value回收-get阶段

getEntryAfterMiss过程当中会调用expungeStaleEntry()方法删除过期数据,判断过期数据的标准是对Entry.get()方法返回的key为null(被jvm的gc主动回收了),这个时候需要把对应的value也进行删除。

ThreadLocal的expungeStaleEntry()方法内部除了删除staleSlot指定的过期数据外,还负责检查往后遍历直至遇到数组元素为null停止。在删除过程中还会对某些未过期的数据进行重hash,我认为之所以重hash有可能之前通过线性探测法往后放的,通过重hash后如果发现原来位置为空则可以直接放置hash值对应的下标位置。之所以这么做我个人觉得是因为get或者set的依据都是以null作为结束依据。

get时候重hash是为了把元素放置到hash值第一次对应的位置当中(未经过线性探测法解决碰撞冲突),这样就可以支持以null作为结尾的依据了。

 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; } private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; } 


ThreadLocal的value回收过程

cleanSomeSlots以i作为起始地址,expungeStaleEntry()完成i位置的元素的gc回收,然后继续遍历回收直至遇到null元素的下标i,然后由cleanSomeSlots继续nextIndex()下移到下一个位置继续查找。

 private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; } private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; } 
img_4ab49fb0df5149bc53be50ed36a3e566.png
cleanSomeSlots过程图

说明:

  • 1、如图当前n等于hash表的size即n=10,i=1,在第一趟搜索过程中通过nextIndex,i指向了索引为2的位置,此时table[2]为null,说明第一趟未发现脏entry,则第一趟结束进行第二趟的搜索。

  • 2、第二趟所搜先通过nextIndex方法,索引由2的位置变成了i=3,当前table[3]!=null但是该entry的key为null,说明找到了一个脏entry,先将n置为哈希表的长度len,然后继续调用expungeStaleEntry方法,该方法会将当前索引为3的脏entry给清除掉(令value为null,并且table[3]也为null),但是该方法可不想偷懒,它会继续往后环形搜索,往后会发现索引为4,5的位置的entry同样为脏entry,索引为6的位置的entry不是脏entry保持不变,直至i=7的时候此处table[7]位null,该方法就以i=7返回。至此,第二趟搜索结束;

  • 3、由于在第二趟搜索中发现脏entry,n增大为数组的长度len,因此扩大搜索范围(增大循环次数)继续向后环形搜索;

  • 4、直到在整个搜索范围里都未发现脏entry,cleanSomeSlot方法执行结束退出。


参考文章

ThreadLocal理解与使用
一篇文章,从源码深入详解ThreadLocal内存泄漏问题

原文链接:https://yq.aliyun.com/articles/666327
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章