ThreadLocal
这个类提供线程局部变量,这些变量与其他正常的变量的不同之处在于,每一个访问该变量的线程在其内部都有一个独立的初始化的变量副本;
ThreadLocal
实例变量通常采用private static
在类中修饰。只要
ThreadLocal
的变量能被访问,并且线程存活,那每个线程都会持有ThreadLocal
变量的副本。当一个线程结束时,它所持有的所有ThreadLocal
相对的实例副本都可被回收。
一句话说就是 ThreadLocal
适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用(相同线程数据共享),也就是变量在线程间隔离(不同的线程数据隔离)而在方法或类间共享的场景。
ThreadLocal 的使用场景:
对同一个线程调用的多个方法中,共享了某一个变量,这个变量需要传递到多个方法中,这样传来传去太麻烦了,这时就可以采用 ThreadLocal 了。
存储单个线程上下文信息。比如存储id等;
使变量线程安全。变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了;
对象实例与 ThreadLocal
变量的映射关系是由线程 Thread
来维护的
其实就是对象实例与 ThreadLocal
变量的映射关系是存放的一个 Map
里面(这个 Map
是个抽象的 Map
并不是 java.util
中的 Map
),而这个 Map
是 Thread
类的一个字段!而真正存放映射关系的 Map
就是 ThreadLocalMap
。ThreadLocalMap保存的是Entry结点,Entry结点中保存了ThreadLocal对象和threadLocal对象相关联的线程局部变量
public class Demo01 {
private String string;
private String getString() {
return string;
}
private void setString(String string) {
this.string = string;
}
public static void main(String[] args) {
int threads = 9;
final Demo01 demo = new Demo01();
// CountDownLatch countDownLatch = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
Thread thread = new Thread(() -> {
if (demo.getString() == null){
demo.setString(Thread.currentThread().getName());
}
System.out.println("demo.getString()================>"+ demo.getString());
//countDownLatch.countDown();
}, "执行线程 - " + i);
thread.start();
}
}
}
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
public class Demo01 {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
private String getString() {
return threadLocal.get();
}
private void setString(String string) {
threadLocal.set(string);
}
public static void main(String[] args) {
int threads = 9;
Demo01 demo = new Demo01();
CountDownLatch countDownLatch = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
Thread thread = new Thread(() -> {
if (demo.getString() == null){
demo.setString(Thread.currentThread().getName());
}
//demo.setString(Thread.currentThread().getName());
System.out.println("demo.getString()================>" + demo.getString());
countDownLatch.countDown();
}, "执行线程 - " + i);
thread.start();
}
}
}
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 1
demo.getString()================>执行线程 - 2
demo.getString()================>执行线程 - 3
demo.getString()================>执行线程 - 4
demo.getString()================>执行线程 - 5
demo.getString()================>执行线程 - 6
demo.getString()================>执行线程 - 7
demo.getString()================>执行线程 - 8
/**
* hreadLocalHashCode ---> 用于threadLocals的桶位寻址:
* 1.线程获取threadLocal.get()时:
* 如果是第一次在某个threadLocal对象上get,那么就会给当前线程分配一个value,
* 这个value 和 当前的threadLocal对象被包装成为一个 entry
* 其中entry的 key 是threadLocal对象,value 是threadLocal对象给当前线程生成的value
* 2.这个entry存放到当前线程 threadLocals 这个map的哪个桶位呢?
* 桶位寻址与当前 threadLocal对象的 threadLocalHashCode有关系:
* 使用 threadLocalHashCode & (table.length - 1) 计算结果得到的位置就是当前 entry 需要存放的位置。
*/
private final int threadLocalHashCode = nextHashCode();
/**
* nextHashCode: 表示hash值
* 每创建一个threadLocal对象时,就会使用 nextHashCode 分配一个hash值给这个对象
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* HASH_INCREMENT: 表示hash值的增量~
* 每创建一个ThreadLocal对象,ThreadLocal.nextHashCode的值就会增长HASH_INCREMENT(0x61c88647)
* 这个值很特殊,它是斐波那契数也叫黄金分割数。
* hash增量为这个数字,带来的好处就是hash分布非常均匀。
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* 返回一个nextHashCode的hash值:
* 创建新的ThreadLocal对象时,使用这个方法,会给当前对象分配一个hash值。
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
/*
* 初始化一个起始value:
* 默认返回null,一般情况下都是需要重写这个方法的
*/
protected T initialValue() {
return null;
}
public ThreadLocal() {
}
getMap
、getEntry
、setInitialValue
、/**
* 如果当前线程没有分配局部变量,则使用 initialValue方法去分配初始局部变量值!
*/
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// getMap(t):获取到当前线程Thread对象的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果条件成立:说明当前线程已经拥有自己的ThreadLocalMap对象了
if (map != null) {
// key:当前threadLocal对象(this)
// 根据key调用map.getEntry()方法,获取threadLocalMap中该threadLocal关联的entry
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果条件成立(当前获取的entry不为空):
// 说明当前线程初始化过 ThreadLocal对象与当前threadLocal对象相关联的线程局部变量!
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
// 返回value值
return result;
}
}
// 执行到这里有几种情况?
// 情况1:当前线程对应的threadLocalMap是空
// 情况2:当前线程与当前threadLocal对象没有生成过相关联的线程局部变量,也就是当前threadLocal对象没有被初始化到table中
// setInitialValue方法初始化当前线程与当前threadLocal对象相关联的线程局部变量value值,
// 且当前线程如果没有threadLocalMap的话,还会初始化创建map!
return setInitialValue();
}
getMap
获取当前线程t的ThreadLocalMap对象// ThreadLocalMap(位于Thread类中)
ThreadLocalMap getMap(Thread t) {
// 返回当前线程的 threadLocals
return t.threadLocals;
}
getEntry
得到当前的ThreadLocal实例在table中的ThreadLocal对象(存有当前threadLocal相关的局部变量)private Entry getEntry(ThreadLocal<?> key) {
// 传入ThreadLocal引用的key,然后通过按位与找到key在map中的位置
int i = key.threadLocalHashCode & (table.length - 1);
// 取出此Entry(里面存着ThreadLocal对象,和threadLocal对象相关联的线程局部变量)
Entry e = table[i];
// 非空判断,是否与传入的ThreadLocal对象相同
if (e != null && e.get() == key)
return e;
else
// 走到这里说明两个不同的ThreadLocal对象hash到了同一个位置,然后就从此位置一次往后找,没找到就返回null
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;
// e不等于null,而k等于null 说明当前e已经过期了,就把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);
}
setInitialValue
初始化当前线程与当前threadLocal对象相关联的线程局部变量value值/**
* setInitialValue方法初始化当前线程与当前threadLocal对象相关联的线程局部变量value值,
* 且当前线程如果没有threadLocalMap的话,还会初始化创建map!
* @return the initial value
*/
private T setInitialValue() {
// 调用的当前ThreadLocal对象的initialValue方法,这个方法大部分情况下咱们都会重写来给当前 ThreadLocal 对象赋初始值。
// value值就是当前ThreadLocal对象与当前线程相关联的线程局部变量。
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取当前线程内部的threadLocals(threadLocalMap对象)
ThreadLocalMap map = getMap(t);
// 如果条件成立:说明当前线程内部已经初始化过threadLocalMap对象了(线程的threadLocals只会初始化一次)
if (map != null)
// 向ThreadLocalMap中保存当前threadLocal与当前线程生成的线程局部变量。
// key: 当前threadLocal对象
// value:线程与当前threadLocal相关的局部变量
map.set(this, value);
// 如果执行到else ---> 说明当前线程内部threadLocalMap对象还没有初始化过:
else
// 这里调用createMap方法给当前线程创建ThreadLocalMap对象:
// 参数1:当前线程t
// 参数2:线程与当前threadLocal相关的局部变量
createMap(t, value);
// 返回线程与当前threadLocal相关的局部变量,第一次就为空
return value;
}
createMap
创建当前线程的ThreadLocalMap对象/**
* 创建当前线程的ThreadLocalMap对象
*/
void createMap(Thread t, T firstValue) {
// 传递t的意义就是要访问当前这个线程 t.threadLocals字段,给这个字段初始化:
// new ThreadLocalMap(this, firstValue):
// 创建一个ThreadLocalMap对象,初始k-v为:
// key:this <当前threadLocal对象>
// value:线程与当前threadLocal相关的局部变量
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* 修改当前线程与当前threadLocal对象相关联的线程局部变量:
*/
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的threadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果条件成立:说明当前线程的threadLocalMap已经初始化过了
if (map != null)
// 调用threadLocalMap.set方法进行重写或者添加:
map.set(this, value);
// 如果执行到else ---> 说明当前线程内部threadLocalMap对象还没有初始化过:
else
// 这里调用createMap方法给当前线程创建ThreadLocalMap对象:
// 参数1:当前线程t
// 参数2:线程与当前threadLocal相关的局部变量
createMap(t, value);
}
map.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;
// 找到key的位置
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果该位置原来就有ThreadLocal对象,就直接覆盖
if (k == key) {
e.value = value;
return;
}
// 如果该位置没有,说明当前entry是过期数据(因为e!=null,说明已经初始化过),这个时候可以强行占用该桶位
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// e=null,就直接初始化一个Entry,里面存有ThreadLocal对象和其threadLocal相关的局部变量
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* 移除当前线程与当前threadLocal对象相关联的线程局部变量:
*/
public void remove() {
// 获取当前线程的threadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果条件成立:说明当前线程已经初始化过threadLocalMap对象了
if (m != null)
// 调用threadLocalMap.remove( key = 当前threadLocal)移除线程局部变量
m.remove(this);
}
set(value)
设置值的时候,是往各自的ThreadLocalMap对象数组中设置值。threadLocalHashCode
计算而来。即多线程环境下ThreadLocal对象的threadLocalHashCode
是共享的。threadLocalHashCode
是一个原子自增的变量,通过类方法initValue
初始化值。ThreadLocal local = new ThreadLocal();
时,就会初始化threadLocalHashCode
的值,这个值不会再变。所以,同一个线程在同一个ThreadLocal对象中set()
值,只能保存最后一次set
的值。threadLocalHashCode
都不一样,也就映射到ThreadLocalMap对象数组下的不同下标。2/3
,当数组中,存储 Entry
节点的个数大于等于 2/3
时,会它并不会直接开始扩容。而是先调用 rehash()
方法,在该方法中,全面扫描整个数组,并将数组中过期的数据(key == null
)给清理掉,重新整理数组。如果重新整理数组,并将过期的数据清理后,再次重新判断数组内的 Entry
节点的个数是否达到扩容阈值的3/4
,如果达到再调用真正扩容的方法resize();
resize()
方法内部?
resize()
方法在真正执行扩容时,内部逻辑是先创建一个新的数组,新数组长度是原来数组长度的 2 倍。threshold
。public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
Object c = new Object();
Object strongA = a;
SoftReference<Object> softB = new SoftReference<>(b);
WeakReference<Object> weakC = new WeakReference<>(c);
a = null;
b = null;
c = null;
System.out.println("Before gc...");
System.out.println(String.format("strongA = %s, softB = %s, weakC = %s", strongA, softB.get(), weakC.get()));
System.out.println("Run GC...");
System.gc();
System.out.println("After gc...");
System.out.println(String.format("strongA = %s, softB = %s, weakC = %s", strongA, softB.get(), weakC.get()));
}
Before gc...
strongA = java.lang.Object@61bbe9ba, softB = java.lang.Object@610455d6, weakC = java.lang.Object@511d50c0
Run GC...
After gc...
strongA = java.lang.Object@61bbe9ba, softB = java.lang.Object@610455d6, weakC = null
ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的
由于Entry的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除并且还会调用expungeStaleEntry
移除key=null的Entry,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
参考
ThreadLocal源码分析_02 内核(ThreadLocalMap)
ThreadLocal原理详解——终于弄明白了ThreadLocal
深挖ThreadLocal
ThreadLocal原理及内存泄露预防