前言
感谢王宝令老师极客时间关于并发的课程
背景
这篇文章解决并发问题的一个重要方法:避免共享
多个线程同时读写同一共享变量会出现并发问题,前面文章我们突破的是"写",如果不存在写的情况,只是读操作就不会存在并发情况。其实还可以突破共享变量 这个层次来解决并发问题,正所谓 没有共享,就没有伤害。
并发领域如何做到不共享呢?每个线程都有自己的一份变量,彼此间不共享,也就没有并发问题了。
局部变量可以做到避免共享,那还有没有其他方法可以做到呢?必须有 ,java 语言提供的线程本地存储(ThreadLocal )就能够做到。
ThreadLocal 的使用方法
一个实际中使用过的方式:
SimpleDateFormat
Date formats are not synchronized. It is recommended to create separate format instances for each thread.If multiple threads access a format concurrently, it must be synchronized externally。
既然直接使用是不安全的,那如何在并发场景下使用呢?
自然想到了 用ThreadLocal 来保证每个线程单独一份数据
ThreadLocal 的工作原理
如果让你来设计一个用于本地存储的组件,你该如何设计呢?答案如下:
class MyThreadLocal<T> {
private ConcurrentMap<Thread, T> map = Maps.newConcurrentMap();
public ConcurrentMap<Thread, T> getMap() {
return map;
}
public void setMap(ConcurrentMap<Thread, T> map) {
this.map = map;
}
public T get(Thread t) {
return map.get(t);
}
void set(T t) {
map.put(Thread.currentThread(), t);
}
}
以上代码就是对 ThreadLocal 的一个简单实现。但是 JDK 中是不是这么实现的呢?
显然不是,我们设计的 map 是属于ThreadLocal 的,而 JDK 中的实现是是里面ThreadLocalMap 属于Thread 。这两种哪种更合理呢?当然 JDK 了,该方案中ThreadLocal 仅仅是一个代理工具,内部并不持有任何与线程相关的数据。所有跟线程相关的数据都保存在Thread 里面,这样设计更容易理解。从数据的亲缘性角度讲,ThreadLocalMap 属于Thread 也更加合理。
当然还有一个深层次的原因就是,不容易产生内存泄漏。在我们的设计方案中,ThreadLocal 持有的map 会持有Thread 对象的引用,这意味着 只要ThreadLocal 对象存在,那么Map 中的对象Thread 就永远不会被回收,ThreadLocal 的生命周期往往都比线程要长,所以这种设计方案很容易导致内存泄露,而Java 的实现中,Thread 持有ThreadLocalMap ,而且 ThreadLocalMap 里面对 ThreadLocal 的引用还是弱引用,所以只要是 Thread 对象可以被收回,那么ThreadLocalMap 就可以被收回。Java 的实现方案虽然看上去复杂,但是安全一些。
Java 的 ThreadLocal 既然实现如此缜密,那能否一定能避免内存泄露了呢?
多线程中使用ThreadLocal 有可能导致内存泄露。
ThreadLocal 与内存泄露
在线程池中使用为什么会导致内存泄露呢?原因就在于线程池中线程的存活时间太长,往往都是和程序共生死。这就意味着 Thread 持有的ThreadLocalMap 一直都不会回收,再加上 ThreadLocalMap 中 的Entry 对 ThreadLocal 是弱引用,所以只要 ThreadLocal 结束了自己的生命周期是可以被回收掉的。 但是 Entry 中的 Value 却是被 Entry 强引用的, 所以即便 Value 的生命周期结束了, Value 也是无法被回收的,从而导致内存泄露。
那在线程池中,我们该如何正确使用 ThreadLocal 呢? 其实很简单,既然 JVM 不能做到自动释放对 Value 的强引用, 那我们手动释放就可以了。 。如何能做到手动释放呢? 估计你马上想到 try{}finally{}方案了, 这个简直就是手动释放资源的利器。
InheritableThreadLocal 与继承性
通过 ThreadLocal 创建的线程变量, 其子线程是无法继承的。 也就是说你在线程中通过 ThreadLocal 创建了线程变量 V, 而后该线程创建了子线程, 你在子线程中是无法通过 ThreadLocal 来访问父线程的线程变量 V 的。
如果你需要子线程继承父线程的线程变量, 那该怎么办呢?其实很简单,Java 提供了 InheritableThreadLocal 来支持这种特性, InheritableThreadLocal 是 ThreadLocal 子类,所以用法和 ThreadLocal 相同,这里就不多介绍了。
不过,我完全不建议你在线程池中使用 InheritableThreadLocal,不仅仅是因为它具有 ThreadLocal 相同的缺点——可能导致内存泄露,更重要的原因是:线程池中线程的创建是动态的,很容易导致继承关系错乱,如果你的业务逻辑依赖 InheritableThreadLocal,那么很可能导致业务逻辑计算错误,而这个错误往往比内存泄露更要命。
总结
线程本地存储模式本质上是一种避免共享的方案,由于没有共享,所以自然也就没有并发问题。如果你需要在并发场景中使用一个线程不安全的工具类,最简单的方案就是避免共享。避免共享有两种方案,一种方案是将这个工具类作为局部变量使用,另外一种方案就是线程本地存储模式。这两种方案,局部变量方案的缺点是在高并发场景下会频繁创建对象,而线程本地存储方案,每个线程只需要创建一个工具类的实例,所以不存在频繁创建对象的问题。线程本地存储模式是解决并发问题的常用方案,所以 Java SDK 也提供了相应的实现:ThreadLocal。通过上面我们的分析,你应该能体会到 Java SDK 的实现已经是深思熟虑了,不过即便如此,仍不能尽善尽美,例如在线程池中使用 ThreadLocal 仍可能导致内存泄漏,所以使用 ThreadLocal 还是需要你打起精神,足够谨慎。
网友评论