线程安全——线程封闭(四)

什么是线程封闭?

实现好的并发是一件困难的事情,所以很多时候我们都想躲避并发。避免并发最简单的方法就是线程封闭。

就是把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对象就算不是线程安全的也不会出现任何安全问题。

实现线程封闭的方法?

  1. 栈封闭

    什么是栈封闭呢?简单的说就是局部变量。

  2. 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
    21
    public 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
2
3
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

每个线程都将自己维护一个 ThreadLocal.ThreadLocalMap 类在上下文中;

ThreadLocal类的set方法,是将ThreadLocal的对象赋予当前线程的 threadLocals, threadLocals以ThreadLocal类为 key。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

get 方法也是类似的道理, 从线程的 ThreadLocalMap 中获取以当前 ThreadLocal 为 key 对应的 value:

1
2
3
4
5
6
7
8
9
10
11
12
13
public T get() { 
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

需要注意的是, 如果没有 set 过 value, 此处 get() 将返回 null, 不过 initialValue() 方法是一个 protected 方法, 所以子类可以重写逻辑实现自定义的初始默认值。

综上所述: ThreadLocal 实现线程关联的原理是与 Thread 类绑定, 将数据存储在对应 Thread 的上下文中。

ThreadLocal 导致的内存泄露

弱引用

ThreadLocal.ThreadLocalMap 类中维护了的一个自定义数据结构 Entry, 其定义如下:

1
2
3
4
5
6
7
8
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

这里要注意的是, 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并不是去解决多线程共享变量的问题,而是为每一个线程在本地维护一个与其他线程隔离的实例。

文章目录
  1. 1. 什么是线程封闭?
  2. 2. 实现线程封闭的方法?
  3. 3. ThreadLocal原理
  4. 4. ThreadLocal 导致的内存泄露
    1. 4.1. 弱引用
    2. 4.2. 主动清理
  5. 5. 总结
|