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

2,我们现在网上搜索一下前人对于ThreadLocal这个类的一些总结

ThreadLocal特性及使用场景:
1、方便同一个线程使用某一对象,避免不必要的参数传递;
2、线程间数据隔离(每个线程在自己线程里使用自己的局部变量,各线程间的ThreadLocal对象互不影响);
3、获取数据库连接、Session、关联ID(比如日志的uniqueID,方便串起多个日志);

从上面的总结来看,主要是用来线程间的数据隔离的,即ThreadLocal 对象可以在多个线程中共享, 但每个线程只能读写其中自己的数据副本。

ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<>();
mBooleanThreadLocal.set(true);
Boolean result = mBooleanThreadLocal.get();

主要就是这三个方法  ,那咱们就一个一个来看

2.1 构造函数

 /**
* ThreadLocals rely on per-thread linear-probe hash maps attached
* to each thread (Thread.threadLocals and
* inheritableThreadLocals). The ThreadLocal objects act as keys,
* searched via threadLocalHashCode. This is a custom hash code
* (useful only within ThreadLocalMaps) that eliminates collisions
* in the common case where consecutively constructed ThreadLocals
* are used by the same threads, while remaining well-behaved in
* less common cases.
*/
private final int threadLocalHashCode = nextHashCode(); /**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger(); /**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647; public ThreadLocal() {
}

  ThreadLocal的构造方法是一个空方法 ,但是有三个参数,nextHashCode 和HASH_INCREMENT 是ThreadLocal类的静态变量,真正变量只有 threadLocalHashCode 这一个,这三个参数都不是善茬啊。

  HASH_INCREMENT 英文注释解释是“连续生成的哈希码之间的差异——将隐式顺序线程本地id转换为接近最优扩散的乘法哈希值,用于大小为2的幂的表。” 这句解释看得我们一脸蒙蔽啊,不过我记得看HashMap源码的时候 有解决哈希冲突这一说,我们先不探究这么多,先将0x61c88647这个奇怪的值记在心里一下。

  nextHashCode 的表示了即将分配的下一个ThreadLocal实例的threadLocalHashCode 的值。

  threadLocalHashCode 见名知意 这个ThreadLocal对象的hashcode

private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

  调用的是nextHashCode方法  ,就是将ThreadLocal类的下一个hashCode值即nextHashCode的值赋给实例的threadLocalHashCode,然后nextHashCode的值增加HASH_INCREMENT这个值。

  我们先不管这些参数的生产方式,先知道有这三个参数就行,继续往下面看流程

2,2 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;
} void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

  我们可以看到,首先通过当前调用的线程获取到线程中对应的threadLocals变量(这里我一脸懵逼  线程中竟然使用到ThreadLocal了,赶紧去看看线程的源码)

public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

  果然Thread中拥有变量threadLocals,孤陋寡闻啊,各位老铁,咋们继续往下看代码,判断一下从线程中获取到的ThreadLocalMap,如果不为空则,调用ThreadLocalMap类的set方法保存一下,,如果为空,则new一个ThreadLocalMap对象出来 这里涉及到了ThreadLocalMap这个类,我们来详细的看一下这个类

2.3 ThreadLocalMap类

2.3.1 构造函数

 static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
} /**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16; /**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table; /**
* The number of entries in the table.
*/
private int size = 0; /**
* The next size value at which to resize.
*/
private int threshold; // Default to 0 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);
} /**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0

第32-33行 :  我们看到ThreadLocalMap是ThreadLocal的匿名内部类,且构造方法也就是将该ThreadLocal实例作为key,要保持的对象作为值。变量table用来用来存放存放数据,我们可以看到table数组的初始大小是INITIAL_CAPACITY = 16  英文注释是“初始容量——必须是2的幂。”   这里我们又碰到了“2的幂”关键字了,我们继续往下看

     第34行: int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  这行代码完全有些看不懂 ,为了不理解流程,我们可以先不用懂,就模糊的理解为通过ThreadLoad的threadLocalHashCode变量哈希码来生成一个下标位置,继续往下看

  第35 - 49行:将ThreadLocal对象和value值保存到Entry对象中再保存到table数组中,设置初始的size,设置threshold阈值,这个阈值适用于扩容,当发现存入的数据大小打到了当前长度size的二分之三,就会触发扩容,将当前table数组的大小扩充到原来的两倍。

  然后我们可以看到Entry对象中对我们的ThreadLocal参数是采用弱引用的,这点对我们后续分析ThreadLocal内存泄漏这款有所帮助,先提醒一下大家。

  然后我们全面来看一下int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);   这行代码所带来的的意思,我们再和上面有可能有联系的一些代码全部给粘贴在一起

private static final int HASH_INCREMENT = 0x61c88647;
private static AtomicInteger nextHashCode = new AtomicInteger();
private final int threadLocalHashCode = nextHashCode(); int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;

  在看代码之前我们先来了解一个小的知识点,看过HashMap源码的同学一定知道“哈希冲突”这个问题

我们使用同一个哈希函数来计算不止一个的待存放的数据在表中的存放位置,
总是会有一些数据通过这个转换函数计算出来的存放位置是相同的,这就是哈希冲突。
也就是说,不同的关键字通过同一哈希转换函数计算出相同的哈希地址。

  通过ThreadLocal和ThreadLocalMap的源码可以知道,里面存值table的下标是通过ThreadLocal的哈希码生成的,那么在ThreadLocalMap同样的存在这个哈希冲突问题,那我们来看看ThreadLocal是怎么来解决这个问题的呢?

  我们知道ThreadLocalMap的初始长度为16,每次扩容都增长为原来的2倍,即它的长度始终是2的n次方,大小必须是2的N次方呀(len = 2^N),那 len-1 的二进制表示就是低位连续的N个1,以16为例,16-1的二进制是15(十进制) = 1111(二进制),而 key.threadLocalHashCode & (len-1) 的值就是 threadLocalHashCode 的低N位(&运算符我就不给大家进行解释了,大家自己百度一下位运算符),而这样做的目的是能均匀的产生哈希码的分布,我一脸懵逼,那让我们来看一下

public static  int HASH_INCREMENT = 0x61c88647 ;

public static void range(int value){
for (int i = 0; i < value; i++) {
int nextHashCode = i*HASH_INCREMENT + HASH_INCREMENT;
System.out.println((nextHashCode & (value - 1))+",");
}
} range(16); //输出结果
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0

  卧槽,真的可以均匀的产生,难道0x61c88647这个数值这么神奇?下面是网上搜到的关于这个数字的节选,我反正是看不懂,还是给大家贴出来吧

①这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。
②斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。
换句话说 (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是
1640531527也就是0x61c88647 。
③通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。

  理解了生成方式我们继续往下看ThreadLocalMap的源码吧

2.3.2 set函数

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

  set方法也很简单,就是去table中获取第i位的数据,如果发现当前第i为的ThreadLocal等于当前传入的ThreadLocal,就更新i位的value,如果发现第i为的ThreadLocal为空,由于我们的Entry对ThreadLocal是弱引用,就表示之前保存的ThreadLocal已经被回收了,replaceStaleEntry()方法就不和大家细看了,就是首先清理掉空的Entry,然后将后面的 Entry 进行 rehash 填补空洞,25-27行就是对阈值的判断,如果超过了就进行扩容。

2.3.3 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);
} 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 static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

  getEntry函数也很简单,使用位运算找到哈希槽。若哈希槽中为空或 key 不是当前 ThreadLocal 对象则会调用getEntryAfterMiss方法,可以看到getEntryAfterMiss 方法会循环查找直到找到或遍历所有可能的哈希槽, 在循环过程中可能遇到4种情况:

①哈希槽中是当前ThreadLocal, 说明找到了目标
②哈希槽中为其它ThreadLocal, 需要继续查找
③哈希槽中为null, 说明搜索结束未找到目标
④哈希槽中存在Entry, 但是 Entry 中没有 ThreadLocal 对象。因为 Entry 使用弱引用, 这种情况说明 ThreadLocal 被GC回收。 为了处理GC造成的空洞(stale entry), 需要调用expungeStaleEntry方法进行清理。

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

  remove方法和get方法类似,也是找出对应的Entry对象,然后调用其clean方法清理

  

  ok,这里我们就把ThreadLocalMap的源码看完了,其实类似ThreadLocal的源码一样,来我们继续往下看

2.4  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();
} 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;
} protected T initialValue() {
return null;
}

  都很简单,首先去获取当前线程的ThreadLocalMap中的Entry对象,如果不为空直接返回Entry的value值,如果为空,则调用initialValue方法,这个方法可以被重写 ,默认返回为null,将当前线程的ThreadLocal对象保存在ThreadLocalMap中,返回上层null。

2.5 remove函数

 public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

  也很简单,这里就不在啰嗦了  ,ok,到这里我们已经看完了ThreadLocal源码 ,真正的功能都是在ThreadLocalMap中进行操作的,这里我有一个疑问,为什么不适用HashMap来替代ThreadLocalMap呢?如一下代码:

class ThreadLocal {
private Map values = Collections.synchronizedMap(new HashMap()); public Object get() {
Thread curThread = Thread.currentThread();
Object o = values.get(curThread);
if (o == null && !values.containsKey(curThread)) {
o = initialValue();
values.put(curThread, o);
}
return o;
} public void set(Object newValue) {
values.put(Thread.currentThread(), newValue);
}
}

    这样貌似也没问题啊,乍一看的确没毛病,但是我们知道ThreadLocal本意是避免并发,用一个全局Map显然违背了这一初衷,且会导致内存泄漏,用Thread当key,除非手动调用remove,否则即使线程退出了会导致:1)该Thread对象无法回收;2)该线程在所有ThreadLocal中对应的value也无法回收。

  这时候会有同学提出疑问了,你使用ThreadLocalMap的话就不会导致内存泄漏吗?

  很多人认为:threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了。在这种情况下我们的确会存在value的泄漏。

  我们看上图,每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收, 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。

  我们从之前的ThreadLocalMap源码可以知道,弱引用只存在于key上,所以key会被回收,但当线程还在运行的情况下,value还是在被线程强引用而无法释放,只有当线程结束之后或者我们调用set、get方法的时候回去移除已被回收的Entry( replaceStaleEntry这个方法),给出的建议是,当ThreadLocal使用完成的时候,调用remove方法将value移除掉,这样就不会存在内存泄漏了。

3,总结

  我们代码看完了,需要对ThreadLocal的使用场景进行总结一下 :

  ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

  ok,感觉好久都没有些博客了,思路和语言表达都有些生疏,争取后面一直坚持写一写,加油

  

  

Java -- 基于JDK1.8的ThreadLocal源码分析的更多相关文章

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

    1,上周末我们一起分析了ArrayList的源码并进行了一些总结,因为最近在看Collection这一块的东西,下面的图也是大致的总结了Collection里面重要的接口和类,如果没有意外的话后面基本 ...

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

    1,前言 很久没有写博客了,很想念大家,18年都快过完了,才开始写第一篇,争取后面每周写点,权当是记录,因为最近在看JDK的Collection,而且ArrayList源码这一块也经常被面试官问道,所 ...

  3. Java集合基于JDK1.8的LinkedList源码分析

    上篇我们分析了ArrayList的底层实现,知道了ArrayList底层是基于数组实现的,因此具有查找修改快而插入删除慢的特点.本篇介绍的LinkedList是List接口的另一种实现,它的底层是基于 ...

  4. Java集合基于JDK1.8的ArrayList源码分析

    本篇分析ArrayList的源码,在分析之前先跟大家谈一谈数组.数组可能是我们最早接触到的数据结构之一,它是在内存中划分出一块连续的地址空间用来进行元素的存储,由于它直接操作内存,所以数组的性能要比集 ...

  5. Java并发编程笔记之ThreadLocal源码分析

    多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作的顺序是不可预期的,多线程访问同一个共享变量特别容易出现并发问题,特别是多个线程需要对一个共享变量进行写入时候, ...

  6. 基于jdk1.8的ArrayList源码分析

    前言ArrayList作为一个常用的集合类,这次我们简单的根据源码来看看AarryList是如何使用的. ArrayList拥有的成员变量 public class ArrayList<E> ...

  7. Java并发编程之ThreadLocal源码分析

    ## 1 一句话概括ThreadLocal<font face="微软雅黑" size=4>  什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象 ...

  8. 【JAVA】ThreadLocal源码分析

    ThreadLocal内部是用一张哈希表来存储: static class ThreadLocalMap { static class Entry extends WeakReference<T ...

  9. Java多线程学习之ThreadLocal源码分析

    0.概述 ThreadLocal,即线程本地变量,是一个以ThreadLocal对象为键.任意对象为值的存储结构.它可以将变量绑定到特定的线程上,使每个线程都拥有改变量的一个拷贝,各线程相同变量间互不 ...

随机推荐

  1. shell test条件检查

    Shell test 命令 Shell中的 test 命令用于检查某个条件是否成立,它可以进行数值.字符和文件三个方面的测试. 数值测试 参数 说明 -eq 等于则为真 -ne 不等于则为真 -gt ...

  2. 自助法(Bootstraping)

    自助法(Bootstraping)是另一种模型验证(评估)的方法(之前已经介绍过单次验证和交叉验证:验证和交叉验证(Validation & Cross Validation)).其以自助采样 ...

  3. inotify 监控文件系统操作

    path0=path1=########################################################dir2watch1=/home/nanjing2/GridON ...

  4. UML的使用

    软件工程项目这周要交一个设计文档,其中涉及UML图的画法,根据上课给的ppt做一个记录. 有关于UML的介绍在这里不再赘述,直接开整! UML的基本模型 当然必要的介绍必不可少,这里先介绍UML的基本 ...

  5. 剑指offer:数组中只出现一次的数字

    题目描述: 一个整型数组里除了两个数字之外,其他的数字都出现了两次.请写程序找出这两个只出现一次的数字. 思路分析: 1. 直接想法,每个数字遍历,统计出现次数,复杂度O(n^2),超时. 2. 借助 ...

  6. 【转】URL短地址压缩算法 微博短地址原理解析 (Java实现)

    转自: URL短地址压缩算法 微博短地址原理解析 (Java实现) 最近,项目中需要用到短网址(ShortUrl)的算法,于是在网上搜索一番,发现有C#的算法,有.Net的算法,有PHP的算法,就是没 ...

  7. 【转】使用eclipse的todo标签管理任务

    Eclipse中的一些特殊的注释技术包括:    1.    // TODO —— 表示尚未完成的待办事项.    2.    // XXX —— 表示被注释的代码虽然实现了功能,但是实现方案有待商榷 ...

  8. canvas api 速记

    基本骨骼 <canvas id="canvas" width=1000 height=1000 style="border: 1px black dotted&qu ...

  9. 【Git】.git/FETCH_HEAD: Permission denied 的解决方法

    背景: 用webhook去拉取代码.报错 .git/FETCH_HEAD: Permission denied 原因分析:.git/FETCH_HEAD的这个文件所属组和所属主是root权限,而我用w ...

  10. QLineEdit 加省略号

    第一种方法: QFontMetrics elidfont(ui->lineEdit->font()); ui->lineEdit->setText (elidfont.elid ...