多个线程同时读写同一个共享变量会造成并发问题,一种解决方案就是避免变量共享。我们可以使用线程封闭技术,即使用局部变量,每个线程都有各自的调用栈,局部变量就存在栈帧中,不会与其他线程共享。我们还可以使用线程本地存储ThreadLocal。
如何使用 ThreadLocal
下面这段代码会为每个线程分配一个唯一的线程Id,同一个线程每次调用 get() 获得的 Id 是一样的,不同的线程调用 get() 获得的 Id 是不一样的。
static class ThreadId {
static final AtomicLong nextId = new AtomicLong(0);
//定义ThreadLocal变量
static final ThreadLocal<Long> tl=
ThreadLocal.withInitial(()->nextId.getAndIncrement());
//此方法会为每个线程分配一个唯一的Id
static long get(){
return tl.get();
}
}
ThreadLocal 中除了构造方法还有 4 个公共的方法:
-
get()
:返回此线程局部变量当前副本中的值 -
remove()
:移除此线程局部变量当前副本中的值 -
set(T value)
:将线程局部变量当前副本中的值设置为指定值 -
withInitial(Supplier<? extends S> supplier)
:返回此线程局部变量当前副本中的初始值
ThreadLocal 的工作原理
ThreadLocal 要实现的目标是:不同的线程对应不同的变量,很自然地可以想到创建一个 Map,其中 Key 是线程,Value 是线程对应的值。那么可以让 ThreadLocal 持有一个这样的 map,并提供对应的方法,就像下面这样:
class MyThreadLocal<T> {
Map<Thread, T> locals =
new ConcurrentHashMap<>();
//获取线程变量
T get() {
return locals.get(
Thread.currentThread());
}
//设置线程变量
void set(T t) {
locals.put(
Thread.currentThread(), t);
}
}
这样设计会产生内存泄露的问题。ThreadLocal 持有 Map 的引用,Map 持有 Thread 对象的引用。这就意味着,只要 ThreadLocal 对象存在,那么 Map 中的 Thread 对象就不会被释放。ThreadLocal 对象的生命周期往往比线程要长得多,当长生命周期的对象持有短生命周期对象的引用,就会造成内存泄露问题。
在 Java 的设计中,ThreadLocal 持有的 Map 被命名为 ThreadLocalMap。ThreadLocalMap 并不是由 ThreadLocal 持有,而是由 Thread 持有。ThreadLocal 作为一个代理工具类,内部并不持有任何与线程相关的数据,所有和线程相关的数据都存储在 Thread 里面。如下面的代码所示:
public
class Thread implements Runnable {
// Thread 内部持有 ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
}
public class ThreadLocal<T> {
public T get() {
// 1. 获取线程持有的 ThreadLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 2. 在Map中查找变量
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
static class ThreadLocalMap{
// Entry定义,是一个弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 内部是数组而不是Map
private Entry[] table;
// 根据ThreadLocal查找Entry
Entry getEntry(ThreadLocal key){
//省略查找逻辑
...
}
}
}
理解 ThreadLocal 的原理,要结合上面这张图和代码:
- 当前线程线程持有 ThreadLocalMap,ThreadLocalMap 持有 Entry;
- Entry 的 Key 是一个 ThreadLocal 实例,并且是一个弱引用,value 是我们要存储的值;
Java 的实现中 Thread 持有 ThreadLocalMap,ThreadLocalMap 中的 Entry 对 ThreadLocal 的引用是弱引用,所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。
ThreadLocal 与内存泄露
在线程池中使用 ThreadLocal 任然可能会出现内存泄露。
因为线程池中线程的存活时间太长了,往往是和应用程序同生共死的。这就意味着 Thread 持有的 ThreadLocalMap 一直不会被回收,ThreadLocalMap 中 Entry 对 ThreadLocal 的引用是弱引用,所以只要 ThreadLocal 的生命周期结束是可以被回收的。但是 Entry 对 Value 是强引用,即使 value 的生命周期结束也无法被回收,这就造成了内存泄露。
在线程池中如何正确使用 ThreadLocal?
那在线程池中,我们该如何正确使用 ThreadLocal 呢?既然 JVM 无法帮我们释放对 value 的引用,那么我们就使用 try{}finally{} 手动释放资源:
ExecutorService es;
ThreadLocal tl;
es.execute(()->{
//ThreadLocal增加变量
tl.set(obj);
try {
// 省略业务逻辑代码
}finally {
//手动清理ThreadLocal
tl.remove();
}
});