概述
并发问题,有时候,可以用ThreadLocal方式来避免。
ThreadLocal,顾名思义,就是线程自己的,独享的,就像线程栈是线程独享的一样。
本文讨论三点:
- 基本用法
- 设计原理
- 父子线程
基础用法
考虑类A有doSync方法,可能会被并发调用. 因为SimpleDateFormat非线程安全,所以在方法内new创建。
public void doSync(){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
simpleDateFormat.format(new Date());
// .... some complex ops
}
复制代码
可以优化为以下方案,让每个线程都有自己的SimpleDateFormat, 从而不用每次调用都new一个:
private ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public void doSync(){
SimpleDateFormat simpleDateFormat = sdf.get();
simpleDateFormat.format(new Date());
// .... some complex ops
}
复制代码
说明:本段代码可能在某个类A中, 方法doSync可能会被并发调用。
如果在doSync内部new一个SimpleDateFormat,同一个线程调用也要每次都new一个,有损性能,其实同一个线程可以共享一个。所以,可以用一个ThreadLocal类型的变量,包含一个SimpleDateFormat。 这里没有调用remove,是希望每个线程里都常驻一个日期格式化对象。
另外的一个栗子是,我们在web开发里,有时候会跨层传播一些上下文信息,会使用ThreadLocal,譬如在某个filter里使用set方法设置,然后结束的时候remove。
ThreadLocal主要方法说明:
- withInitial : 接受一个Supplier(函数接口,定义了get方法,顾名思义,就是提供者),提供什么?当然是提供要放在ThreadLocal内的变量,因为是要在线程内创建,不是马上要,所以需要的是一个supplier
- set: 设置当前线程ThreadLocal包含的值
- get: 获取当前线程ThreadLocal包含的值
- remove:移除当前线程ThreadLocal包含的值
设计原理
为了更加直观的感受ThreadLocal和ThreadLocal所容纳变量的关系,可以继续看下图。 ThreadLocal仅仅是一个访问者,线程独占的变量在各自线程的ThreadLocalMap中。
不过需要注意的是,图中,ThreadLocal对象T1本身的引用,有对象A,线程1,线程2,线程3一共4个持有者。
我们还是用上文日期格式化的代码来说明对应关系:
// 类A的代码片段
private ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public void doSync(){
SimpleDateFormat simpleDateFormat = sdf.get();
simpleDateFormat.format(new Date());
// .... some extremely complex ops
}
复制代码
- 变量X: 就是 ThreadLocal.withInitial 里面 Supplier方法的返回值,一个SimpleDateFormat对象
- ThreadLocal对象T1: 就是类A里定义的成员变量 ThreadLocal sdf
- ThreadLocalMap: 一种Map数据结构,类似HashMap,线程框架里自己实现一个Map,应该是不想和集合框架耦合吧
问题1: 为什么要用一个Map呢?
因为这种A对象可能有很多个,变量X,ThreadLocal对象T1都会有很多个。
问题2: 有人说ThreadLocal有内存泄漏,是什么意思?
首先我们明确一下内存泄漏: 不会再使用的对象或者变量,占用着内存,且无法被GC掉,称为内存泄漏。 ThreadLocal在线程的ThreadLocalMap中,Key是ThreadLocal对象, Value是变量X副本,泄漏的可能是Key和Value。
案例中的日期格式化工具,仅仅在A的代码片段里有用,而当A对象GC-Root不可达要被干掉了,ThreadLocal对象T1的强引用sdf就没有了,而线程1,2,3里的各自ThreadLocalMap中还有。当不规范使用的时候,或者就是倔强,不remove。久而久之,就会有很多无用的Key和Value充斥着ThreadLocalMap。
但是呢,倔强的我回想了一下,其实往往都没事,这么久了,我都没删啊,也没遇到泄漏啊!
作为框架设计者自然会考虑到,为了方便这些上帝使用框架(Java程序员),从2点分别针对Key和value的泄漏:
- 使用ThreadLocal弱引用作为Key,当ThreadLocal变量只有弱引用时,就会被GC掉,ThreadLocalMap里的key就会指向null(或者说Key就是null)
- ThreadLocalMap当rehash的时候,会干掉key为null对应的Value (这或许也是自己实现一个Map的原因吧)
所以,如果没有rehash,泄漏还是存在的,只不过,一般很难达到觉察的程度。
下面,从源码的角度佐证一下针对泄漏所做的2个要点。
弱引用:
// ThreadLocal.ThreadLocalMap.Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // ThreadLocal k 在这里开始被弱引用指向了
value = v;
}
}
复制代码
清理key为null的value:
// ThreadLocal.ThreadLocalMap.rehash
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
// ThreadLocal.ThreadLocalMap.resize
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();
if (k == 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;
}
复制代码
父子线程
有时候,执行业务逻辑需要异步,但是当前线程的ThreadLocal变量,怎么传递给子线程呢?
ThreadLocal有个子类InheritableThreadLocal, 基本使用如下:
static class A {
private InheritableThreadLocal<HashMap<String, String>> map1 = new InheritableThreadLocal<HashMap<String, String>>(){
@Override
protected HashMap<String, String> initialValue() {
return new HashMap<>(8);
}
};
private ThreadLocal<HashMap<String, String>> map2 = new ThreadLocal<HashMap<String, String>>(){
@Override
protected HashMap<String, String> initialValue() {
return new HashMap<>(8);
}
};
public void doAsync(){
map1.get().put("name", "zhangsan");
map2.get().put("name", "zhangsan");
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("map1: " + map1.get().get("name")); // 子线程t可以读取到map1的name
System.out.println("map2: " + map2.get().get("name")); // 却无法读取到map2的name
}
}, "A-SUB-0");
t.start();
}
}
复制代码
怎么传递的呢?
创建线程的时候,Thread类的构造函数会判断当前线程中是否存在InheritableThreadLocal, 如果有,就会拷贝一份。
// Thread类构造函数执行的代码片段: 体现了对inheritableThreadLocal的复制
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
复制代码
仅仅是在创建线程的时候,会发生一次拷贝, 拷贝的是ThreadLocalMap里的Entry数组,即包含Key:ThreadLocal对象和Value对象。
- 后续父线程内有增减ThreadLocal,都和子线程无关。所以和线程池结合使用的时候,需要特别注意一下。
- Key和Value都是引用拷贝,所以,同一个ThreadLocal Key和对应的Value变化,父子线程是共享的
伏笔
写到这里,发现还遗漏了一个知识点,就是ThreadLocalMap这个数据结构怎么实现的。
可以带着这几个问题去看下源码,本文暂时先留下伏笔吧~
- 如何仅仅用一个数组来实现Map
- 如何解决hash冲突
- 如何扩容?