写点什么

当我们谈到 ThreadLocal 的时候,我们在谈什么?

用户头像
Jason
关注
发布于: 2020 年 05 月 06 日
当我们谈到ThreadLocal的时候,我们在谈什么?

一、ThreadLocal是什么



从名称来看ThreadLocal的直接翻译就是线程本地,可以粗糙的理解成当前现成的本地数据,是不和其他线程共享的数据。但是这么理解是不是太片面呢,这里我们看一下JDK源码对ThreadLocal的注释是什么吧。



1. JDK源码说明



/**
*
* 这个类提供线程局部变量。这些变量与普通的变量不同,因为每个访问的线程(通过其get或set方法)都有
* 自己的独立初始化的变量副本。ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态
* (private static)字段(例如:一个用户ID或事务ID)。
*
* 只要线程存活并且ThreadLocal实例可以访问,每个线程都保存对其线程局部变量副本的隐含引用;
* 线程结束之后,线程本地实例的所有副本都将被垃圾回收(除非存在对这些副本的其他引用)。
* @author Josh Bloch and Doug Lea
* @since 1.2
*/



2. 个人理解



根据Jdk源码注释,我们可以得到以下理解:



  1. 每一个线程都在Threadlocal中单独存储,将ThreadLocal对象作为key,将存储的类型作为Value直接存储到ThreadLocalMap中;

  2. 由于是按照线程进行区分的,各个线程之间的变量不会互相影响;

  3. 因为ThreadLocal是和线程绑定的,如果是使用线程池的话,如果之前的线程没有在使用结束的时候执行remove操作,等到线程池再轮循到这个线程的时候,可能会读取到脏数据;

  4. 一个ThreadLocal在同一个线程中只能存储一个对象,如果多次执行set操作,后面存储的对象会覆盖前面存储的对象

  5. 由于这种kv的数据结构,我们可以粗略的将ThreadLocal理解成一个HashMap,只不过key是Threadlocal本身而已,有趣的时候,ThreadLocal在执行set的时候,也会执行自己的Hash寻址算法,这点和Hashmap很像。



3. 使用场景



如果你希望构造这样一个对象,将这个对象设置为共享变量,并统一设置初始值。但是你还希望每个线程对这个值的修改都是互相的独立的。那么这个对象就是ThreadLocal



4. ThreadLocal和Thread的关系



ThreadLocal有一个静态内部类叫ThreadLocalMap,它还有一个静态内部类Entry,在Thread中的ThreadLocalMap属性的赋值是在ThreadLocal类中的createMap中进行的。ThreadLocal和ThreadLocalMap有三组对应的方法:get、set和remove,在ThreadLocal中对它们只做校验和判断,最终的实现会落在ThreadLocalMap上。Entry继承自WeakReference,没有方法,只有一个value成员变量,它的key是threadLocal对象。



 // 静态内部类Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// Thread类中的Thread'Local'Map
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;



简单梳理一下他们的关系:



  1. 1个Thread有且仅有一个ThreadLocalMap对象;

  2. 1一个Entry对象的Key弱引用指向1个ThreadLocal对象;

  3. 1个ThreadLocalMap对象可以存储多个Entry

  4. 1个ThreadLocal对象可以被多个线程所共享

  5. ThreadLocal对象不持有value,value由线程的entry对象持有



所有Entry对象都被ThreadLocalMap类实例化对象threadLocals持有。当线程对象执行完毕时,线程对象内的实例属性均会被垃圾回收。但是,ThreadLocal对象经常被设置为私有静态变量使用,那么其生命周期至少不会随着线程结束而结束。



二、ThreadLocal怎么用



1. API



返回值 方法名 备注

void set 存储

void remove 删除

T get 获取



2. 在多线程情况下的Demo



启动类

public class ThreadDemo {
// 声明一个ThreadLocal,存储类型为Integer
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
public static void main(String[] args) throws InterruptedException {
// 创建一个线程池大小为1的线程池
Executor executor = Executors.newFixedThreadPool(1);
// 执行四个线程,这四个线程分别会先执行get在执行set操作
for (int i = 0; i < 4; i++) {
executor.execute(new RunnableDemo());
}
while (true) {
// System.out.println(threadLocal.get());
}
}
}



线程类

public class RunnableDemo implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " get num : " + ThreadDemo.threadLocal.get());
Integer num = new Random().nextInt();
System.out.println(Thread.currentThread().getName() + " out num : " + num);
ThreadDemo.threadLocal.set(num);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}



输出结果:

pool-1-thread-1 get num : null
pool-1-thread-1 out num : 162662825
pool-1-thread-1 get num : 162662825
pool-1-thread-1 out num : -168394526
pool-1-thread-1 get num : -168394526
pool-1-thread-1 out num : 842018131
pool-1-thread-1 get num : 842018131
pool-1-thread-1 out num : 1188266731



结论:



由于我们的线程对象中没有对ThreadLocal执行remove方法,当线程池第二次轮询到这个线程的时候,直接执行threadLocal.get()方法获取到的还是上一次执行的结果。这种情况是要尽量避免,因为有可能因为没有执行remove操作,而导致第二次获取到的数据是错误的。



3. ThreadLocal无法解决共享对象的更新问题



如例子所示:

public class ThreadLocalDemo {
private static final StringBuilder INIT_VALUE = new StringBuilder("init");
private static final ThreadLocal<StringBuilder> builder = new ThreadLocal<StringBuilder>() {
@Override
protected StringBuilder initialValue() {
return INIT_VALUE;
}
};
private static class AppendStringThread extends Thread {
@Override
public void run() {
StringBuilder inThread = builder.get();
for (int i = 0; i < 10; i++) {
inThread.append("-").append(i);
}
System.out.println(Thread.currentThread().getName() + inThread.toString());
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new AppendStringThread().start();
}
TimeUnit.SECONDS.sleep(10);
}
}



输出结果:

Thread-1init-0-1-2-3-4-5-6-7-8-9
Thread-3init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
Thread-0init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
Thread-2init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9



可以看到输出的结构是乱序不可控的,所以使用讴歌引用来操作共享对象时,依然需要进行线程同步。



现在我们将AppendStringThread类中的intThread计算来做上锁来保证线程之间的同步机制,其他方法不边。AppendStringThread具体代码如下:

private static class AppendStringThread extends Thread {
@Override
public void run() {
StringBuilder inThread = builder.get();
ReentrantLock lock = new ReentrantLock(true);
lock.lock();
try {
for (int i = 0; i < 10; i++) {
inThread.append("-").append(i);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + inThread.toString());
}
}

输出结果:

Thread-0init-0-1-2-3-4-5-6-7-8-9
Thread-1init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
Thread-2init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
Thread-4init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
Thread-6init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
Thread-7init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
Thread-8init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
Thread-9init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
Thread-3init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
Thread-5init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9



三、ThreadLocal 源码分析



1. 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);
}



源码解析



set方法内的东西看起来比较少,甚至我们不用看具体实现就能大概知道他想做什么。执行逻辑:



  1. 获取当前线程;

  2. 获取当前线程的ThreadLocalMap内部类对象;

  3. 判断ThreadLocalMap内部类是否为空;

  4. 如果不为空则执行set操作,将当前ThreadLocal作为key,value作为值存储到ThreadLocalMap中;

  5. 如果ThreadMap是null,则执行新建操作,并且将给定的ThreadLocal作为key,传入的对象作为value写入ThreadLocal中;



具体是不是这样的呢,我们可以看一下代码。Thread t = Thread.currentThread();这段代码就不用解析了,因为不涉及到其他方法。我们直接来看第二行ThreadLocalMap map = getMap(t);



ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}



可以看到返回的当前线程的threadLocals,那我们追过去看一下threadLocals是个什么东西呢?



 ThreadLocal.ThreadLocalMap threadLocals = null;



从这里我们可以看到所谓t.threadLocals本质上是指向了ThreadLocal类的一个内部类ThreadLocalMap,我们可以看一下ThreadLocalMap内部类中的属性

/**
* 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



是不是看起来似曾相识,是不是有点像HashMap。我们这边先暂时按下对ThreadLocalMap这个内部类的好奇之心,继续往下看。ThreadLocal.set方法后面发生了什么,它开始判断这个ThreadLocalMap是不是null,刚才我们看源码得到ThreadLocal.ThreadLocalMap threadLocals = null;可以得知,第一次访问的时候这个值一定是null,那么它就会触发createMap(t, value);方法。



createMap(t, value);方法显然是一个初始化方法,我们看一下它做了什么:



 void createMap(Thread t, T firstValue) {
     t.threadLocals = new ThreadLocalMap(this, firstValue);
 }



将当前线程的threadLocals的成员变量指向了一个新的ThreadMap对象,将当前ThreadLocal对象作为key,存储的对象作为value对ThreadLocalMap进行初始化



接下来我们看一下ThreadLocalMap的构造器

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 将ThreadLocalMap的table属性初始化为一个长度为16的数组
table = new Entry[INITIAL_CAPACITY];
// 根据threadlocal对象进行hashcode和长度-1进行于预算,用来获取这个threadlocal放在数组中的位置,其实本质上这一步就是一个寻址算法,而且这寻址算法和hashmap的寻址算法及其相似,Hashmap中的寻址算法源码是这样的tab[i = (n - 1) & hash],为什么直接取模,是因为对于计算机来说这样的运算效率更高。
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 根据寻址算法找到的位置,将ThreadLocal作为key,存储的对象作为value封装成Entry对象存储到数组中
table[i] = new Entry(firstKey, firstValue);
// 将ThreadLocalMap中包含的元素个数修改为1
size = 1;
// 这个值类似于Hashmap中的threshold,是阈值,如果当前数组的大小大于它的时候就会触发rehash操作
setThreshold(INITIAL_CAPACITY);
}
private void setThreshold(int len) {
threshold = len * 2 / 3;
}



到现在为止,第一次初始化ThreadLocalMap的代码逻辑就已经全部梳理完了,现在我们看一下,第二次乃至第N次存储数据的时候,ThreadLocal是如何处理的



if (map != null) // 第二次、第三次、第N次存储的时候,map肯定不是null,就会触发ThreadLocalMap的set方法
map.set(this, value);



private void set(ThreadLocal<?> key, Object value) {
// 获取当前Thread'Local'Map的table对象,将其赋值给局部变量tab
Entry[] tab = table;
// 获取长度
int len = tab.length;
// 获取当前ThreadLocal应该存储到的下表位置
int i = key.threadLocalHashCode & (len-1);
// 进行循环,根据寻址算法给定的下表位置获取坐标,如果不是就继续循环下一个
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 获取entry的key,判断key是否和当前传入的key是同一个,如果是就覆盖value并结束set方法
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
// 如果当前的entry还没有初始化过值
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 将tab下标为i的对象赋值为new Entry
tab[i] = new Entry(key, value);
int sz = ++size;
// 判断是否需要扩容(见1.2) 判断增加这个entry之后,是否比阈值要大,如果比阈值要大就会进行rehash算法
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}



注1.2

private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
// 判断i+1是否超过了tab的长度,如果超过了返回0,否则返回i+1
i = nextIndex(i, len);
Entry e = tab[i];
// 如果entry不是null,但是没有Treadlocal作为key存储进去
// 将长度传入的n设置为当前tab的长度
// 将removed设置为ture
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}

2. get方法



public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
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;
}
}
// 如果当前线程的ThreadLocalMap为null或者当前的这个threadLocal对象在map中不存在,直接初始化
return setInitialValue();
}



threadLocal的方法非常简单,寥寥几行。大概的意思就是判断一下当前线程的threadLocalMap是否为null,如果不是null,再判断当前这个threadlocal对象是否存储过数据,如果存储过就直接返回存储的数据,如果没有存储过。再执行初始化操作setInitialValue



每个线程都有自己的ThreadLocalMap,如果map==null,则直接执行setInitialValue。如果map已经创建,则就表示Thread类的threadLocalMap属性已经初始化,如果e==null,依然会执行到setInitialValue。接下来我们看一下这个setInitialValue方法



private T setInitialValue() {
// 注1 这是一个保护方法,默认返回null,如果需要使用,需要覆写
T value = initialValue();
// 获取当前线程,并获取当前线程的ThreadLocalMap,如果为null则创建,否则直接写入
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}



注1



protected T initialValue() {
return null;
}



这个方法默认是返回的null,如果大家希望再初始化value的时候,给定一个不同的值,那么就需要继承ThreadLocal并重写此方法。通常用于匿名内部类中,例如:



private static final ThreadLocal<Integer> INIT_DEMO = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 1000;
}
};
public static void main(String[] args) throws InterruptedException {
System.out.println(INIT_DEMO.get());
}



上面代码中,就没有通过set方法给INIT_DEMO对象赋值,而是通过重写了initialValue方法,在INIT_DEMO对象调用get方法的时候给对象进行赋值的。



3. remove方法



public void remove() {
// 获取当前线程的threadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果map不是null,直接将key为当前threadlocal对象的entry删除掉
if (m != null)
// 根据key删除entry
m.remove(this);
}



remove方法应该是ThreadLocal中最简单的一个方法了,因为他不涉及到大量的方法调用,他就是获取到了当前线程的ThreadLocalMap对象,然后判断一下这个map是否为null,如果不是null,就尝试删除这个map中key为当前threadLocal的entry。下面是remove方法的源码



private void remove(ThreadLocal<?> key) {
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)]) {
// 找到key了,直接清空这个entry
if (e.get() == key) {
e.clear();
// 将这个key从数组中移除
expungeStaleEntry(i);
return;
}
}
}



四、ThreadLocal的副作用



1. 脏数据



线程复用会产生咱数据。由于线程池会重用Thread对象,那么与Thread绑定的静态属性ThreadLocal变量也会被宠用。如果在实现的线程的run方法中不显示地调用remove方法清理与线程相关的threadLocal信息,那么倘若下一个线程不调用set设置初始值,就可能get到重用的线程信息,包括threadlocal所关联的线程对象的value值。



我们在【2.在多线程情况下的Demo】中就复现了这个问题,我们创建了一个线程池大小固定为1的线程池。然后将四个线程放入线程池执行。



第一个线程执行完之后将162662825作为value存入threadLocal中,但是在线程的run方法中,并没有显示地调用remove方法。第一个线程执行完毕后,第二个线程开始执行。



第二个线程在执行set之前,先执行了get方法,然后就获取到了上一个线程执行过程中set到threadlocal中的值,于是就出现了如下的结果:



// 线程1
pool-1-thread-1 get num : null
pool-1-thread-1 out num : 162662825
// 线程2
pool-1-thread-1 get num : 162662825
pool-1-thread-1 out num : -168394526



2. 内存泄漏



在源码注释中提示使用static关键字来修饰ThreadLocal。在这个场景下, 寄希望于ThreadLocal对象失去引用后,触发弱引用机制来回收Entry的value就不现实了。在上例中,如果不进行remove操作,那么这个线程回收完之后,通过ThreadLocal对象持有的String对象是不会被释放的。



3. 解决方案



其实以上两个问题解决方法很简单,就是在每次用完ThreadLocal时,必须要及时调用remove方法显示的清理。



五、参考资料



《Java并发编程实战》

《码出高效 Java开发手册》

《JDK1.8源码》



发布于: 2020 年 05 月 06 日阅读数: 54
用户头像

Jason

关注

还未添加个人签名 2019.07.23 加入

还未添加个人简介

评论

发布
暂无评论
当我们谈到ThreadLocal的时候,我们在谈什么?