1 一句话概括ThreadLocal

  什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象的线程创建了一个独立的变量副本。

2 ThreadLocal使用场景

  用一句话总结ThreadLocal真的实在是太苍白无力了!我们通过一个简单的例子入手。比如现在有A和B两台服务器需要通过http请求传递数据,但又希望数据安全性有一定保障,因此发送方A决定用AES算法对传输数据加密后再发送给B。接受方B收到数据后,通过密钥解密数据并进行后续的业务处理。
  对数据进行AES解密,接收方B可选择JAVA提供的Cipher类来实现。我们在调用Cipher类进行解密时时,需要获取Cipher对象的实例,如下所示:

Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");

  接着我们调用该实例就可以进行数据解密工作。但很不幸的是,Cipher类存在线程安全问题,它无法工作于多线程场景下。简单来说就是单个Cipher实例无法同时解密多条数据。
  那怎么办呢?
有没有什么办法能让每个线程拥有相同的instance实例,且彼此互不干扰呢?这个时候我们可以借助ThreadLocal类来实现特定功能,ThreadLocal能够为每个使用该对象的线程创建独立的变量的副本。这样就满足了我们的需求。

  后续篇幅我们会深入到ThreadLocal的源码层来探讨它的实现机制,在看ThreadLocal类的几个基本方法前,让我们先看一下ThreadLocal中静态类ThreadLocalMap的实现,它对于我们理解ThreadLocal有着举足轻重的作用!

3 ThreadLocalMap源码分析

  ThreadLocalMap是ThreadLocal类中的一个静态类,它拥有一个Entry数组类型的成员变量,名为table。这个Entry是啥?我们来看看。

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

  Entry类是ThreadLocalMap中的一个静态类,它继承了WeakReference类,同时拥有一个类型为Object的value成员变量。当我们创建Entry对象后,调用Entry.get()方法,获取到的实际上是ThreadLocal对象的弱引用。而这个设计则保证了Entry对象中保存的ThreadLocal弱引用是易被回收的。网上有很多关于ThreadLocal对象是否会引发内存泄漏的文章,这里的内存泄漏通常指的不是entry的key,也就是ThreadLocal的弱引用。而是这里的value对象。实际上ThreadLocalMap自身提供了一套回收无用Entry节点的机制。在后面我们会聊到它的实现。关于ThreadLocal是否会引发内存泄漏,这里暂时不做探讨。

3.1 ThreadLocalMap.set()方法

private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.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();
}

  ThreadLocalMap类持有一个Entry数组,名为table。当我们调用ThreadLocalMap的set()方法时,其实就是更新table中某个Entry,或往table中插入一个Entry。set()方法其实是根据threadLocal对象的threadLocalHashCode来计算当前Entry节点应落入什么位置。当然存在多个entry落入位置发生冲突的情况,在ThreadLocal中使用了线性探测法来解决冲突。知道了这一点,那set方法的实现思路就很清晰了。就是找一个位置让我放节点嘛!如果已经有现成的了,更新一下value就行。要是找不到,那我就按线性探测法来找落入位置就好了嘛。值得注意的是replaceStaleEntry()方法!当entry节点不为空,而key为null时会调用这个方法。这个方法看名字好像是用来替换过时的Entry节点的?我们来看一下它到底是干嘛的?

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;tab[i] = tab[staleSlot];
tab[staleSlot] = e;// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

  这个方法好像还挺长的,上面我们已经猜测过它的用途了,现在我们就来揭开它的庐山真面目。
  首先关注一个这个方法的参数:

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot)

  第一个参数就是我们的key,估摸着还是用来计算hash值找位置的;第二个就是要放的value了;第三个staleSlot呢?我们上面好像是找到了一个key为null的entry节点吧?没错,这个staleSlot就是这个节点在tab中的位置了。然后从这个staleSlot节点开始往前找,如果发现某个entry不为空,但key等于null,用slotToExpunge记录下它的位置,直到往前找到一个entry为null的节点停止。这个slotToExpunge是用来干嘛的呢?后面会提到。
  我们接着看。往前找完之后,我们又从staleSlot的下一个节点开始往后找,如果发现了某个节点的键值等于我们的key。我们是不是应该用我们的value替换掉这个位置原先的值呢?好像是应该替换。但是别忘记了前面还有个key为null的entry节点呢!由于之前key为null的节点和当前节点计算出来hash值其实是一样的。这里我们将e节点的值更新为最新的value后,互换tab[i]和entry的位置。这一步的目的是什么呢?我猜大概是这样的,因为ThreadLocalMap是根据线性探测法来解决冲突的,因此可能会出现key的哈希值相同但散落位置不连续的情况。为了在一定程度上提高查找哈希值相同entry节点的效率,交换一下位置会是更好的选择。同时接下来会执行cleanSomeSlots()方法。我们上面的for循环会一直往后找,直到发现一个null节点为止。如果找到了null节点,那就说明按照线性探测法找不到这个节点了啊!那咋办呢?staleSlot节点不是空着呢么。直接塞进去不就完事了。。
  最后一句又调用了cleanSomeSlots()方法。下面就轮到它了。。

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;
}

  在看cleanSomeSlots()方法前,还得看expungeStaleEntry()。有时候不得不说,好的java命名规范,真的是很重要啊!看到这个方法的名字就知道它大概是用来清理过期节点的。回想一下,有什么节点是需要我们的清理的吗?好像有。。前面是不是有找到过key为null的entry节点啊?这个key为null的节点好像没啥用啊!

cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

  还记得这个slotToExpunge吗?不记得了的话往前翻一翻。这个slotToExpunge位置指向了一个key为null的entry节点。既然知道这个节点是没用的,那它就应该被回收。这里就很直接粗暴了,直接把它直接null以待后面垃圾回收器清理。清理完之后,又是一个for循环。如果key为null,将该entry置为null。如果不为null,重新计算一下hash值,如果位置与当前位置不同,需要重新找一个位置放该节点。当然也是利用线性探测法了,找到连续位置后面第一个为null的节点放置。
  最后返回的节点为从slotToExpunge往后的第一个值为null的entry节点。
  再看看cleanSomeSlots方法主体。

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;
}

  这个方法顾名思义是用来清理某些节点的。清理啥节点呢?还是那些不为null但是key为null的节点。参数n决定了for循环要执行的次数。>>>在java中是无符号位移的意思,也就是说如果每次循环tab[i]均不需要清理,最多会执行logn次。如果有需要清理的节点,就会调用expungeStaleEntry()方法去回收这个节点。
  上面说了这么一大堆,终于把ThreadLocalMap的set()方法说完了。下面接着来看getEntry()方法。

3.2 ThreadLocalMap.getEntry()方法

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);
}

  getEntry()方法比较简单。先根据key值计算出对应在table中的位置,如果table[i]的key值和我们的参数key相同,直接返回table[i];反之,调用getEntryAfterMiss()方法。

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;
}

  进入到这个方法的情况可能如下三种:

  1. entry节点为null
    如果entry节点为null,直接返回null。
  2. entry节点不为null,但是key为null
    执行expungeStaleEntry()方法回收该节点,回收完之后,tab[i]节点也就变成了null,直接返回null。
  3. entry节点不为null,entry节点的key和参数key不同
    从i节点开始往后找,如果有key值相同的节点,也就是我们找到了我们需要的节点,返回entry即可。如果找不到,从i节点往后找,遇到key为null的回收一下该节点后返回null,遇到entry为null的直接返回null;

  getEntry()方法会被ThreadLocal的get()方法调用,我们会在稍后的ThreadLocal源码的讲解中再谈。
  介绍ThreadLocalMap用了不少的篇幅啊!下面就来看看我们ThreadLocal啦!关于ThreadLocal的方法网上已经有太多太多的文章介绍了。不过这里我们还是简单的结合我们上面所说的ThreadLocalMap来总结一下!

4 ThreadLocal源码分析

4.1 ThreadLocal.Set()方法

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

  当我们调用ThreadLocal对象的set方法时,程序会获取当前线程,并将其作为作为参数传递给getMap()方法。

Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocal.java
ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

  查看Thread类源码可以发现,Thread类中包含了一个ThreadLocalMap对象,这个家伙我们上面已经花了很大的篇幅来说了,简单的说它的key为ThreadLocal的弱引用,而value为待保存的对象。至于为什么是弱引用,大家自己去google下。
  继续说getMap()方法。getMap()方法很简单:返回当前线程中的ThreadLocalMap对象。

  • 获取到ThreadLocalMap对象后,如果它不为空。则往该对象里面塞入一个键值对,key为ThreadLocal对象的弱引用,vaule为需要保存的对象。
  • 如果ThreadLocalMap对象为null,则调用createMap()方法。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

  createMap()方法在这里不细说。它会初始化当前线程的ThreadLocalMap对象,并将当前需要保存的对象放入ThreadLocalMap中,key值为当前线程的弱引用对象。
  小结一下:ThreadLocal的set方法会获取当前线程的ThreadLocalMap对象,如果TreadLocalMap对象不为空,则将当前线程的弱引用作用key,待保存对象作为value保存起来;若ThreadLocalMap对象为null,则会先初始化,再放入键值对。

4.2 ThreadLocal.Get()方法

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}

  知道了怎么放,接下来聊一聊怎么取。眼尖的朋友们肯定已经发现了,这里又出现了ThreadLocalMap对象。那是自然,因为我们上面不就是往ThreadLocalMap里面放的吗!还记得ThreadLocalMap里面存了啥不?不记得的往上翻一翻。
  如果ThreadLocalMap对象不为空,当前线程作为key值,从ThreadLocalMap中取出来了一个ThreadLocalMap.Entry对象。这个getEntry()方法我们在上面已经已经介绍过了,可能再返回去看看。当然了,肯定有人要问!我们刚才放的时候放的明明不是ThreadLocalMap.Entry对象!这咋回事呢?

ThreadLocalMap.java
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}

  实际上,在ThreadLocalMap中有一个静态类,它名叫Entry,继承了WeakReference类。再看看Entry的构造方法。如果调用Entry的get方法,实际上拿到的是ThreadLocal对象的弱引用对象。是不是很熟悉?上面的set方法有聊到过。
  继续说上面的get()方法。当我们拿到了Entry对象后,如果Entry对象不为空,直接返回Entry对象的value值,即我们想要的值。
那么如果ThreadLocalMap为空呢?则会执行setInitialValue()方法。光看名字,你肯定觉得它无非执行了两步操作:1.初始化对象;2. 将初始化后的对象塞入ThreadLocalMap对象中;3. 返回初始化后的对象。那我们来看看我们的猜想对不对呢?

private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

  除了第一句代码,后面的是不是好像都在哪里见过啊?可不是!不就是上面的set()方法吗?这个方法我们只需要关注initialValue()方法...而这个initialValue()方法是需要我们自己重写的。

  小结一下:如果ThreadLocalMap中有ThreadLocal对应的值,取Entry对象的value值;如果ThreadLocalMap为null,三步走!初始化,将初始化后的对象放入ThreadLocalMap中,返回初始化后的对象。

5 ThreadLocal总结

  很感谢您能耐心的看到这里,我们最后再总结一下ThreadLoacl的实现机制。
  在Thread类中存在一个ThreaLocalMap变量,ThreadLocalMap中又有一个Entry类型的数组,而这个Entry对象则以ThreadLocal的弱引用为key。当我们调用ThreadLocal的get()方法时,会先获取当前线程的ThreadLocalMap对象,并将当前ThreadLocal对象作为key(实际上key为ThreadLocal的弱引用),去它的Entry数组中寻找我们需要的value。就这是我们说ThreadLocal为每个线程创建了一个变量副本的意思,线程对自己ThreadLocalMap中的值进行操作时,并不会对其它线程造成影响。

转载于:https://www.cnblogs.com/cfyrwang/p/8166369.html

Java并发编程之ThreadLocal源码分析相关推荐

  1. Java并发编程之CountDownLatch源码解析

    一.导语 最近在学习并发编程原理,所以准备整理一下自己学到的知识,先写一篇CountDownLatch的源码分析,之后希望可以慢慢写完整个并发编程. 二.什么是CountDownLatch Count ...

  2. 并发编程之 Semaphore 源码分析

    前言 并发 JUC 包提供了很多工具类,比如之前说的 CountDownLatch,CyclicBarrier ,今天说说这个 Semaphore--信号量,关于他的使用请查看往期文章并发编程之 线程 ...

  3. 并发编程之 ThreadLocal 源码剖析

    前言 首先看看 JDK 文档的描述: 该类提供了线程局部 (thread-local) 变量.这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局 ...

  4. Java并发编程之FutureTask源码解析

    上次总结一下AQS的一些相关知识,这次总结了一下FutureTask的东西,相对于AQS来说简单好多呀 之前提到过一个LockSupport的工具类,也了解一下这个工具类的用法,这里也巩固一下吧 /* ...

  5. Java并发编程:从源码分析几道必问线程池的面试题?

    引言 上一篇文章我们有介绍过线程池的一个基本执行流程<[Java并发编程]面试必备之线程池>以及它的7个核心参数,以及每个参数的作用.以及如何去使用线程池 还留了几个小问题..建议看这篇文 ...

  6. Java 并发编程之 ThreadLocal 线程局部变量

    ThreadLocal 通过get和set方法,为每个使用该变量的线程提供一个独立的副本,使得线程安全的共享某个变量:使用 set 方法设置变量后,一定要记得及时使用 remove 方法清理,否则多线 ...

  7. Java高并发程序设计学习笔记(五):JDK并发包(各种同步控制工具的使用、并发容器及典型源码分析(Hashmap等))...

    转自:https://blog.csdn.net/dataiyangu/article/details/86491786#2__696 1. 各种同步控制工具的使用 1.1. ReentrantLoc ...

  8. Java并发编程之CAS第三篇-CAS的缺点

    Java并发编程之CAS第三篇-CAS的缺点 通过前两篇的文章介绍,我们知道了CAS是什么以及查看源码了解CAS原理.那么在多线程并发环境中,的缺点是什么呢?这篇文章我们就来讨论讨论 本篇是<凯 ...

  9. ThreadLocal源码分析-黄金分割数的使用

    前提# 最近接触到的一个项目要兼容新老系统,最终采用了ThreadLocal(实际上用的是InheritableThreadLocal)用于在子线程获取父线程中共享的变量.问题是解决了,但是后来发现对 ...

最新文章

  1. Openwrt 刷机后配置WAN口,安装luci和设置中文、安装挂载USB存储。
  2. linux进程管理之进程创建
  3. android 两个imageview重叠,在android中覆盖两个图像以设置imageview
  4. 我的vExpert之路-传递正能量!
  5. UTC时间、GMT时间、本地时间、Unix时间戳
  6. 如何有效利用碎片时间?这里有个办法
  7. 潜在语义分析(Latent Semantic Analysis,LSA)
  8. PT短网址缩短还原网站源码,php版+html版
  9. 图论最短路:Bellman-Ford与其优化SPFA算法的一点理解
  10. python函数超时,用装饰器解决 func_timeout
  11. 苹果电脑屏幕刷新率如何调整
  12. webstormjs文件全部报错_springboot启动报错org.yaml.snakeyaml.error.YAMLException...
  13. 大前端技术选型 Native原生iOS, Android, React-Native, Flutter, 微信小程序, HTML5
  14. 电子电路工程师常用模拟电路整理(20个)!
  15. 把ts自动合并 下载网页视频并自动合成视频
  16. Mybatis源码详细分析(最新最全)
  17. 嵌入式Linux应用程序开发
  18. 动态规划解决完全背包问题(cpp)
  19. 前台离岗提示语_酒店客房温馨提示怎么写 酒店前台温馨提示语
  20. 神舟Z8-SP7D1驱动安装

热门文章

  1. 将在2021年改变商业格局的10项技术
  2. Fedora设置DVD为yum源
  3. 机器学习入门方法推荐(少走弯路)入门视频推荐
  4. sublime text3注册激活及失效解决办法
  5. 科普帖:深度学习中GPU和显存分析
  6. 编制一个c语言成绩记录簿_C语言学到什么程度能写推箱子
  7. 信息基础---LDPCcodes随机矩阵构造java项目源代码
  8. mysql command type_mysql command line client 使用命令
  9. oracle的存储过程 替换,为什么在存储过程中,变量替换无法使用索引?
  10. 019_MySQL正则表达式