什么是线程封闭?
实现好的并发是一件困难的事情,所以很多时候我们都想躲避并发。避免并发最简单的方法就是线程封闭。
就是把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对象就算不是线程安全的也不会出现任何安全问题。
实现线程封闭的方法?
栈封闭
什么是栈封闭呢?简单的说就是局部变量。
ThreadLocal 封闭
ThreadLocal 翻译成中文比较准确的叫法应该是:线程局部变量。
其实 ThreadLocal 内部维护了一个 Map,Map 的 key 是每个线程的名称,value 就是我们要封闭的对象。每个线程中的对象都对应着 Map 中一个值,也就是 ThreadLocal 利用 Map 实现了对象的线程封闭。
比如说 DAO 的数据库连接,我们知道 DAO 是单例的,那么他的属性 Connection 就不是一个线程安全的变量。而我们每个线程都需要使用他,并且各自使用各自的。这种情况,ThreadLocal 就比较好的解决了这个问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public final class ConnectionUtil {
private ConnectionUtil() {}
private static final ThreadLocal<Connection> conn = new ThreadLocal<>();
public static Connection getConn() {
Connection con = conn.get();
if (con == null) {
try {
Class.forName("com.mysql.jdbc.Driver");
con = DriverManager.getConnection("url", "userName", "password");
conn.set(con);
} catch (ClassNotFoundException | SQLException e) {
// ...
}
}
return con;
}
}这样子,都是用同一个连接,但是每个连接都是新的,是同一个连接的副本。
ThreadLocal原理
1、 线程关联
ThreadLocal 并不是一个独立的存在, 它与 Thread 类是存在耦合的, java.lang.Thread 类针对 ThreadLocal 提供了如下支持:
1 | /* ThreadLocal values pertaining to this thread. This map is maintained |
每个线程都将自己维护一个 ThreadLocal.ThreadLocalMap 类在上下文中;
ThreadLocal类的set方法,是将ThreadLocal的对象赋予当前线程的 threadLocals, threadLocals以ThreadLocal类为 key。
1 | public void set(T value) { |
get 方法也是类似的道理, 从线程的 ThreadLocalMap 中获取以当前 ThreadLocal 为 key 对应的 value:
1 | public T get() { |
需要注意的是, 如果没有 set 过 value, 此处 get() 将返回 null, 不过 initialValue() 方法是一个 protected 方法, 所以子类可以重写逻辑实现自定义的初始默认值。
综上所述: ThreadLocal 实现线程关联的原理是与 Thread 类绑定, 将数据存储在对应 Thread 的上下文中。
ThreadLocal 导致的内存泄露
弱引用
ThreadLocal.ThreadLocalMap 类中维护了的一个自定义数据结构 Entry, 其定义如下:
1 | static class Entry extends WeakReference<ThreadLocal<?>> { |
这里要注意的是, Entry 类继承了弱引用 WeakReference, 更具体的说, Entry 中的 key (ThreadLocal 类型) 使用弱引用, value 依旧使用强引用。
假设 Entry 不继承 WeakReference, 令 key 也使用强引用, 那么结合上一节的内容, 只要该 thread 不退出, 通过 Thread -> ThreadLocal.ThreadLocalMap -> key 这条引用链, 该 key 就可以一直与 gc root 保持连通; 这时即便在外部这个 key 对应的 threadLocal 已经没有有效引用链了, 但只要该 thread 不退出, jvm 依旧会判定该 threadlocal 不可回收。
于是尴尬的事情发生了: 由于 ThreadLocal.ThreadLocalMap 这个内部类没有对外暴露 public 方法, 在 Thread 类里面 ThreadLocal.ThreadLocalMap 也是 package accessible 的, 这意味着我们已经没有任何方法访问到该 key 对应的 value 了, 可它就是无法被回收, 这便是一个典型的内存泄露。
而如果使用 WeakReference 这个问题就解决了: 当该 key 对应的 threadlocal 在外部已经失效后, 便仅存在 thread 里的 weak reference 指向它, 下次 gc 时这个 key 就会被回收掉。
针对这一特性, ThreadLocal.ThreadLocalMap 也配套了与之相适应的内部清理方法,会遍历整个 entry table, 当发现有 key 为 null 时, 就会触发 rehash 压缩整个 table, 以达到清理的作用。
主动清理
下面就要提到这里的一个隐藏的坑, ThreadLocal 并没有配合使用 ReferenceQueue 来监听已经回收的 key 以实现自动回调 expungeStaleEntry 方法清理空间的功能; 所以 threadlocal 实例是回收了, 但是引用本身还在, 其所对应的 value 也就还在:
实际上, expungeStaleEntry 方法是被安插到了 ThreadLocal.ThreadLocalMap 中的 get, set, remove 等方法中, 并被 ThreadLocal 的 get, set, remove 方法间接调用, 必须显式得调用这些方法, 才能主动式地清理空间。
在某些极端场景下, 如果某些 threadlocal 设置的 value 是大对象, 而所涉及的 thread 却没来得及在 threadlocal 被 gc 前作 remove, 再加上之后也没有什么其他 threadlocal 去作 get / set 操作, 那这些大对象是没机会被回收的, 这将造成严重的内存泄露甚至是 OOM。所以使用 ThreadLocal 要谨记一点: 用完主动 remove, 主动释放内存, 而且是放在 finally 块里面 remove, 以确保执行。
在很多系统中, 我们会定义一个 static final 的全局 ThreadLocal, 这样其实就不存在 threadlocal 被回收的情况了, 上面说的 WeakReference 机制也将效用有限, 这种环境下我们就更加需要用完后主动作 remove 了。
总结
ThreadLocal并不是去解决多线程共享变量的问题,而是为每一个线程在本地维护一个与其他线程隔离的实例。