ThreadLocal
认识和深入
ThreadLocal
类介绍
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal
是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
ThreadLocal
是 JDK
包提供的,它提供线程本地变量,如果创建了ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示
此类提供线程局部变量。这些变量与它们的普通对应变量不同,因为每个访问一个的线程(通过其 {@code get} 或 {@code set} 方法)都有自己的、独立初始化的变量副本。 {@code
ThreadLocal
} 实例通常是希望将状态与线程相关联的类中的私有静态字段(例如,用户 ID 或事务 ID)。只要线程处于活动状态并且 {@code
ThreadLocal
} 实例是可访问的,每个线程都持有对其线程局部变量副本的隐式引用;线程消失后,它的所有线程本地实例副本都将进行垃圾回收(除非存在对这些副本的其他引用)
ThreadLocal
简单使用
下面的例子中,开启两个线程,在每个线程内部设置了本地变量的值,然后调用 print 方法打印当前本地变量的值。如果在打印之后调用本地变量的 remove 方法会删除本地内存中的变量,代码如下所示
package com.kelecc.demo.thead;
/**
* 功能描述: 线程测试类
*
* @Author keLe
* @Date 2021/11/30
*/
public class TheadLocalTest {
public static ThreadLocal<String> local = new ThreadLocal<String>();
public static void print(String str){
//打印当前线程中本地内存中本地变量的值
System.out.println(str+":"+local.get());
//清除本地内存中的本地变量
local.remove();
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
public void run() {
local.set("localVal1");
print("thread1");
//打印本地变量
System.out.println("after remove : " + local.get());
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
//设置线程1中本地变量的值
local.set("localVar2");
//调用打印方法
print("thread2");
//打印本地变量
System.out.println("after remove : " + local.get());
}
});
t1.start();
t2.start();
}
}
ThreadLocal
的实现原理
其中关键就在 get 和 set 方法 (方法在 TheadLocal.class
199行)
set方法
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取存储数据结构类型
ThreadLocalMap map = getMap(t);
//如果map不存在,就创建,否则就set
if (map != null){
map.set(this, value);
}else{
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
//thread中维护了一个ThreadLocalMap
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
//实例化一个新的Map,并赋值给线程的成员变量threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
每个线程持有一个 ThreadLocalMap
对象。每一个新的线程 Thread 都会实例化一个 ThreadLocalMap
并赋值给成员变量 threadLocals
,使用时若已经存在 threadLocals
则直接使用已经存在的对象。(声明变量在 Thead.class
182行)
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
接下来看createMap
方法中的实例化过程( ThreadLocalMap.class
为 TheadLocal
静态内部类,298行)
static class ThreadLocalMap {
/**
* 这个哈希映射中的条目扩展了 WeakReference,
* 使用它的主要 ref 字段作为键(它总是一个 ThreadLocal 对象)。
* 请注意,空键(即 entry.get() == null)意味着不再引用该键,
* 因此可以从表中删除该条目。 此类条目在以下代码中称为“陈旧条目”
*
* 简单来说:
* Entry为ThreadLocalMap静态内部类,对ThreadLocal的弱引用
* 同时让ThreadLocal和储值形成key-value的关系
*
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**初始容量 -- 必须是 2 的幂。*/
private static final int INITIAL_CAPACITY = 16;
/**表格,根据需要调整大小。 table.length 必须始终是 2 的幂*/
private Entry[] table;
/**省略一些变量*/
.......
/**
* 要调整大小的下一个大小值
*/
private int threshold; // Default to 0
/**
* 设置调整大小阈值以在最坏情况下保持 2/3 的负载因子。
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* 构造方法一个最初包含 (firstKey, firstValue) 的新映射。
* ThreadLocalMaps 是惰性构造的,所以我们只有在至少有一个条目可以放入时才创建一个
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//内部成员数组,INITIAL_CAPACITY值为16的常量
table = new Entry[INITIAL_CAPACITY];
//位运算,结果与取模相同,计算出需要存放的位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
在实例化 ThreadLocalMap
时创建了一个长度为 16 的 Entry 数组。
通过 hashCode
与length
位运算确定出一个索引值 i,这个 i 就是被存储在 table 数组中的位置。
每个线程 Thread 持有一个 ThreadLocalMap
类型的实例 threadLocals
,结合此处的构造方法可以理解成每个线程 Thread 都持有一个 Entry 型的数组 table,而一切的读取过程都是通过操作这个数组 table
完成的。
ThreadLocalMap
中的 Entry 的 key 使用的是 ThreadLocal
对象的弱引用,在没有其他地方对 ThreadLocal
依赖,ThreadLocalMap
中的 ThreadLocal
对象就会被回收掉,但是对应的不会被回收,这个时候 Map 中就可能存在 key 为 null 但是 value 不为 null 的项,这需要实际的时候使用完毕及时调用 remove
方法避免内存泄漏。
每个线程 Thread
持有一个 ThreadLocalMap
类型的实例 threadLocals
,结合此处的构造方法可以理解成每个线程 Thread
都持有一个 Entry
型的数组 table
,而一切的读取过程都是通过操作这个数组table
完成的
那么 table 是 set 和 get 的方法就是我们重点看的,我们先看下如下代码:
//在某一线程声明了ABC三种类型的ThreadLocal
ThreadLocal<A> t1 = new ThreadLocal<A>();
ThreadLocal<B> t2 = new ThreadLocal<B>();
ThreadLocal<C> t3 = new ThreadLocal<C>();
由前面我们知道对于一个 Thread
来说只有持有一个 ThreadLocalMap
,所以 ABC 对应同一个 ThreadLocalMap
对象。为了管理 ABC,于是将他们存储在一个数组的不同位置,而这个数组就是上面提到的 Entry
型的数组 table
。
那么问题来了,ABC 在 table
中的位置是如何确定的?为了能正常够正常的访问对应的值,肯定存在一种方法计算出确定的索引值i
(代码 454行)
/**
* 设置key值
*
* @param 线程本地对象
* @param 要设置的值
*/
private void set(ThreadLocal<?> key, Object value) {
// 我们不像 get() 那样使用快速路径,因为使用 set() 创建新条目至少与替换现有条目一样常见,在这种情况下,快速路径会经常失败.
Entry[] tab = table;
int len = tab.length;
//获取索引值,这个地方是比较特别的地方
int i = key.threadLocalHashCode & (len-1);
//遍历tab如果已经存在则更新值
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;
//满足条件数组扩容x2
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//刷新table,
rehash();
}
看到 int i = key.threadLocalHashCode & (len-1);
此处源码 85行开始;
简而言之就是将 threadLocalHashCode
进行一个位运算(取模)得到索引 i,threadLocalHashCode
代码如下。
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;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
因为 static
的原因,在每次 new ThreadLocal
时因为 threadLocalHashCode
的初始化,会使 threadLocalHashCode
值自增一次,增量0x61c88647
。
0x61c88647
是斐波那契散列乘数,它的优点是通过它散列 (hash) 出来的结果分布会比较均匀,可以很大程度上避免 hash 冲突,已初始容量 16 为例,hash 并与 15 位运算计算数组下标结果如下:
hashCode | 数组下标 |
---|---|
0x61c88647 | 7 |
0xc3910c8e | 14 |
0x255992d5 | 5 |
0x8722191c | 12 |
0xe8ea9f63 | 3 |
0x4ab325aa | 10 |
0xac7babf1 | 1 |
0xe443238 | 8 |
0x700cb87f | 15 |
总结:
- 对于某一
ThreadLocal
来讲,他的索引值 i 是确定的,在不同线程之间访问时访问的是不同的 table 数组的同一位置即都为 table [i],只不过这个不同线程之间的 table 是独立的。 - 对于同一线程的不同
ThreadLocal
来讲,这些ThreadLocal
实例共享一个 table 数组,然后每个ThreadLocal
实例在 table 中的索引 i 是不同的。 - 每个线程都对应一个
ThreadLocalMap
, 而threadlocal
负责访问和维护ThreadLocalMap.
get方法
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获得一个 ThreadLocalMap
ThreadLocalMap map = getMap(t);
//ThreadLocalMap,不是空就返回table里面的值;
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//创建一个新的ThreadLocalMap;
return setInitialValue();
}
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
//此处set实现
return t.threadLocals;
}
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
//声明一个泛型变量
T value = initialValue();
//获取一个当前线程
Thread t = Thread.currentThread();
//获取一个ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果Map不是空的就设置值,否则创建一个新的TheadLocalMap
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
ThreadLocal
特性
ThreadLocal
和 Synchronized
都是为了解决多线程中相同变量的访问冲突问题,不同的点是
Synchronized
是通过线程等待,牺牲时间来解决访问冲突ThreadLocal
是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized
,ThreadLocal
具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
正因为 ThreadLocal
的线程隔离特性,使他的应用场景相对来说更为特殊一些。在 android 中 Looper、ActivityThread
以及 AMS 中都用到了 ThreadLocal
。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用 ThreadLocal
。
ThreadLocal
不支持继承性
同一个 ThreadLocal
变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals
中为当前调用线程对应的本地变量,所以二者自然是不能共享的)
package com.kelecc.mybatis.thead;
/**
* 功能描述: ThreadLocal 继承性
*
* @Author keLe
* @Date 2021/12/1
*/
public class ThreadLocalTest2 {
//创建线程本地变量
public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public static void main(String[] args) {
//在main线程中添加main线程的本地变量
threadLocal.set("mainVal");
//新创建一个子线程
Thread thread = new Thread(new Runnable() {
public void run() {
System.out.println("子线程中的本地变量值:"+threadLocal.get());
}
});
thread.start();
//输出main线程中的本地变量值
System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
}
}
ThreadLocal
使用不当的内存泄漏问题
ThreadLocal
只是一个工具类,他为用户提供 get、set、remove 接口操作实际存放本地变量的 threadLocals
(调用线程的成员变量),也知道 threadLocals
是一个 ThreadLocalMap
类型的变量。
此处说下Java
中的四种引用类型;
-
强引用:Java 中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被
GC
; -
软引用:简言之,如果一个对象具有弱引用,在
JVM
发生OOM
之前(即内存充足够使用),是不会GC
这个对象的;只有到JVM
内存不足的时候才会GC
掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中; -
弱引用(这里讨论
ThreadLocalMap
中的 Entry 类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC
掉 ( 被弱引用所引用的对象只能生存到下一次GC
之前,当发生GC
时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉 )。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM
会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的 get 方法得到,当引用的对象被回收掉之后,再调用 get 方法就会返回 null; -
虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被
GC
掉之后收到一个通知。(不能通过 get 方法获得其指向的对象)
看此处代码
/**
* 是继承自WeakReference的一个类,该类中实际存放的key是
* 指向ThreadLocal的弱引用和与之对应的value值(该value值
* 就是通过ThreadLocal的set方法传递过来的值)
* 由于是弱引用,当get方法返回null的时候意味着能引用
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** value就是和ThreadLocal绑定的 */
Object value;
//k:ThreadLocal的引用,被传递给WeakReference的构造方法
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
public class WeakReference<T> extends Reference<T> {
/**
* Creates a new weak reference that refers to the given object. The new
* reference is not registered with any queue.
*
* @param referent object the new weak reference will refer to
*/
public WeakReference(T referent) {
//调用父类构造方法
super(referent);
}
/**
* Creates a new weak reference that refers to the given object and is
* registered with the given queue.
*
* @param referent object the new weak reference will refer to
* @param q the queue with which the reference is to be registered,
* or <tt>null</tt> if registration is not required
*/
public WeakReference(T referent, ReferenceQueue<? super T> q) {
//调用父类构造方法
super(referent, q);
}
}
public abstract class Reference<T> {
//Reference构造方法
Reference(T referent) {
this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
}
总结:
- 当前
ThreadLocal
的引用 k 被传递给WeakReference
的构造函数,所以ThreadLocalMap
中的 key 为ThreadLocal
的弱引用。 - 当一个线程调用
ThreadLocal
的 set 方法设置变量的时候,当前线程的ThreadLocalMap
就会存放一个记录,这个记录的key
值为ThreadLocal
的弱引用,value 就是通过 set 设置的值。 - 如果当前线程一直存在且没有调用该
ThreadLocal
的 remove 方法,如果这个时候别的地方还有对ThreadLocal
的引用,那么当前线程中的ThreadLocalMap
中会存在对ThreadLocal
变量的引用和 value 对象的引用,是不会释放的,就会造成内存泄漏。 - 考虑这个
ThreadLocal
变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap
里面的 key 是弱引用,所以当前线程的ThreadLocalMap
里面的ThreadLocal
变量的弱引用在gc
的时候就被回收,但是对应的 value 还是存在的这就可能造成内存泄漏 (因为这个时候ThreadLocalMap
会存在 key 为 null 但是 value 不为 null 的 entry 项)。 THreadLocalMap
中的 Entry 的 key 使用的是ThreadLocal
对象的弱引用,在没有其他地方对ThreadLocal
依赖,ThreadLocalMap
中的ThreadLocal
对象就会被回收掉,但是对应的不会被回收,这个时候 Map 中就可能存在 key 为 null 但是 value 不为 null 的项,这需要实际的时候使用完毕及时调用 remove 方法避免内存泄漏。