ThreadLocal源码及相关问题分析

前言

  在高并发的环境下,当我们使用一个公共的变量时如果不加锁会出现并发问题,例如SimpleDateFormat,但是加锁的话会影响性能,对于这种情况我们可以使用ThreadLocal。ThreadLocal是将公共变量copy一份到线程私有内存中以消除并发问题,ThreadLocal是JDK内部提供的高效解决并发问题的工具类之一,本文介绍ThreadLocal的重要方法的源码实现以及相关问题的分析。

数据结构

  由上图可以看出,在Thread中维护了一个Entry的列表,Entry存储的是公共变量的copy,这个列表是由ThreadLocal维护,每次从ThreadLocal中获取的是一个公共变量的副本,所以避免了并发问题,接下来在看实现方法之前我们先看一下上面提到的类的定义。

变量

  首先来看一下ThreadLocal重要的成员变量。

 1 public class ThreadLocal<T> {
 2     private final int threadLocalHashCode = nextHashCode(); 3
 4     private static AtomicInteger nextHashCode =
 5         new AtomicInteger();
 6
 7     private static final int HASH_INCREMENT = 0x61c88647;
 8
 9     private static int nextHashCode() {
10         return nextHashCode.getAndAdd(HASH_INCREMENT);
11     }
12 }

  threadLocalHashCode是ThreadLocal实例的ID,其主要用于做散列(后面会讲到),从上面可以看出来threadLocalHashCode是所有ThreadLocal贡献的原子变量nextHashCode加上一个固定的HASH_INCREMENT生成的。为什么

  HASH_INCREMENT的值是0x61c88647?

  前面说了,ThreadLocal是使用散列做存储的,而这个数字可以让生成出来的ThreadLocal的ID较为均匀地分布在大小为2的N次方的数组中。

  接下来看一下ThreadLocal中真正存储数据的Entry类。

 1 static class ThreadLocalMap {
 2
 3         static class Entry extends WeakReference<ThreadLocal<?>> {
 4
 5             Object value;
 6
 7             Entry(ThreadLocal<?> k, Object v) {
 8                 super(k);
 9                 value = v;
10             }
11         }
12
13         private static final int INITIAL_CAPACITY = 16;
14
15         private Entry[] table;
16
17         private int size = 0;
18
19         private int threshold;
20 }

  ThreadLocalMap是ThreadLocal的内部类,Entry继承WeakReference,前面提到Entry是K-V形式,key就是WeakReference的成员变量referent,value就是Entry的value。我们可以发现,key是一个弱引用,它的生命周期到下一个GC就结束了,为什么要这样设计呢?在后面的内存泄漏中会提到。

  table是一个大小为2的N次方的数组,threshold是数组扩容的临界点(与HashMap一个作用),默认是数组的大小的2/3,数组每次扩容是将长度扩大1倍。

方法

  ThreadLocal重要的方法如下:

1 public T get(); //获取值
2
3 public void set(T value); //设置值
4
5 public void remove(); //删除值

  首先来看set()方法:

1 public void set(T value) {
2         Thread t = Thread.currentThread();
3         ThreadLocalMap map = getMap(t); //获取当前Thread中的ThreadLocalMap
4         if (map != null)
5             map.set(this, value); //设置值
6         else
7             createMap(t, value); //创建一个ThreadLocalMap
8 }

  在Thread类中有 ThreadLocal.ThreadLocalMap threadLocals = null; 这样的成员变量,就是用来存储当前线程设置的值,所有的ThreadLocal都是在操作这个成员变量。

  接下来看一下map.set()这个方法:

 1 private void set(ThreadLocal<?> key, Object value) {
 2         Entry[] tab = table;
 3         int len = tab.length;
 4         int i = key.threadLocalHashCode & (len - 1); // 计算hash散列
 5
 6         // 从i开始遍历列表
 7         for (Entry e = tab[i];
 8              e != null;
 9              e = tab[i = nextIndex(i, len)]) {
10             ThreadLocal<?> k = e.get();
11             // 找到了key值
12             if (k == key) {
13                 e.value = value;
14                 return;
15             }
16             // key=null表示该key值已被回收
17             if (k == null) {
18                 replaceStaleEntry(key, value, i);19                 return;
20             }
21         }
22         // 当遍历到一个可以插入数据的空位置时
23         tab[i] = new Entry(key, value);
24         int sz = ++size;
25         if (!cleanSomeSlots(i, sz) // 清理列表中已经被GC回收的
26             && sz >= threshold) // 判断是否需要扩容
27             rehash();// 扩容并重新计算hash散列
28 }

  ThreadLocal通过遍历Entry数组,如果当前key已存在则覆盖,没有则新增,如果在遍历过程中遇到已经被GC回收的key,则会清除掉无效的key对应的值。

  从上面的循环可以看出来,ThreadLocal采用hash散列的线性探测存储,这种方式实现简单但是无法存储大量的数据,所以不建议用ThreadLocal存储大量的数据。这里还有一个问题是,nextIndex()方法是循环的获取数组下标,所以如果table满的就会导致无限循环,所以threshold的值是小于table的大小并且每次set之后都会清理一次数组的无效数据。

  我们先看一下cleanSomeSlots()方法:

 1 private boolean cleanSomeSlots(int i, int n) {
 2     boolean removed = false;
 3     Entry[] tab = table;
 4     int len = tab.length;
 5     do {
 6         i = nextIndex(i, len);
 7         Entry e = tab[i];
 8         if (e != null && e.get() == null) {
 9             n = len;
10             removed = true;
11             i = expungeStaleEntry(i); // 从下标i开始清除数组中的无效元素
12         }
13     } while ( (n >>>= 1) != 0); // 该循环会循环log2(n)次
14     return removed;
15 }

  该方法是通过线性查找,从下标为i开始查找已经被GC清理的key对应的值,一般情况下会查找log2(n)次,expungeStaleEntry()是真正的清理方法,源码如下:

 1 private int expungeStaleEntry(int staleSlot) {
 2     Entry[] tab = table;
 3     int len = tab.length;
 4
 5     // 清理对应数据
 6     tab[staleSlot].value = null;
 7     tab[staleSlot] = null;
 8     size--;
 9
10     // 重新计算hash散列,直到遇见null
11     Entry e;
12     int i;
13     for (i = nextIndex(staleSlot, len);
14          (e = tab[i]) != null;
15          i = nextIndex(i, len)) {
16         ThreadLocal<?> k = e.get();
17         if (k == null) {
18             e.value = null;
19             tab[i] = null;
20             size--;
21         } else {
22             int h = k.threadLocalHashCode & (len - 1);
23             if (h != i) {
24                 tab[i] = null;
25                 while (tab[h] != null)
26                     h = nextIndex(h, len);
27                 tab[h] = e;
28             }
29         }
30     }
31     return i;
32 }

  前面提到,ThreadLocal存储数据的方式是使用线性探测法,所以expungeStaleEntry()方法在清理掉一个数据之后会将该下标之后的所有非null位置重新计算一次hash散列,经过这样的操作之后,数组中的元素最终会符合hash散列的要求,如果不重新计算一次hash散列,那么最终数组结果可能不符合hash散列的要求,比如:元素a、b计算后得到存储位置冲突,通过线性探测法,最终结果是元素a在下标0,元素b在下标1,如果删除a之后,b就应该放在下标0而不是下标1上。

  我们在回过头来看看map.set()方法中的replaceStaleEntry()方法:

 1 private void replaceStaleEntry(ThreadLocal<?> key, Object value,
 2                                        int staleSlot) {
 3     Entry[] tab = table;
 4     int len = tab.length;
 5     Entry e;
 6     // slotToExpunge用来记录清除无效key的开始位置,初始值等于staleSlot,staleSlot的值是无效key的下标
 7     int slotToExpunge = staleSlot;
 8     // 向前查找无效的key
 9     for (int i = prevIndex(staleSlot, len);
10          (e = tab[i]) != null;
11          i = prevIndex(i, len))
12         if (e.get() == null)
13             slotToExpunge = i;
14
15     // 向后查找无效的key,直到遍历到一个有效key
16     for (int i = nextIndex(staleSlot, len);
17          (e = tab[i]) != null;
18          i = nextIndex(i, len)) {
19         ThreadLocal<?> k = e.get();
20
21         // 如果在查找的过程中找到了我们需要找的key,则将无效key与该值替换
22         if (k == key) {
23             e.value = value;
24
25             tab[i] = tab[staleSlot];
26             tab[staleSlot] = e;
27
28             // 判断在向前查找无效key的过程中有没有找到
29             if (slotToExpunge == staleSlot)
30                 slotToExpunge = i;
31             // 清理无效key
32             cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
33             return;
34         }
35         // 如果向前没有查找到无效key并且当前key为无效key
36         if (k == null && slotToExpunge == staleSlot)
37             slotToExpunge = i;
38     }
39
40     // 将需要插入的数据插入到staleSlot位置
41     tab[staleSlot].value = null;
42     tab[staleSlot] = new Entry(key, value);
43
44     // 如果向前查找到了无效key或者向后查找到了无效key,则清理无效key
45     if (slotToExpunge != staleSlot)
46         cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
47 }

  该方法时在set数据值遍历到了一个无效key执行的方法,ThreadLocal认为,出现无效key的位置附近也会出现无效key,所以在清理该无效key的时候回查找附近连续的无效key。

  值得注意的是,代码中找到了我们需要找的key的时候,将无效key与有效key交换的原因是,清除方法expungeStaleEntry()只能够清除连续的无效的key,如果向前没有找到无效key,向后找到了无效key的情况下,会出现如下情况:

  SS是staleSlot,SE是slotToExpunge,如果不交换,则无法清理到SS位置的无效key,而且需要找的key后面同样也可能会出现无效key,同样无法清除到。

  除了这种情况,还有一种情况是向前找到了无效key,会出现如下情况:

  这种情况下,交换了SS跟需要找的key之后会出现无法清理SS位置的无效key的请款,所以代码中不止调用了一次expungeStaleEntry(),还会调用cleanSomeSlots(),源码在前面已经分析过了,这样就可以把SS以及之后的无效key都清掉。

  以上set()方法的源码就分析完了,接下来是get()方法,get()方法是调用ThreadLocalMap的getEntry()方法,所以我们直接分析getEntry()方法:

 1 private Entry getEntry(ThreadLocal<?> key) {
 2     int i = key.threadLocalHashCode & (table.length - 1);
 3     Entry e = table[i];
 4     if (e != null && e.get() == key)
 5         return e;
 6     else
 7         return getEntryAfterMiss(key, i, e); // hash散列之后的位置不是需要找的key,即发生了hash碰撞
 8 }
 9
10 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
11     Entry[] tab = table;
12     int len = tab.length;
13
14     while (e != null) {
15         ThreadLocal<?> k = e.get();
16         if (k == key)
17             return e;
18         if (k == null)
19             expungeStaleEntry(i); // 清理无效key
20         else
21             i = nextIndex(i, len); // 向后遍历
22         e = tab[i];
23     }
24     return null;
25 }

  get()方法较为简单,这里不做过多赘述,接下来是remove()方法,同样该方法是直接调用的ThreadLocalMap的remove()方法,源码如下:

 1 private void remove(ThreadLocal<?> key) {
 2     Entry[] tab = table;
 3     int len = tab.length;
 4     int i = key.threadLocalHashCode & (len-1);
 5     for (Entry e = tab[i];
 6          e != null;
 7          e = tab[i = nextIndex(i, len)]) {
 8         if (e.get() == key) {
 9             e.clear();// 将Entry的key置为null
10             expungeStaleEntry(i); // 清理该key
11             return;
12         }
13     }
14 }

为什么会内存泄漏

  我们已经知道Entry的key是一个弱引用,当他被GC之后,那么这个Entry就是一个无效的对象,无法被外部获取到。而在Thread对象中是持有了ThreadLocalMap的引用,而ThreadLocalMap又持有了Entry数组的引用,所以该无效的Entry是无法被回收的,如果不手动的删除就会发生内存泄漏,好在ThreadLocal在set(),get()方法中都会主动去清理无效的Entry防止内存泄漏,但是依然会存在内存泄漏的风险,所以在编程中需要注意remove()不需要的值。

  这么一看,似乎ThreadLocal发生内存泄漏的原因是Entry使用了弱引用,其实使用弱引用恰恰是为了防止内存泄漏。试想,如果一个线程的生命周期很长,其中在使用完了ThreadLocal之后我们手动将其引用设置为null,希望GC清理调它,如果Entry是强引用的话,GC是无法清理调的,因为还存在一个Thread->ThreadLocalMap->Entry->ThreadLocal的强引用链,导致整个Entry都不会被回收,从而发生内存泄漏。Java团队使用弱引用的原因就是为了处理生命周期长的线程而使用的。

原文地址:https://www.cnblogs.com/ouhaitao/p/11188620.html

时间: 2024-10-11 23:22:35

ThreadLocal源码及相关问题分析的相关文章

【JAVA】ThreadLocal源码分析

ThreadLocal内部是用一张哈希表来存储: 1 static class ThreadLocalMap { 2 static class Entry extends WeakReference<ThreadLocal<?>> { 3 /** The value associated with this ThreadLocal. */ 4 Object value; 5 6 Entry(ThreadLocal<?> k, Object v) { 7 super(k)

Java -- 基于JDK1.8的ThreadLocal源码分析

1,最近在做一个需求的时候需要对外部暴露一个值得应用  ,一般来说直接写个单例,将这个成员变量的值暴露出去就ok了,但是当时突然灵机一动(现在回想是个多余的想法),想到handle源码里面有使用过ThreadLocal这个类,想了想为什么不想直接用ThreadLocal保存数据源然后使用静态方法暴露出去呢,结果发现使用ThreadLocal有时候会获取不到值,查了下原因原来同事是在子线程中调用的(捂脸哭泣),所以还是要来看一波源码,看看ThreadLocal底层实现,适用于哪些场景 2,我们现在

Threadlocal源码分析以及其中WeakReference作用分析

今天在看Spring 3.x企业应用开发实战,第九章 Spring的事务管理,9.2.2节ThreadLocal的接口方法时,书上有提到Threadlocal的简单实现,我就去看了下JDK1.8的Threadlocal的源码.发现实现方式与书中讲的并不相同,同时在网上搜索了一下,发现有比较多的人理解错了. 先看一下容易误导的解释:在ThreadLocal类中有一个Map对象,这个Map以每个Thread对象为键,保存了这个线程对应局部变量值,对应的实现方式如下: public class Sim

Java多线程9:ThreadLocal源码剖析

http://www.cnblogs.com/xrq730/p/4854813.html ThreadLocal其实比较简单,因为类里就三个public方法:set(T value).get().remove().先剖析源码清楚地知道ThreadLocal是干什么用的.再使用.最后总结,讲解ThreadLocal采取这样的思路. 三个理论基础 在剖析ThreadLocal源码前,先讲一下ThreadLocal的三个理论基础: 1.每个线程都有一个自己的ThreadLocal.ThreadLoca

ThreadLocal源码学习

ThreadLocal,线程本地化对象,在多线程环境中,使用ThreadLocal对象来维护变量时,ThreadLocal为每个使用该变量的线程维护一个独立的线程副本. ThreadLocal.java源文件内容为: 1 /** 2 * ThreadLocal内部包含一个用数组实现的哈希表,用来存储对应到每个线程的局部对象的值 3 * 其中,ThreadLocal对象担当key,实际通过threadLocalHashCode值来进行检索 4 */ 5 public class ThreadLoc

java多线程17:ThreadLocal源码剖析

ThreadLocal源码剖析 ThreadLocal其实比较简单,因为类里就三个public方法:set(T value).get().remove().先剖析源码清楚地知道ThreadLocal是干什么用的.再使用.最后总结,讲解ThreadLocal采取这样的思路. 三个理论基础 在剖析ThreadLocal源码前,先讲一下ThreadLocal的三个理论基础: 1.每个线程都有一个自己的ThreadLocal.ThreadLocalMap对象 2.每一个ThreadLocal对象都有一个

ArrayList源码和多线程安全问题分析

1.ArrayList源码和多线程安全问题分析 在分析ArrayList线程安全问题之前,我们线对此类的源码进行分析,找出可能出现线程安全问题的地方,然后代码进行验证和分析. 1.1 数据结构 ArrayList内部是使用数组保存元素的,数据定义如下: transient Object[] elementData; // non-private to simplify nested class access 在ArrayList中此数组即是共享资源,当多线程对此数据进行操作的时候如果不进行同步控

阿里架构师浅析ThreadLocal源码——黄金分割数的使用

一. 前提 最近接触到的一个项目要兼容新老系统,最终采用了ThreadLocal(实际上用的是InheritableThreadLocal)用于在子线程获取父线程中共享的变量.问题是解决了,但是后来发现对ThreadLocal的理解不够深入,于是顺便把它的源码阅读理解了一遍.在谈到ThreadLocal之前先买个关子,先谈谈黄金分割数.本文在阅读ThreadLocal源码的时候是使用JDK8(1.8.0_181). 二. 黄金分割数与斐波那契数列 首先复习一下斐波那契数列,下面的推导过程来自某搜

Netty源码学习——ChannelPipeline模型分析

参考Netty API io.netty.channel.ChannelPipeline A list of ChannelHandlers which handles or intercepts inbound events and outbount operations of aChannel.ChannelPipeline implements an advanced form of theIntercepting Filter pattern to give a user full co