Java-多线程-ThreadLocal
0x01 摘要
本文简单分析下ThreadLocal实现原理,再附上小例子。
0x02 ThreadLocal是什么
ThreadLocal提供线程级别的私有局部变量。这些变量和普通变量不同之处在于,通过get或set方法访问这类变量的每个线程都拥有一份独立初始化的变量副本。
ThreadLocal通常用private static
修饰,可以将状态与该线程建立一对一的关系。
下面这个小例子,当第一次调用ThreadId.get()
时会为每个线程生成一个全局唯一的、以1为步长自增的标识符,往后都会保持不变。
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();
}
}
在上面的例子中,只要线程存活且该ThreadLocal实例可访问,那么每个线程都会拥有一个隐式引用,指向自己拥有的ThreadLocal变量副本值。
0x03 原理
3.1 ThreadLocal的属性
3.1.1 HashCode
private final int threadLocalHashCode = nextHashCode();
// 两个连续的hashcode差值
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode = new AtomicInteger();
ThreadLocals依赖于附属于每个线程的线性探测哈希映射(Thread.threadLocals和inheritableThreadLocals,都是ThreadLocalMap类型)
ThreadLocal对象充当键,通过threadLocalHashCode搜索。 这是一个自定义哈希代码(仅在ThreadLocalMaps中有用),它消除了在相同线程使用连续构造的ThreadLocals的常见情况下的冲突,同时在不太常见的情况下保持良好行为。
3.2 ThreadLocal构造方法
public ThreadLocal() {}
这里什么都没做。
3.3 成员方法
3.3.1 initialValue
protected T initialValue() {
return null;
}
这个方法会在第一次使用get
方法调用时执行,除非在此之前调用了set
方法就不会调用initialValue
。一般来说该方法只会被调用一次,但如果使用remove
清空随后又调用get
方法时,又会再次调用initialValue
。
如果我们想设一个初值,一般就是用匿名内部类的方式重写该方法实现自定义初值,比如:
ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
} };
3.3.2 get
public T get() {
Thread t = Thread.currentThread();
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;
}
}
return setInitialValue();
}
get
方法很重要,他被用来获取当前thread私有的threadlocal
的变量副本。 如果变量没有当前线程的值,则首先将其初始化为调用initialValue
方法的返回值。
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
这个是刚刚get
方法里使用的,可以看到他会先调用initialValue
方法去拿到初始值,然后获取当前线程的ThreadLocalMap
。如果map不存在就以当前thread
和value
创建ThreadLocalMap;如果已经存在就以当前ThreadLocal
实例为key,value为值放入该ThreadLocalMap。
3.3.3 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);
}
跟前面提到的setInitialValue
方法差不多,只不过这里指定了value。
3.3.4 remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
获取当前线程的ThreadLocalMap,然后从其中移除当前ThreadLocal实例为key的Entry
。
3.4 内部类
3.4.1 SuppliedThreadLocal
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
这个类的主要作用是配合withInitial
设定初始值,示例如下:
private static ThreadLocal<Integer> ti1 = ThreadLocal.withInitial(new Supplier<Integer>() {
@Override
public Integer get()
{
return 33;
}
});
private static ThreadLocal<Integer> ti2 = ThreadLocal.withInitial(() -> 44);
3.4.2 ThreadLocalMap
ThreadLocalMap是一个自定义的hasmap
,他只适合维护threadlocal values
,没有向外暴露任何方法。
为了应对长期和高负荷的使用,所以采用了WeakReference
来修饰该map的key。也就是说当这些key无其他强引用时,GC会将他们回收。注意,
0x05 坑
5.1 内存泄露
因为每个线程都有一个map,指向使用的ThreadLocal
对象,而且这是一个强引用。也就是说,当一个ThreadLocal强引用持续存在时,使用了该ThreadLocal的线程的ThreadLocalMap里的Entry之key虽是弱引用指向该ThreadLocal对象,但是因为还有强引用存在的关系就一直不会被回收,该entry也会随着线程持续存在而存在,造成内存泄露。所以我们应该在每个线程使用完ThreadLocal对象后调用remove
方法,手动移除该entry。
5.2 线程池
Thread对象就那么几个,都是复用的。也就是说,他们的ThreadLocalMap是不会变的,就会导致其他Runnable的ThreadLocal值交叉混用,出现问题。
0x06 例子
6.1 DateFormat
SimpleDateFormat并不是线程安全的,所以在阿里的Java开发规范里推荐了用ThreadLocal保证线程安全的做法:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
} };
String startTimeStr = "2018-01-11 02:17:02.806";
try {
Date startTime = df.get().parse(startTimeStr);
System.out.println("startTime=" + startTime);
String dfStr = df.get().format(new Date());
System.out.println("dfStr=" + dfStr);
}catch (ParseException e) {
e.printStackTrace();
}
0xFF 参考文档
JavaDoc-java.lang.ThreadLocal