一、简介
ThreadLocal 不知道大家有没有用过,但至少听说过,这篇文章主要讲解下ThreadLocal的源码以及应用场景;
来看下ThreadLocal的类描述:
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*
* <p>For example, the class below generates unique identifiers local to each
* thread.
* A thread's id is assigned the first time it invokes {@code ThreadId.get()}
* and remains unchanged on subsequent calls.
* <pre>
* import java.util.concurrent.atomic.AtomicInteger;
*
* public class ThreadId {
* // Atomic integer containing the next thread ID to be assigned
* private static final AtomicInteger nextId = new AtomicInteger(0);
*
* // Thread local variable containing each thread's ID
* private static final ThreadLocal<Integer> threadId =
* new ThreadLocal<Integer>() {
* @Override protected Integer initialValue() {
* return nextId.getAndIncrement();
* }
* };
*
* // Returns the current thread's unique ID, assigning it if necessary
* public static int get() {
* return threadId.get();
* }
* }
* </pre>
* <p>Each thread holds an implicit reference to its copy of a thread-local
* variable as long as the thread is alive and the {@code ThreadLocal}
* instance is accessible; after a thread goes away, all of its copies of
* thread-local instances are subject to garbage collection (unless other
* references to these copies exist).
*
* @author Josh Bloch and Doug Lea
* @since 1.2
*/
从中可以看出:
ThreadLocal 提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同,而同一个线程在任何时候访问这个本地变量的结果都是一致的。当此线程结束生命周期时,所有的线程本地实例都会被 GC。ThreadLocal 相当于提供了一种线程隔离,将变量与线程相绑定。ThreadLocal 通常定义为 private static 类型。
注意重点,它的作用是提供局部变量给线程内部使用。也就是说,它使用了一套机制保证:你new了一个变量threadLocal,在一个线程里,给threadLocal变量set一个别的线程无法访问使用的类型A的实例a,然后一段时间后,你可以从threadLocal变量中get出实例a,重点是这个threadLocal变量是可以跨线程的,在多个线程里做同样的事(set一个a1,a2…)否则跟在线程里直接new一个对象a就没有区别了。同时,JDK建议你把这个threadLocal变量设置为static,因为他需要在多线程中保持全局唯一,他也有能力在全局唯一的情况下,在多线程中提供局部变量。
ThreadLocal使用示例
/**
* <Description> <br>
*
* @author Sunny<br>
* @version 1.0<br>
* @taskId: <br>
* @createDate 2018/12/28 3:42 <br>
* @see com.sunny.concurrent.thread.threadlocal <br>
*/
public class SequenceNumber {
// 使用lambda表达式定义ThreadLocal
private static ThreadLocal<Integer> seqNum = ThreadLocal.withInitial(() -> 0);
// 下一个序列号
public int getNextNum() {
seqNum.set(seqNum.get() + 1);
return seqNum.get();
}
private static class TestClient extends Thread {
private SequenceNumber sn;
public TestClient(SequenceNumber sn) {
this.sn = sn;
}
// 线程产生序列号
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("thread[" + Thread.currentThread().getName() + "] sn[" + sn.getNextNum() + "]");
}
}
}
/**
* @param args
*/
public static void main(String[] args) {
SequenceNumber sn = new SequenceNumber();
// 三个线程产生各自的序列号
TestClient t1 = new TestClient(sn);
TestClient t2 = new TestClient(sn);
TestClient t3 = new TestClient(sn);
t1.start();
t2.start();
t3.start();
}
}
输出如下:
thread[Thread-2] sn[1]
thread[Thread-1] sn[1]
thread[Thread-0] sn[1]
thread[Thread-1] sn[2]
thread[Thread-2] sn[2]
thread[Thread-1] sn[3]
thread[Thread-2] sn[3]
thread[Thread-0] sn[2]
thread[Thread-0] sn[3]
从运行结果可以看出,ThreadLocal确实是可以达到线程隔离机制,确保变量的安全性。接下来看下ThreadLocal的类继承关系:
其中,InheritableThreadLocal类是ThreadLocal类的子类。ThreadLocal中每个线程拥有它自己的值,与ThreadLocal不同的是,InheritableThreadLocal允许一个线程以及该线程创建的所有子线程都可以访问它保存的值。
二、ThreadLocal实现原理
看下Thread类的中定义的threadlocals属性:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
这样就知道每个线程维护了一个ThreadLocalMap的映射表,映射表的key是ThreadLocal示例本身,value是要存储的副本变量。ThreadLocal实例本身并不存储值,它只是提供一个在当前线程中找到副本值的key。如下图所示:
ThreadLocalMap的原理
之前在文章《HashMap深入剖析(JDK8)》中讲述了hash冲突的解决办法有两种:链表法和开放定址法,而且HashMap和ConcurrentHashMap均采用了链表法。而ThreadLocalMap采用了开放定址法。这个其实又印证了那一句话:每一个技术没有绝对的好和不好之分,只有在适用的场景选择最合适的技术。
相对于链表法,开放定址法不会创建链表,当key散列到的数组单元已经被另外一个关键字占用的时候,就会尝试在数组中寻找其他的单元,直到找到一个空的单元。探测数组空单元的方式有很多,ThreadLocalMap主要使用了线性探测法。线性探测法就是从冲突的数组单元开始,依次往后搜索空单元,如果到数组尾部,再从头开始搜索(环形查找)。
具体的链表法和开放定址法的详细对比见《散列冲突处理:开放定址法》等系列文章。
三、ThreadLocal源码剖析
3.1 ThreadLocal源码剖析
ThreadLocal的成员变量
由于ThreadLocalMap中存储对象的key是ThreadLocal对象,所以我们需要考虑ThreadLocal对象在ThreadLocalMap中散列的问题;这里我们先来看下ThreadLocal对象散列相关的属性:
/**
* 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;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
ThreadLocal 通过 threadLocalHashCode 来标识每一个 ThreadLocal 的唯一性;它是一个常量,通过nextHashCode()
函数产生。nextHashCode()
函数其实就是在一个AtomicInteger变量(初始值为0)的基础上每次累加0x61c88647
,使用AtomicInteger为了保证每次的加法是原子操作。而0x61c88647
这个就比较神奇了,它可以使hashcode均匀的分布在大小为2的N次方的数组里。下面写个程序验证下:
/**
* <Description> 检验ThreadLocal中0x61c88647数字的散列能力<br>
*
* @author Sunny<br>
* @version 1.0<br>
* @taskId: <br>
* @createDate 2019/01/02 15:49 <br>
* @see com.sunny.concurrent.thread.threadlocal <br>
*/
public class ThreadLocalHashCodeTest {
public static void main(String[] args) {
AtomicInteger hashCode = new AtomicInteger();
int hash_increment = 0x61c88647;
int size = 32;
List<Integer> list = new ArrayList<>();
for (int i = 0; i < size; i++) {
list.add(hashCode.getAndAdd(hash_increment) & (size - 1));
}
System.out.println("Original: " + list);
Collections.sort(list);
System.out.println("Sorted: " + list);
}
}
将size分别设置为16, 32, 64分别输出如下:
// size=16
original:[0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9]
sort: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
// size=32
original:[0, 7, 14, 21, 28, 3, 10, 17, 24, 31, 6, 13, 20, 27, 2, 9, 16, 23, 30, 5, 12, 19, 26, 1, 8, 15, 22, 29, 4, 11, 18, 25]
sort: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
// size=64
original:[0, 7, 14, 21, 28, 35, 42, 49, 56, 63, 6, 13, 20, 27, 34, 41, 48, 55, 62, 5, 12, 19, 26, 33, 40, 47, 54, 61, 4, 11, 18, 25, 32, 39, 46, 53, 60, 3, 10, 17, 24, 31, 38, 45, 52, 59, 2, 9, 16, 23, 30, 37, 44, 51, 58, 1, 8, 15, 22, 29, 36, 43, 50, 57]
sort: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
可以看到不管size怎么变化,只要保证是2的N次方,hashcode 总能均匀的分布。其实这就是 Fibonacci Hashing,具体可以参考 知乎。所以虽然 ThreadLocal 的 hashcode 是固定的,当 ThreadLocalMap 中的散列表调整大小(变为原来的 2 倍)之后重新散列,hashcode 仍能均匀的分布在散列表中。
那么原理是什么?
long l1 = (long) ((1L << 31) * (Math.sqrt(5) - 1));//(根号5-1)*2的31次方=(根号5-1)/2 *2的32次方=黄金分割数*2的32次方
System.out.println("as 32 bit unsigned: " + l1);//32位无符号整数
int i1 = (int) l1;
System.out.println("as 32 bit signed: " + i1);//32位有符号整数
System.out.println("MAGIC = " + 0x61c88647);
运行结果:
as 32 bit unsigned: 2654435769
as 32 bit signed: -1640531527
MAGIC = 1640531527
这里不再拓展,跟斐波那契数列(和黄金分割数)有关:
1.0x61c88647对应十进制=1640531527。
2.(根号5-1)*2的31次方,转换成long类型就是2654435769,转换成int类型就是-1640531527。
ThreadLocal的set方法
先看下源码:
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
// 根据当前线程的对象获取其内部Map
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal在调用set方法时,如果 getMap(注意是以Thread引用为key) 返回的 t.threadLocals 为null,那么表示该线程的 ThreadLocalMap 还没有初始化,所以调用createMap进行初始化:t.threadLocals = new ThreadLocalMap(this, firstValue);这里使用了延迟初始化;
线程隔离的秘密,就在于ThreadLocalMap这个类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。
ThreadLocal的setInitialValue方法
/**
* 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() {
//获取初始化值,默认为null(如果没有子类进行覆盖)
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//不为空不用再初始化,直接调用set操作设值
if (map != null)
map.set(this, value);
else
//第一次初始化,createMap在上面介绍set()的时候有介绍过。
createMap(t, value);
return value;
}
setInitialValue方法的实现基本同set,只是将value值设为默认值null。
ThreadLocal的get方法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的成员变量 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从当前线程的ThreadLocalMap获取相对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 获取目标值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//为空返回初始化值
return setInitialValue();
}
跟set方法的类似,获取当前线程t;返回当前线程t的成员变量ThreadLocalMap(以下简写map);map不为null,则获取以当前线程为key的ThreadLocalMap的Entry(以下简写e),如果e不为null,则直接返回该Entry的value;如果map为null或者e为null,返回setInitialValue()的值。setInitialValue()调用重写的initialValue()返回新值(如果没有重写initialValue将返回默认值null),并将新值存入当前线程的ThreadLocalMap(如果当前线程没有ThreadLocalMap,会先创建一个)。
ThreadLocal的remove方法
获取当前线程的ThreadLocalMap,map不为空,则移除当前ThreadLocal作为key的键值对。
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
注意:remove()移除当前线程的当前ThreadLocal数据(只是清空该key-value键值对),而且是立即移除,移除后,再调用get方法将重新调用initialValue方法初始化(除非在此期间调用了set方法赋值)。
ThreadLocal的withInitial方法
JDK8新增,支持Lambda表达式,和ThreadLocal重写的initialValue()效果一样。
/**
* Creates a thread local variable. The initial value of the variable is
* determined by invoking the {@code get} method on the {@code Supplier}.
*
* @param <S> the type of the thread local's value
* @param supplier the supplier to be used to determine the initial value
* @return a new thread local variable
* @throws NullPointerException if the specified supplier is null
* @since 1.8
*/
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
可以看出,withInitial()方法的入参是函数式接口Supplier,返回值是JDK8新增的内部类SuppliedThreadLocal,区别仅在于支持Lambda表达式赋值而已。使用事例如下:
@Test
public void jdk8Test(){
Supplier<String> supplier =new Supplier<String>(){
@Override
public String get(){
return"supplier_new";
}
};
threadLocal= ThreadLocal.withInitial(supplier);
System.out.println(threadLocal.get());// supplier_new
threadLocal= ThreadLocal.withInitial(()->"sup_new_2");
System.out.println(threadLocal.get());// sup_new_2
ThreadLocal<DateFormat> localDate = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd"));
System.out.println(localDate.get().format(new Date()));// 2017-01-22
ThreadLocal<String> local =new ThreadLocal<>().withInitial(supplier);
System.out.println(local.get());// supplier_new
ThreadLocal<Integer> seqNum = ThreadLocal.withInitial(() -> 0);
}
其中,withInitial(supplier)是有返回值ThreadLocal的,So实例化时需将其赋值给ThreadLocal实例
3.2 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.
* 数组里面entrys的个数,可以用于判断table当前使用量是否超过负因子。
*/
private int size = 0;
/**
* The next size value at which to resize.
* 进行扩容的阈值,表使用量大于它的时候进行扩容。
*/
private int threshold; // Default to 0
跟HashMap类似,INITIAL_CAPACITY代表这个Map的初始容量;table 是一个Entry 类型的数组,用于存储数据;size 代表表中的存储数目; threshold 代表需要扩容时对应 size 的阈值。
内部类Entry
与普通的Map一样,ThreadLocalMap也有自己的存储结构:
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
* Entry继承WeakReference,并且用ThreadLocal作为key.如果key为null
* (entry.get() == null)表示key不再被引用,表示ThreadLocal对象被回收
* 因此这时候entry也可以从table从清除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry继承WeakReference,使用弱引用,可以将ThreadLocal对象的生命周期和线程生命周期解绑;
为什么使用弱引用?
下面我们分两种情况讨论:
key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
如果使用了弱引用是否不会存在内存溢出的场景呢?
- 首先来说,如果把ThreadLocal置为null,那么意味着Heap中的ThreadLocal实例不在有强引用指向,只有弱引用存在,因此GC是可以回收这部分空间的,也就是key是可以回收的。但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread销毁时,value才能得到释放。
- 因此,只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间内不会被回收的,就发生了我们认为的内存泄露。最要命的是线程对象不被回收的情况,比如使用线程池的时候,线程结束是不会销毁的,再次使用的,就可能出现内存泄露。
那么如何有效的避免呢?
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。我们也可以通过调用ThreadLocal的remove方法进行释放!
ThreadLocalMap的构造函数
ThreadLocalMap类有两个构造函数,其中常用的是ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue):
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化table
table = new Entry[INITIAL_CAPACITY];
//计算索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
//设置阈值
setThreshold(INITIAL_CAPACITY);
}
构造函数的第一个参数就是本ThreadLocal实例(this),第二个参数就是要保存的线程本地变量。构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的哈希值,然后存储到table中,并设置size和threshold。
注意一个细节,计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现(和HashMap中的思路相同)。正是因为这种算法,我们要求size必须是2的指数,因为这可以使得hash发生冲突的次数减小。
ThreadLocalMap的set方法
ThreadLocalMap使用 线性探测法
来解决哈希冲突,线性探测法的地址增量di = 1, 2, … , m-1,其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。
按照上面的描述, 可以把table看成一个环形数组
。
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be 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;
// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
int i = key.threadLocalHashCode & (len-1);
// 使用线性探测法查找元素
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// ThreadLocal 对应的 key 存在,直接覆盖之前的值
if (k == key) {
e.value = value;
return;
}
// key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,
// 当前数组中的 Entry 是一个陈旧(stale)的元素
if (k == null) {
// 用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal 对应的 key 不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的 Entry。
tab[i] = new Entry(key, value);
int sz = ++size;
/**
* cleanSomeSlots用于清除那些e.get()==null,也就是table[index] != null && table[index].get()==null
* 之前提到过,这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
* 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行rehash()
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
- 首先还是根据key计算出位置i,然后查找i位置上的Entry,
- 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值。
- 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry
- 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。
- 最后调用cleanSomeSlots,这个函数就不细说了,你只要知道内部还是调用了上面提到的expungeStaleEntry函数清理key为null的Entry就行了,最后返回是否清理了Entry,接下来再判断sz>thresgold,这里就是判断是否达到了rehash的条件,达到的话就会调用rehash函数。
&emsp;如果冲突了,就会通过nextIndex方法再次计算哈希值:
/**
* Increment i modulo len.
* 获取数组的下一个索引
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
到这里,我们看到 ThreadLocalMap 解决冲突的方法是 线性探测法(不断加 1),而不是 HashMap 的 链地址法,这一点也能从 Entry 的结构上推断出来。
如果entry里对应的key为null的话,表明此entry为staled entry,就将其替换为当前的key和value:
/**
* 替换无效entry
* Replace a stale entry encountered during a set operation
* with an entry for the specified key. The value passed in
* the value parameter is stored in the entry, whether or not
* an entry already exists for the specified key.
*
* As a side effect, this method expunges all stale entries in the
* "run" containing the stale entry. (A run is a sequence of entries
* between two null slots.)
*
* @param key the key
* @param value the value to be associated with key
* @param staleSlot index of the first stale entry encountered while
* searching for key.
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
/**
* 根据传入的无效entry的位置(staleSlot),向前扫描
* 一段连续的entry(这里的连续是指一段相邻的entry并且table[i] != null),
* 直到找到一个无效entry,或者扫描完也没找到
*/
int slotToExpunge = staleSlot; //之后用于清理的起点
/**
* 向后扫描一段连续的entry
*/
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
/**
* 向后扫描一段连续的entry
*/
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
/**
* 如果找到了key,将其与传入的无效entry替换,也就是与table[staleSlot]进行替换
*/
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
//如果向前查找没有找到无效entry,则更新slotToExpunge为当前值i
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.
/**
* 如果向前查找没有找到无效entry,并且当前向后扫描的entry无效,则更新slotToExpunge为当前值i
*/
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
/**
* 如果没有找到key,也就是说key之前不存在table中
* 就直接最开始的无效entry——tab[staleSlot]上直接新增即可
*/
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
/**
* slotToExpunge != staleSlot,说明存在其他的无效entry需要进行清理。
*/
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
首先我们回想上一步是因为这个位置的Entry的key为null才调用replaceStaleEntry。
- 第1个for循环:我们向前找到key为null的位置,记录为slotToExpunge,这里是为了后面的清理过程,可以不关注了;
- 第2个for循环:我们从staleSlot起到下一个null为止,若是找到key和传入key相等的Entry,就给这个Entry赋新的value值,并且把它和staleSlot位置的Entry交换,然后调用CleanSomeSlots清理key为null的Entry。
- 若是一直没有key和传入key相等的Entry,那么就在staleSlot处新建一个Entry。函数最后再清理一遍空key的Entry。
这替换过程里面也进行了不少的垃圾清理动作以防止引用关系存在而导致的内存泄露。
若是经历了上面步骤没有命中hash,也没有发现无用的Entry,set方法就会创建一个新的Entry,并会进行启发式的垃圾清理,用于清理无用的Entry。主要通过cleanSomeSlots方法进行清理(清理的时机通常为添加新元素或另一个无用的元素被回收时。参见注释):
/**
* Heuristically scan some cells looking for stale entries.
* This is invoked when either a new element is added, or
* another stale one has been expunged. It performs a
* logarithmic number of scans, as a balance between no
* scanning (fast but retains garbage) and a number of scans
* proportional to number of elements, that would find all
* garbage but would cause some insertions to take O(n) time.
*
* @param i a position known NOT to hold a stale entry. The
* scan starts at the element after i.
*
* @param n scan control: {@code log2(n)} cells are scanned,
* unless a stale entry is found, in which case
* {@code log2(table.length)-1} additional cells are scanned.
* When called from insertions, this parameter is the number
* of elements, but when from replaceStaleEntry, it is the
* table length. (Note: all this could be changed to be either
* more or less aggressive by weighting n instead of just
* using straight log n. But this version is simple, fast, and
* seems to work well.)
*
* @return true if any stale entries have been removed.
*/
/**
* 启发式的扫描清除,扫描次数由传入的参数n决定
*
* @param i 从i向后开始扫描(不包括i,因为索引为i的Slot肯定为null)
*
* @param n 控制扫描次数,正常情况下为 log2(n) ,
* 如果找到了无效entry,会将n重置为table的长度len,进行段清除。
*
* map.set()点用的时候传入的是元素个数,replaceStaleEntry()调用的时候传入的是table的长度len
*
* @return true if any stale entries have been removed.
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
//重置n为len
n = len;
removed = true;
//依然调用expungeStaleEntry来进行无效entry的清除
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0); //无符号的右移动,可以用于控制扫描次数在log2(n)
return removed;
}
一旦发现一个位置对应的 Entry 所持有的 ThreadLocal 弱引用为null,就会把此位置当做 staleSlot 并调用 expungeStaleEntry 方法进行整理 (rehashing) 的操作:
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
* 连续段清除
* 根据传入的staleSlot,清理对应的无效entry——table[staleSlot],
* 并且根据当前传入的staleSlot,向后扫描一段连续的entry(这里的连续是指一段相邻的entry并且table[i] != null),
* 对可能存在hash冲突的entry进行rehash,并且清理遇到的无效entry.
*
* @param staleSlot index of slot known to have null key key为null,需要无效entry所在的table中的索引
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging). 返回下一个为空的solt的索引。
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
// 做清除工作。
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
//将后面的Entry重新Hash
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果遇到key为null,表示无效entry,进行清理.
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//如果key不为null,计算索引
int h = k.threadLocalHashCode & (len - 1);
/**
* 计算出来的索引——h,与其现在所在位置的索引——i不一致,置空当前的table[i]
* 从h开始向后线性探测到第一个空的slot,把当前的entry挪过去。
*/
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
//下一个为空的solt的索引。
return i;
}
只要没有清理任何的 stale entries 并且 size 达到阈值的时候(即 table 已满,所有元素都可用),都会触发rehash。
为什么要做 rehash 呢?
因为我们在清理的过程中会把某个值设为null,那么这个值后面的区域如果之前是连着前面的,那么下次循环查找时,就会只查到null为止。
举个例子就是:...,<key1(hash1), value1>, <key2(hash1), value2>,...(即key1和key2的hash值相同)
此时,若插入<key3(hash2), value3>,其hash计算的目标位置被<key2(hash1), value2>占了,于是往后寻找可用位置,hash表可能变为:
..., <key1(hash1), value1>, <key2(hash1), value2>, <key3(hash2), value3>, ...
此时,若<key2(hash1), value2>被清理,显然<key3(hash2), value3>应该往前移(即通过rehash调整位置),否则若以key3查找hash表,将会找不到key3
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
// 清理一次陈旧数据
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
/**
* threshold = 2/3 * len
* 所以threshold - threshold / 4 = 1en/2
* 这里主要是因为上面做了一次全清理所以size减小,需要进行判断。
* 判断的时候把阈值调低了。
*/
if (size >= threshold - threshold / 4)
resize();
}
/**
* Expunge all stale entries in the table.
* 全清理,清理所有无效entry
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
//使用连续段清理
expungeStaleEntry(j);
}
}
rehash 操作会执行一次全表的扫描清理工作,并在 size 大于等于 threshold 的四分之三时进行 resize。但注意在 setThreshold 的时候又取了三分之二:
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
因此 ThreadLocalMap 的实际 load factor 为 3/4 * 2/3 = 0.5。
清理完空key的Entry后,如果size大于3/4的threshold,则调用resize函数:
/**
* Double the capacity of the table.
* 扩容,扩大为原来的2倍(这样保证了长度为2的冥)
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
//虽然做过一次清理,但在扩容的时候可能会又存在key==null的情况。
if (k == null) {
//这里试试将e.value设置为null
e.value = null; // Help the GC
} else {
//同样适用线性探测来设置值。
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
//设置新的阈值
setThreshold(newLen);
size = count;
table = newTab;
}
由源码我们可知每次扩容大小扩展为原来的2倍,然后再一个for循环里,清除空key的Entry,同时重新计算key不为空的Entry的hash值,把它们放到正确的位置上,再更新ThreadLocalMap的所有属性。
ThreadLocalMap的getEntry方法
接下来我们分析下getEntry方法:
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
//ThreadLocalMap中的getEntry方法
//首先进行Hash找到entry,如果发现key和我们的不同,那就是发生了两种情况
//第一种是已经被GC清除了,或者产生了冲突,这时候我们调用getEntryAfterMiss方法。
private Entry getEntry(ThreadLocal<?> key) {
//根据key计算索引,获取entry
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);
}
- 首先是计算索引位置i,通过计算key的hash%(table.length-1)得出;
- 根据获取Entry,如果Entry存在且Entry的key恰巧等于ThreadLocal,那么直接返回Entry对象;
- 否则,也就是在此位置上找不到对应的Entry,那么就调用getEntryAfterMiss。
ThreadLocalMap的getEntryAfterMiss方法
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
* 通过直接计算出来的key找不到对于的value的时候适用这个方法.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
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;
//如果是被GC收集掉了,那么调用expungeStaleEntry方法把后面的重新Hash
if (k == null)
expungeStaleEntry(i);
else //基于线性探测法向后扫描
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
这个方法我们还得结合上一步看,上一步是因为不满足e != null && e.get() == key才沦落到调用getEntryAfterMiss的,所以首先e如果为null的话,那么getEntryAfterMiss还是直接返回null的,如果是不满足e.get() == key,那么进入while循环,这里是不断循环,如果e一直不为空,那么就调用nextIndex,不断递增i,在此过程中一直会做两个判断:
- 如果k==key,那么代表找到了这个所需要的Entry,直接返回;
- 如果k==null,那么证明这个Entry中key已经为null,那么这个Entry就是一个过期对象,这里调用expungeStaleEntry清理该Entry。
ThreadLocalMap的remove方法
它用于在map中移除一个不用的Entry。也是先计算出hash值,若是第一次没有命中,就循环直到null,在此过程中也会调用expungeStaleEntry清除空key节点。
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
//计算索引
int i = key.threadLocalHashCode & (len-1);
//进行线性探测,查找正确的key
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//调用weakrefrence的clear()清除引用
e.clear();
//连续段清除
expungeStaleEntry(i);
return;
}
}
}
四、ThreadLocal应用
ThreadLocal在dubbo中的应用,主要是请求缓存的场景:
/**
* CacheFilter is a core component of dubbo.Enabling <b>cache</b> key of service,method,consumer or provider dubbo will cache method return value.
* Along with cache key we need to configure cache type. Dubbo default implemented cache types are
* <li>lur</li>
* <li>threadlocal</li>
* <li>jcache</li>
* <li>expiring</li>
*
* <pre>
* e.g. 1)<dubbo:service cache="lru" />
* 2)<dubbo:service /> <dubbo:method name="method2" cache="threadlocal" /> <dubbo:service/>
* 3)<dubbo:provider cache="expiring" />
* 4)<dubbo:consumer cache="jcache" />
*
*If cache type is defined in method level then method level type will get precedence. According to above provided
*example, if service has two method, method1 and method2, method2 will have cache type as <b>threadlocal</b> where others will
*be backed by <b>lru</b>
*</pre>
*
* @see org.apache.dubbo.rpc.Filter
* @see org.apache.dubbo.cache.support.lru.LruCacheFactory
* @see org.apache.dubbo.cache.support.lru.LruCache
* @see org.apache.dubbo.cache.support.jcache.JCacheFactory
* @see org.apache.dubbo.cache.support.jcache.JCache
* @see org.apache.dubbo.cache.support.threadlocal.ThreadLocalCacheFactory
* @see org.apache.dubbo.cache.support.threadlocal.ThreadLocalCache
* @see org.apache.dubbo.cache.support.expiring.ExpiringCacheFactory
* @see org.apache.dubbo.cache.support.expiring.ExpiringCache
*
*/
@Activate(group = {Constants.CONSUMER, Constants.PROVIDER}, value = Constants.CACHE_KEY)
public class CacheFilter implements Filter {
private CacheFactory cacheFactory;
/**
* Dubbo will populate and set the cache factory instance based on service/method/consumer/provider configured
* cache attribute value. Dubbo will search for the class name implementing configured <b>cache</b> in file org.apache.dubbo.cache.CacheFactory
* under META-INF sub folders.
*
* @param cacheFactory instance of CacheFactory based on <b>cache</b> type
*/
public void setCacheFactory(CacheFactory cacheFactory) {
this.cacheFactory = cacheFactory;
}
/**
* If cache is configured, dubbo will invoke method on each method call. If cache value is returned by cache store
* then it will return otherwise call the remote method and return value. If remote method's return valeu has error
* then it will not cache the value.
* @param invoker service
* @param invocation invocation.
* @return Cache returned value if found by the underlying cache store. If cache miss it will call target method.
* @throws RpcException
*/
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
if (cacheFactory != null && ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.CACHE_KEY))) {
Cache cache = cacheFactory.getCache(invoker.getUrl(), invocation);
if (cache != null) {
String key = StringUtils.toArgumentString(invocation.getArguments());
Object value = cache.get(key);
if (value != null) {
if (value instanceof ValueWrapper) {
return new RpcResult(((ValueWrapper)value).get());
} else {
return new RpcResult(value);
}
}
Result result = invoker.invoke(invocation);
if (!result.hasException()) {
cache.put(key, new ValueWrapper(result.getValue()));
}
return result;
}
}
return invoker.invoke(invocation);
}
/**
* Cache value wrapper.
*/
static class ValueWrapper implements Serializable{
private static final long serialVersionUID = -1777337318019193256L;
private final Object value;
public ValueWrapper(Object value){
this.value = value;
}
public Object get() {
return this.value;
}
}
}
可以看出,在RPC调用(invoke)的链路上,会先使用请求参数判断当前线程是否刚刚发起过同样参数的调用——这个调用会使用ThreadLocalCache保存起来。
/**
* This class store the cache value per thread. If a service,method,consumer or provided is configured with key <b>cache</b>
* with value <b>threadlocal</b>, dubbo initialize the instance of this class using {@link ThreadLocalCacheFactory} to store method's returns value
* to server from store without making method call.
* <pre>
* e.g. <dubbo:service cache="threadlocal" />
* </pre>
* <pre>
* As this ThreadLocalCache stores key-value in memory without any expiry or delete support per thread wise, if number threads and number of key-value are high then jvm should be
* configured with appropriate memory.
* </pre>
*
* @see org.apache.dubbo.cache.support.AbstractCacheFactory
* @see org.apache.dubbo.cache.filter.CacheFilter
* @see Cache
*/
public class ThreadLocalCache implements Cache {
/**
* Thread local variable to store cached data.
*/
private final ThreadLocal<Map<Object, Object>> store;
/**
* Taken URL as an argument to create an instance of ThreadLocalCache. In this version of implementation constructor
* argument is not getting used in the scope of this class.
* @param url
*/
public ThreadLocalCache(URL url) {
this.store = new ThreadLocal<Map<Object, Object>>() {
@Override
protected Map<Object, Object> initialValue() {
return new HashMap<Object, Object>();
}
};
}
/**
* API to store value against a key in the calling thread scope.
* @param key Unique identifier for the object being store.
* @param value Value getting store
*/
@Override
public void put(Object key, Object value) {
store.get().put(key, value);
}
/**
* API to return stored value using a key against the calling thread specific store.
* @param key Unique identifier for cache lookup
* @return Return stored object against key
*/
@Override
public Object get(Object key) {
return store.get().get(key);
}
}
五、总结
本文详细讲述了ThreadLocal的实现原理与源码剖析,但是我们使用中还是要注意一下几点:
1.ThreadLocal不是用来解决线程安全问题的,多线程不共享,不存在竞争!目的是线程本地变量且只能单个线程内维护使用。
2.InheritableThreadLocal对比ThreadLocal唯一不同是子线程会继承父线程变量,并自定义赋值函数。
3.项目如果使用了线程池,那么小心线程回收后ThreadLocal、InheritableThreadLocal变量要remove,否则线程池回收后,变量还在内存中,后果不堪设想!
- 尽量将ThreadLocal设置成private static的,这样ThreadLocal会尽量和线程本身一起消亡。