ThreadLocal是
一种线程隔离机制,为我们提供了另一种解决线程并发访问的问题,利用副本机制解决了并发变量访问安全,采用了空间换时间的策略。
1、threadlocal的使用
如果希望变量在线程运行的时候不受其他线程更新的干扰,想让它成为一个线程级别的变量,则可以考虑使用threadlocal。
public class ThreadLocalTest {
//值的初始化
static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
Thread thread[] = new Thread[3];
for (int i = 0; i < 3; i++) {
thread[i] = new Thread(new Runnable() {
@Override
public void run() {
int num = threadLocal.get(); //local变量值的获取;
threadLocal.set(num + 1); //local变量值的设置;
System.out.println("threadLocal get: " + Thread.currentThread().getName() + " " + threadLocal.get());
}
}, "t" + i);
}
for (int i = 0; i < 3; i++) {
thread[i].start();
}
}
}
运行结果
t1 threadLocal get: 1
t2 threadLocal get: 1
t0 threadLocal get: 1
从结果可以看到,线程之间threadlocal变量是互不影响的,互相隔离的,保证了访问的安全性。
2、ThreadLocal的数据结构
副本机制保证变量的独享性
加锁保证了变量在多线程环境下的读写可见性,而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,也就是必须都是new出来的,直接把某个对象在各个线程中实例化一份,每个线程都有属于自己的对象,让变量与当前的线程绑定,类似copy on write技术的思想;空间换时间,多线程之间不能共享,从而隔离了多个线程对数据的访问冲突。比如context和session。
ThreadLocal类源码分析
:每个Thread内部维护着一个ThreadLocalMap,它是一个Map。这个映射表的Key是一个弱引用,其实就是ThreadLocal对象this本身,Value是真正存的线程变量Object。
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647; //斐波那契黄金分割魔数 - 线性探测-随机值的产生;
public T get();
public void set(T value);
public void remove() ;
protected T initialValue() ; //给变量初始化
}
//threadlocal 的静态内部类
static class ThreadLocalMap {
//key为弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; //强引用 value
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//初始大小 16
private static final int INITIAL_CAPACITY = 16;
//散列表table
private Entry[] table;
//初始元素个数
private int size = 0;
//扩容的阈值
private int threshold; // Default to 0
//扩容的条件:长度达到总len * 2/3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
thredlocal的数据结构是什么?
内部实现:
使用了thradlocal 每个线程都有一个threadlocalMap对象,这是一个集合,这个集合里面会存储一个key-value的数据:
-
每个线程都保存一个threadlocalmap对象;
-
Map的内容:Entry: key-value:
-
散列表冲突解决:线形探测,里面用到了一个黄金分割数-- 魔数: 0x61c88647
threadLocal的key和value是什么?
-
key:this本身 ,弱引用,WeakReference<this>,这也是为什么可以直接get和set值,因为key已定:this对象;
-
value:我们设置的值,强引用;

3、ThreadLocal是否一定安全呢?
ThreadLocal使变量与线程作用域绑定,避免了线程共享,但是使用thradlocal就一定是安全的吗,这里需要注意如果设置的变量本身就是一个共享的对象,那还是会有安全问题,例如
当thradlocal是引用类型的数据的时候还是会存在数据安全问题。
java的参数的传递:
-
传值; - 副本机制
-
传引用 - 本质是共享
实例:
public class ThreadLocalRef {
public static class NumIncrease {
int num;
public void incr() {
num++;
}
public NumIncrease() {
this.num = 0;
}
}
static NumIncrease numIncrease = new NumIncrease();
//此时传递的是地址,是一个引用类型的,当多个线程访问的时候还是存在线程安全问题;
static ThreadLocal<NumIncrease> threadLocalRef = new ThreadLocal<NumIncrease>() {
protected NumIncrease initialValue() {
return numIncrease;
}
};
//此时传递的是一个副本值,是一个值类型的,当多个线程访问的时候就不存在线程安全问题;
static ThreadLocal<NumIncrease> threadLocal = new ThreadLocal<NumIncrease>() {
protected NumIncrease initialValue() {
return new NumIncrease();
}
};
public static void main(String[] args) {
for (int i = 0; i < 3; i++)
new Thread(() -> {
numIncrease.incr();
System.out.println(Thread.currentThread().getName() + "threadLocalRef " + threadLocalRef.get().num);
System.out.println(Thread.currentThread().getName() + " threadLocal " + threadLocal.get().num);
}).start();
}
}
运行结果:
Thread-0threadLocalRef 2
Thread-2threadLocalRef 3
Thread-2 threadLocal 0
Thread-1threadLocalRef 2
Thread-1 threadLocal 0
Thread-0 threadLocal 0
从结果可以看出,threadLocalRef是的对象地址,引用类型还是存在线程安全问题,值变成了不确定的2-2-3;而传递副本址的threadLocal就很好的做到了线程变量的隔离,这里需要注意。
4、Hash冲突的解决
Threadlocal对hash冲突的处理策略是:
线性探测法
Hash表是可以根据key进行直接访问的数据结构,也就是说我们可以通过 hash函数把key映射到hash表中的一个位置来访问记录,从而加快查找的速度,存放记录的数据就是hash表(散列表),当我们针对一个key通过hash函数计算产生的一个位置,在hash表中已经被另外一个键值对占用时,那么线性探测解决这个冲突就分两种情况:
-
写入: 查找hash表中离冲突单元最近的空闲单元,把新的键值插入到这个空闲单元;
-
查找: 根据hash函数计算的一个位置处开始往后查找,指导找到与key对应的value或者找到空的单元;
其他的hash冲突解决:
-
再hash法;
-
开放链地址法;
-
建立一个 公共的溢出区;
冲突hash散列的黄金魔数
每个threadlocal对象都有一个hash值threadLocalHashCode,每初始化一个threadLocal对象,hash值就增加一个固定的大小:0x61c88647。
分布的比较均匀,避免了冲突,散列堆积,保证大概率索引的下一个坑是没有数据插入的。
hashcode中的黄金分割魔数:
0x61c88647
,会让hashcode非常均匀;
private static final int HASH_INCREMENT = 0x61c88647;
public static void magicHash(int size) {
int hashCode = 0;
for (int i = 0; i < size; i++) {
hashCode = i * HASH_INCREMENT + HASH_INCREMENT;
System.out.println(hashCode & (size -1)); //todo 可以发现非常均匀的分布
}
}
5、内存泄漏
源码中threadlocalmap中key和value的定义:
//key继承来弱引用,WeakReference,在gc的时候会被回收;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; //value是强引用
Entry(ThreadLocal<?> k, Object v) {
super(k); //key是弱引用,jvm在下一轮gc的时候就会回收;
value = v;
}
}
Entry类继承了WeakRefeeence<Threadlocal>,即每个Entry对象都有一个threadlocal弱引用。
-
key是this的 threadlocal弱引用;在下次gc的时候就会被回收;
-
value是强引用;
内存泄漏原因:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应被gc回收key的value就会导致内存泄漏。
分析:
因为如果弱引用key=ThreadLocal Ref被回收,key= threadLocal = null,但value此时还存在一条强引用链,
Current
Thread
--> ThreadLocalMap-->Entry—>Value,这条强引用链会导致Entry不会回收,占着10m的内存,因为key被回收,此value永远也不会被访问到,
该回收的内存没有回收,造成了内存的泄漏,只有当线程结束,value才能被回收。也就是说,发生的内存泄漏的时间段是:
key被回收到线程销毁的时间段内;但是如果线程一直存在,那么内存泄漏将一直存在。

结论:
只要这个线程对象能被及时回收,这个内存泄漏就能及时避免,只是在key被gc回收到线程结束期间value不会被回收,就发生了内存泄漏。
但重要的是:如果线程对象不被回收,泄漏就比较严重了,value会一直存在,尤其在高并发的场景:
-
* 静态变量:使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
-
* 线程池:分配使用了ThreadLocal又不再调用get()、set()、remove()方法,那么就会导致内存泄漏。
解决办法:
-
使用完,调用remove()方法;
-
为了减小内存泄漏的可能性,threadlocal本身也有清除策略;每次get和set的时候都会清除map里所有key为null的value;
6、local的继承-InheritableThreadLocal
-
父子线程的ThreadLocal时可以传递继承的,InheritableThreadLocal
-
子线程get父线程的值,且父子线程之间互不影响;
代码实例:
public class InheritableThreadLocal {
//可继承的threadlocal
public static final ThreadLocal<String> local = new java.lang.InheritableThreadLocal<>();
public static void main(String[] args){
local.set("parent"); //父进程main
System.out.println(Thread.currentThread().getName() + " " + local.get()); //parent
new Thread(()->{
ThreadLocal<String> localson = new ThreadLocal<>();
localson.set("king-son");
System.out.println(Thread.currentThread().getName() + local.get()); //parent
local.set("son");
System.out.println(Thread.currentThread().getName() + local.get()); //son
System.out.println(Thread.currentThread().getName() + localson.get());//king-son
}).start();
System.out.println(Thread.currentThread().getName() + local.get()); //parent
}
}
7、ThreadLocal源码分析
ThreadLocal的使用和方法非常简单,来看看具体做来那些事情。

7.1、set()
初始化:
前面分析了
set
方法第一次初始化
ThreadLocalMap,初始化一个大小为16的map
的过程
扩容条件:
当容量达到2/3时-
装载因子:0.66
,扩容为2倍。主线程定义几个thradlocal变量,map中就有几个key;
长度为2^n,和hashmap一样,方便位运算,提高效率;
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
map不为空时set方法的执行逻辑:
-
根据key(key是localhost的对象,value是值)的散列哈希计算Entry数组下标;
-
通过线性探索探测从i开始往后一直遍历到数组的最后一个Entry
-
如果map中的key和传入key相等,表示数据已经存在,直接覆盖;
-
如果map中key为空,则用新的key/value覆盖,并清理key=null的数据;
-
判断是否需要rehash扩容;
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //每个线程都会存储一个ThreadLocalMap,获取map数据结构;
if (map != null)
map.set(this, value); //设置值
else
createMap(t, value); //如果map为空,则创建一个map;
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); //获取value的索引值下标
//这个for循环就是线形探测的过程
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get(); //得到的是弱引用值
if (k == key) { //防止hash冲突,比较key的值;相等修改完成返回;
e.value = value;
return;
}
if (k == null) { //e != null,但是k == null ,这里说明什么?说明key被jvm回收了
replaceStaleEntry(key, value, i); //清理策略,防治内存泄漏:替换脏数据,这里也是尽量避免内存泄漏问题;
return;
}
}
tab[i] = new Entry(key, value); //e == null,直接赋值就OK了;
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash(); //如果map中的值的个数大于阈值,大于总容量的2/3,直接rehash;
}
7.2、get()方法
从map中取值很简单,这里主要看一下thradlocal在get的时候,初始化map的过程;
public T get() {
Thread t = Thread.currentThread(); //得到当前的线程对象
ThreadLocalMap map = getMap(t); //获取threadlocalmap对象
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //通过key-this 获取值:value
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果map为空则初始化map
return setInitialValue();
}
//从这里可以看出threadlocals是线程t的一个成员变量;
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //每个线程都有一个threadlocal变量
}
private T setInitialValue() {
T value = initialValue(); //得到初始化的值;
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 得到threadlocalmap对象
if (map != null)
map.set(this, value); //map已创建,则直接set值;
else
createMap(t, value); //创建thradlocalmap
return value;
}
创建map对象
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue); // 创建实例化map对象
}
private static final int INITIAL_CAPACITY = 16; //初始大小 16
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//设置table的初始大小;
table = new Entry[INITIAL_CAPACITY];
//得到value在hashtable中的下表索引位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//将值key-value存入entry中;
table[i] = new Entry(firstKey, firstValue);
//记录map中值的个数
size = 1;
//设置threadlocal的扩容大小为 len*2/3
setThreshold(INITIAL_CAPACITY);
}
//达到总容量的 2/3 就开始扩容
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
至此,threadlocalmap 创建工作完成。
7.3、remove()
所以最稳妥的方法就是在使用完ThreadLocal后及时调用
remove()方法。该方法会查找以当前Threadlocal对象为key的Entry,及时清理这个元素的key/value/entry本身,将其设置为null;并且也会向前向后查找清除key=null的数据value;
ps: 在使用
线程池的情况下,及时调用remove()清理ThreadLocalMap的脏值,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
//清理不用的entry节点
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) { // 找到要清除的key,直接clear()
e.clear(); // 清除key-value
expungeStaleEntry(i); //执行清除策略,向前向后查找
return;
}
}
public void clear() {
this.referent = null;
}
7.4、ThreadLocal的内存清理策略
为了更好的使用内存,ThreadLocalMap本身对
已被回收key的value内存的清理也做了很多的工作,例如每次执行get()和set()方法的时候,如果get和set的时候获取的key=null,则附近大概率节点也会为null,所以就会
向前或者向后查找具体key=null的value数据进行清理。
执行垃圾清理策略的时机?
-
rehash() - 执行一次rehash()-扩容操作的时候会对key=null的值进行清理;
-
remove();直接清除key=null的值;
-
get()和set()方法的调用时会自动向前向后查找key=null的value进行清除;
// 替换清理过期的Entry节点
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
//向前查找清理key被回收无用的value
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
//向后查找,清理key被回收无用的value
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;
// 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);
}
8、threadlocal与同步机制的比较
都是为了解决多线程中相同变量的访问冲突问题,
ThreadLocal和线程同步机制相比有什么优势呢?
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
对于多线程资源共享的问题,
同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问;而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
9、问题思考
思考
一:为什么
key
不是
Thread ID?
用线程id做key,无法区分多个value。
如果是thread id,那一个thread就只能存储一个value值。从 controller -> service -> dao,这都是同一个线程,这样变量的设置就局限为一个了。为了满足同一个线程经历不同的阶段有不同的threadlocal,所以放弃了threadid,采用thread的this变量更加灵活。
思考二:
key为什么要使用弱引用?
弱引用帮我们更好的管理我们的内存资源,能及时的释放不再使用的线程级别的对象;主要是为了保证threadlocal对象内存的及时回收,弱引用的对象只能生存到下一次gc回收前;如果每个key都是强引用,那么这个threadlocal就会因为和entry存在强引用无法被回收,但其实大部分的线程级别的变量的生命周期都很短,除非线程结束,线程被回收了,map才会被回收,这样浪费了内存。
强引用如果线程不回收,其实还是会引起内存的泄漏,因为线程也是gc-root,这样这个已经过期的本该回收的数据一直存在一个强引用,可达性分析还是可达,gc不会被回收,造成内存泄漏。
思考三
:为什么所有的
Thread不
共享一个全局的
ConcurrentHashMap
?
既然每个Thread都有自己的局部变量存储空间,那map肯定是属于Thread类的成员变量,全局的map反而需要全局并发控制。
思考四:
为什么采取线性探测
处理冲突
,不采用链表?
-
第一:threadlocal使用数据量小,不像Hashmap一次存放的数据量比较大;
-
第二: 查询加速,利用线性探测的方式使得内存地址连续,查询效率极高,可以采用缓存加速;
但是线性探测也有缺点:
数据量多的情况下,冲突增加,
容易产生数据堆积;
10、小结
ThreadLocal虽然用法简单,但是还是有很多小细节值得注意和学习。
使用场景
-
多线程访问共享变量的时候,每个线程都希望获得该变量的初始值的一个副本,线程级别控制的变量;
-
多数据源的切换,数据库的访问对connection的控制;
-
线程级别参数的优雅传递,避免冗余的型参传递;分布式锁的uuid传递;
-
跟踪线程执行,traceId
使用建议
-
一个线程只存放一个threadlocal,避免了冲突的发生,值少会降低冲突的概率;
-
通过initialValue来初始化threadlocal变量的值;默认返回null
-
使用完毕后,调用remove()方法避免内存泄漏;
-
虽然是副本机制,但是也要考虑到引用地址的影响; 如果是引用对象,那么还是会存在数据安全问题;
OK---莫等闲、白了少年头,空悲切。
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。