0%

线程本地存储模式:没有共享就没有伤害

多个线程同时读写同一共享变量存在并发问题。
突破写,没有写操作就没有并发问题。
突破共享变量,没有共享变量也不会有并发问题。

并发编程领域,每个线程都拥有自己的变量,彼此之间不共享,也就没有并发问题了。

并发容器中线程封闭,本质上就是避免共享。
通过局部变量可以做到避免共享,Java语言提供的线程本地存储ThreadLocal也能做到避免共享。

ThreadLocal的使用方法

1
2
3
4
5
6
7
8
9
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();
}
}

该静态类ThreadId会为每个线程分配一个唯一的Id,如果一个线程前后两次调用ThreadId的get()方法,两次get()方法的返回值是相同的。但是如果是两个线程分别调用ThreadId的get()方法,那么两个线程看到的get()方法的返回值是不同的。

SimpleDateFormat不是线程安全的,如果在并发场景下使用它,其中一个办法是用ThreadLocal来解决。不同线程调用SafeDateFormat的get()方法将返回不同的SimpleDateFormat对象实例,由于不同线程并不共享SimpleDateFormat,所以就像局部变量一样是线程安全的。

1
2
3
4
5
6
7
8
9
10
static class SafeDateFormat {
//定义ThreadLocal变量
static final ThreadLocal<DateFormat> tl = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

static DateFormat get() {
return tl.get();
}
}
//不同线程执行下面代码返回的df是不同的
DateFormat df = SafeDateFormat.get();

ThreadLocal的工作原理

ThreadLocal的目标是让不同的线程有不同的变量V,最直接的方法就是创建一个Map,它的Key是线程,Value是每个线程拥有的变量V,ThreadLocal内部持有这样一个Map就可以的了。

1
2
3
4
5
6
7
8
9
10
11
12
//自定义实现ThreadLocal
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);
}
}

Java实现ThreadLocal里面也有一个Map-ThreadLocalMap,不过持有ThreadLocalMap的不是ThreadLocal,而是Thread。Thread这个类内部有一个私有属性ThreadLocals,其类型就是ThreadLocalMap,ThreadLocalMap的Key是ThreadLocal。

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
class Thread {
//内部持有ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals;
}
class ThreadLocal<T> {
public T get() {
//首先获得线程持有的ThreadLocalMap
ThreadLocalMap map = Thread.currentThread().threadLocals;
//在ThreadLocalMap中查找变量
Entry e = map.getEntry(this);
return e.value;
}
static class ThreadLocalMap {
//内部是数组而不是Map
Entry[] table;
//根据ThreadLocal查找Entry
Entry getEntry(ThreadLocal key) {
//省略查找逻辑
}
//Entry定义
static class Entry extends WeakReference<ThreadLocal> {
Object value;
}
}
}

表面上自定义方案和Java方案仅仅是Map的持有方不容而已,自定义里面Map属于ThreadLocal,Java里面ThreadLocalMap属于Thread。

Java的实现方案里,ThreadLocal仅仅是一个代理工具类,内部并不持有任何与线程相关的数据,所有和线程相关的数据都存储在Thread里面。从数据的亲缘性上来讲,ThreadLocalMap属于Thread更加合理。

更深层次,不容易产生内存泄漏。自定义实现方案中,ThreadLocal持有的Map会持有Thread对象的引用,这就意味着,只要ThreadLocal对象存在,那么Map中的Thread对象就永远不会被回收。ThreadLocal的生命周期往往都比线程要长,所以这种设计方案很容易导致内存泄漏。而Java的实现中Thread持有ThreadLocalMap,而且ThreadLocalMap里对ThreadLocal的引用是弱引用(WeakReference),所以知道Thread对象可以被回收,那么ThreadLocalMap就能被回收。Java方案复杂但是安全。

在线程池中使用ThreadLocal,不谨慎依然可能导致内存泄漏。

ThreadLocal与内存泄漏

在线程池中,线程的存活时间太长,往往都是和程序同生共死的,使用ThreadLocal意味着Thread持有的ThreadLocalMap一直都不会被回收,再加上ThreadLocalMap中的Entry对ThreadLocal是弱引用,只要ThreadLocal结束了自己的生命周期是可以被回收掉的但是Entry中的Value是被Entry强引用的,所以即便Value的生命周期结束了,Value也是无法被回收的,从而导致内存泄漏。

JVM不能做到自动释放对Value的强引用,需要手动释放线程池中的ThreadLocal。try{}finally{}方案是手动释放资源的利器。

1
2
3
4
5
6
7
8
9
10
11
12
ExecutorService es;
ThreadLocal tl;
ex.execute(()->{
//ThreadLocal增加变量
tl.set(obj);
try{
//省略业务逻辑代码
} finally {
//手动清理ThreadLocal
tl.remove();
}
});

InheritableThreadLocal与继承性

通过ThreadLocal创建的线程变量,其子线程是无法继承的。在线程中通过ThreadLocal创建了线程变量V,而后该线程创建了子线程,在子线程中是无法通过ThreadLocal来访问父线程的线程变量V的。

如果需要子线程继承父线程的线程变量,Java提供了InheritableThreadLocal来支持这种特性,InheritableThreadLocal是ThreadLocal的子类,用法相同。

线程池中不建议使用InheritableThreadLocal,线程池中线程的创建是动态的,很容易导致继承关系错乱,如果业务略及依赖InheritableThreadLocal,很可能导致业务逻辑计算错误。比内存泄漏更要命。

总结

线程本地存储模式本质上是一种避免共享的方案,由于没有共享,自然没有并发问题。如果需要在并发场景中使用一个线程不安全的工具类,最简单的方案就是避免共享。

  • 将这个工具类作为局部变量使用;
  • 采用线程本地存储模式。
    局部变量方案的缺点是在高并发场景下会频繁创建对象,而线程本地存储方案,每个线程只需要创建一个工具类的实例,不存在频繁创建对象的问题。

线程本地存储是解决并发问题的常用方案,Java SDK也提供了相应的实现:ThreadLocal。