【JAVA】ThreadLocal源码分析
ThreadLocal内部是用一张哈希表来存储:
 static class ThreadLocalMap {
     static class Entry extends WeakReference<ThreadLocal<?>> {
             /** The value associated with this ThreadLocal. */
             Object value;
             Entry(ThreadLocal<?> k, Object v) {
                 super(k);
                 value = v;
             }
     }
     private static final int INITIAL_CAPACITY = 16;
     private Entry[] table;
     private int size = 0;
     private int threshold;
     ......
看过HashMap的话就很容易理解上述内容【Java】HashMap源码分析
而在Thread类中有一个ThreadLocalMap 的成员:
ThreadLocal.ThreadLocalMap threadLocals = null;
所以不难得出如下关系:

每一个线程都有一张线程私有的Map,存放多个线程本地变量
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);
 }
 ThreadLocalMap getMap(Thread t) {
         return t.threadLocals;
 }
不难看出,先获取当前线程的Thread对象,再得到该Thread对象的ThreadLocalMap 成员map,若map为空,需要先createMap()方法,若不为空,则需要调用map的set()方法
 void createMap(Thread t, T firstValue) {
         t.threadLocals = new ThreadLocalMap(this, firstValue);
 }
 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
             table = new Entry[INITIAL_CAPACITY];
             int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
             table[i] = new Entry(firstKey, firstValue);
             size = 1;
             setThreshold(INITIAL_CAPACITY);
 }
 private void setThreshold(int len) {
             threshold = len * 2 / 3;
 }
createMap方法会创建一个ThreadLocalMap对象,在ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)构造方法中,可以看出和HashMap很相似,通过firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)取模,计算出哈希表的下标,将创建好的Entry对象放入该位置,再根据表长计算阈值,可以看出负载因子是2/3,初始哈希表的大小是16。
 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();
 }
不难看出,通过key.threadLocalHashCode & (len-1)计算出哈希表的下标,判断该位置的Entry是否为null,若为null,则创建Entry对象,将其放入该下标位置;若Entry已存在,则需要解决哈希冲突,重新计算下标。最后size自增,再根据!cleanSomeSlots(i, sz) && sz >= threshold进行判断是否需要进行哈希表的调整。
在解决哈希冲突的上,常用的有开链法、线性探测法和再散列法,HashMap中使用的是开链法,而ThreadLocal使用的是线性探测法,即发生哈希冲突,往后移动到合适位置。
 private static int nextIndex(int i, int len) {
             return ((i + 1 < len) ? i + 1 : 0);
 }
 private static int prevIndex(int i, int len) {
             return ((i - 1 >= 0) ? i - 1 : len - 1);
 }
从这两个操作看出,ThreadLocal中的哈希表是利用了循环数组的方式,进行环形的线性探测
在上述for循环中,会取出该Entry上的ThreadLocal对象(键)进行判断,若相同则直接覆盖,若为null,说明该Entry空间存在但其ThreadLocal对象的指向为null,需要进行调整;若都不成立,则继续循环,重复以上操作。
Entry空间指向存在但ThreadLocal对象的指向为null是因为Entry继承自WeakReference<ThreadLocal<?>>,是弱引用,存在被GC的情况,所以会存在这种情况,视为脏Entry,接下来的操作就是通过replaceStaleEntry进行处理。
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e; int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i; for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get(); if (k == key) {
e.value = value; tab[i] = tab[staleSlot];
tab[staleSlot] = e; if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
} if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
} tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value); if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
可以清楚看到第一个for循环前向遍历查找脏Entry,用slotToExpunge保存脏Entry下标;
第二个for循环后向遍历,若遇到ThreadLocal向同,更新value,然后与下标为staleSlot(传入进来的脏Entry)进行交换,接着判断前向查找脏Entry是否存在,slotToExpunge == staleSlot说明的就是前向查找没找到,就更改slotToExpunge的值,然后进行清理操作,结束掉;若后向遍历遇到脏Entry,并且前向没找到,更改slotToExpunge的值,为清理时用,继续循环。
若不存在和ThreadLocal引用相同的Entry,则需要将staleSlot的位置的Entry替换为一个新的Entry对象,tab[staleSlot].value = null是为了GC;
最后根据slotToExpunge来判断前向后向遍历中是否存在脏Entry,若存在还需要进行清理。
其中的expungeStaleEntry方法如下:
 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;
 }
可以看到,先把当前位置的脏Entry清除掉(置为null),size自减。然后从当前位置后向遍历,若遇到脏Entry直接清除,size自减;若不是脏Entry,则需要判断它是否经过哈希冲突的调整的,若调整过,需要将其重新调整,最后返回当前位置为null的table下标;综上,该方法就是后向清除脏Entry,再把调整需要调整的Entry。
在replaceStaleEntry方法中,调用expungeStaleEntry清除掉脏Entry后,还要用cleanSomeSlots方法清除掉返回回来的下标后的脏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;
 }
从下标为i后面的开始后向遍历,遇到脏Entry调用expungeStaleEntry清除掉,令removed为true,i会变为下标为null的位置,继续循环;其中n的用途是控制循环次数,当遇到脏Entry时,会令n等于表长,扩大搜索范围。
在set方法中,最后根据!cleanSomeSlots(i, sz) && sz >= threshold,判断是否清理掉了脏Entry,若清理了什么都不做;若没有清理,还会判断是否达到阈值,进而是否需要rehash操作;
rehash方法:
 private void rehash() {
     expungeStaleEntries();
     // Use lower threshold for doubling to avoid hysteresis
     if (size >= threshold - threshold / 4)
         resize();
 }
首先调用expungeStaleEntries方法:
 private void expungeStaleEntries() {
     Entry[] tab = table;
     int len = tab.length;
     for (int j = 0; j < len; j++) {
         Entry e = tab[j];
         if (e != null && e.get() == null)
             expungeStaleEntry(j);
     }
 }
可以看到expungeStaleEntries方法是遍历整个哈希表,通过调用expungeStaleEntry方法清除掉所有脏Entry。
由于清除掉了脏Entry,还需要对size进行判断,看是否达到了阈值的3/4(提前触发resize),来判断是否真的需要resize;
resize方法:
 private void resize() {
     Entry[] oldTab = table;
     int oldLen = oldTab.length;
     int newLen = oldLen * 2;
     Entry[] newTab = new Entry[newLen];
     int count = 0;
     for (int j = 0; j < oldLen; ++j) {
         Entry e = oldTab[j];
         if (e != null) {
             ThreadLocal<?> k = e.get();
             if (k == null) {
                 e.value = null; // Help the GC
             } else {
                 int h = k.threadLocalHashCode & (newLen - 1);
                 while (newTab[h] != null)
                     h = nextIndex(h, newLen);
                 newTab[h] = e;
                 count++;
             }
         }
     }
     setThreshold(newLen);
     size = count;
     table = newTab;
 }
刚开始的操作可以清楚的明白,每次扩容的大小都是原来的两倍;然后遍历原表的所有Entry,遇到脏Entry直接赋值null引起帮助GC;遇到有效Entry则需要根据新的表长重新计算下标,再通过线性探测完成新表的填充;填充完毕,计算新的阈值,给size和table赋值,结束操作。
至此,有关set的操作就结束了,还剩下get和remove:
get方法:
 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();
 }
和set一样,先获取当前线程,再根据当前线程获取其ThreadLocalMap成员map;
若map不为null,通过map的getEntry方法得到Entry对象,若Entry不为null则直接返回Entry的value;
若map为null,或者map不为null,但是Entry是null,则都需要调用setInitialValue方法。
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);
 }
根据ThreadLocal定位哈希表的下标,若满足则直接返回,若不是,调用getEntryAfterMiss继续找。
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;
 }
看以看到这还是一个后向遍历的查找,若是找到则直接返回;若遇到脏Entry需要调用expungeStaleEntry方法清理掉;最后还没找到返回null。
setInitialValue方法:
 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;
 }
先调用initialValue方法,该方法需要使用者进行覆盖,否则返回的是null。所以当没有使用set方法时覆盖initialValue方法时还是会调用set方法的,效果是一样的。
 protected T initialValue() {
         return null;
 }
后面的操作就和set方法一样。get方法至此结束。
remove方法:
 public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }
以当前线程为参数调用getMap方法:
 ThreadLocalMap getMap(Thread t) {
     return t.threadLocals;
 }
若是当前线程的ThreadLocalMap对象不存在,什么都不做,若存在,调用内部的remove方法:
 private void remove(ThreadLocal<?> key) {
     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)]) {
         if (e.get() == key) {
             e.clear();
             expungeStaleEntry(i);
             return;
         }
     }
 }
首先根据ThreadLocal找到其对应的的哈希表的下标(不一定是它的下标,会有哈希冲突的可能性),然后开始后向遍历,找到真正的位置,调用clear方法删除掉,顺便还进行脏Entry的清理。
clear方法是Reference类的方法:
 public void clear() {
     this.referent = null;
 }
可以看到仅仅只是令指向变为null,因为Reference是WeakReference的父类,ThreadLocalMap继承自WeakReference<ThreadLocal<?>>,弱引用变为null,就会变成脏Entry,所以就需要expungeStaleEntry对其清理。为什么不令tab[i]直接为null,就是因为在expungeStaleEntry执行时还会清理遇到的脏Entry,这样可以尽可能多的删除掉脏Entry。
ThreadLocal源码分析到此结束。
【JAVA】ThreadLocal源码分析的更多相关文章
- Java并发编程之ThreadLocal源码分析
		## 1 一句话概括ThreadLocal<font face="微软雅黑" size=4> 什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象 ... 
- Java多线程学习之ThreadLocal源码分析
		0.概述 ThreadLocal,即线程本地变量,是一个以ThreadLocal对象为键.任意对象为值的存储结构.它可以将变量绑定到特定的线程上,使每个线程都拥有改变量的一个拷贝,各线程相同变量间互不 ... 
- 并发编程(四)—— ThreadLocal源码分析及内存泄露预防
		今天我们一起探讨下ThreadLocal的实现原理和源码分析.首先,本文先谈一下对ThreadLocal的理解,然后根据ThreadLocal类的源码分析了其实现原理和使用需要注意的地方,最后给出了两 ... 
- 并发-ThreadLocal源码分析
		ThreadLocal源码分析 参考: http://www.cnblogs.com/dolphin0520/p/3920407.html https://www.cnblogs.com/coshah ... 
- ThreadLocal源码分析-黄金分割数的使用
		前提 最近接触到的一个项目要兼容新老系统,最终采用了ThreadLocal(实际上用的是InheritableThreadLocal)用于在子线程获取父线程中共享的变量.问题是解决了,但是后来发现对T ... 
- Java Reference 源码分析
		@(Java)[Reference] Java Reference 源码分析 Reference对象封装了其它对象的引用,可以和普通的对象一样操作,在一定的限制条件下,支持和垃圾收集器的交互.即可以使 ... 
- Java 集合源码分析(一)HashMap
		目录 Java 集合源码分析(一)HashMap 1. 概要 2. JDK 7 的 HashMap 3. JDK 1.8 的 HashMap 4. Hashtable 5. JDK 1.7 的 Con ... 
- java集合源码分析(三):ArrayList
		概述 在前文:java集合源码分析(二):List与AbstractList 和 java集合源码分析(一):Collection 与 AbstractCollection 中,我们大致了解了从 Co ... 
- java集合源码分析(六):HashMap
		概述 HashMap 是 Map 接口下一个线程不安全的,基于哈希表的实现类.由于他解决哈希冲突的方式是分离链表法,也就是拉链法,因此他的数据结构是数组+链表,在 JDK8 以后,当哈希冲突严重时,H ... 
随机推荐
- 校验金额、大小写字母、大写字母、合法uri、email
			/* 合法uri*/ export function validURL(url) { const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0- ... 
- CSS学习总结4:派生选择器学习总结
			派生选择器:通过依据元素在其位置的上下文关系来定义样式,你可以使标记更加简洁.派生选择器中一共分为三种:后代选择器.子元素选择器.相邻兄弟选择器. 1.初识派生选择器 实例:你希望列表中的 stron ... 
- springboot中使用ContextLoaderListener.getCurrentWebApplicationContext();获取WebApplicationContext为空问题
			WebApplicationContext applicationContext = ContextLoaderListener.getCurrentWebApplicationContext(); ... 
- etcd-v2第一集
			网站:https://github.com/coreos/etcd 一些观点:https://yq.aliyun.com/articles/11035 1.etcd是键值存储仓库,配置共享和服务发现2 ... 
- Spring遇到的问题合集
			2018-09-15 元素 "tx:annotation-driven" 的前缀 "tx" 未绑定. 后来我加了 http://www.springframew ... 
- Chapter3_操作符_直接常量和指数计数法
			(1)直接常量 在程序中使用直接常量,相当于指导编译器,告诉它要生成什么样的类型,这样就不会产生模棱两可的情况.比如flaot a = 1f等,后缀表示告诉编译器想生成的类型.常用的后缀有l/L(lo ... 
- python中 os._exit() 和 sys.exit(), exit(0)和exit(1) 的用法和区别
			os._exit() 和 sys.exit() os._exit() vs sys.exit() 概述 Python的程序有两中退出方式:os._exit(), sys.exit().本文介绍这两种方 ... 
- 包含复杂函数的excel 并下载
			POI 版本: <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</a ... 
- kali安装配置ftp
			参考:https://zhidao.baidu.com/question/1511146077646448900.html 一)安装 1.用sudo apt-get install 下载安装包 
- linux命令_文件目录操作命令
			# linux命令--文件和目录操作命令 pwd "print working directory" 打印工作目录的绝对路径 范例: 在bash命令行显示当前用户的完整路径 系统B ... 
