ThreadLocal

再次重复一遍,并发问题出现的根源是多个线程同时读写同一共享变量。通过 Immutable 的方式可以只读不写,还有另一种方式就是:避免共享

比如局部变量是线程安全的,因为每个线程都有自己的一份,没有共享,即线程封闭。Java 提供 ThreadLocal 也能做到避免共享,即每个线程都有自己的副本。

使用方法

我们知道 SimpleDateFormat 不是线程安全的,如果要在并发场景下使用它,有一个办法就是通过 ThreadLocal。

如果是同一个线程调用 get 方法,则会返回相同的实例,若是不同的线程调用 get 方法,则会返回不同的实例。

// java.lang.Thread.java
public class SafeDateFormat {
    private static final ThreadLocal<SimpleDateFormat> th = 
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static DateFormat get() {
        return th.get();
    }
}

原理实现

需要实现 ThreadLocal 的逻辑,我们有两个思路:

  • 在 ThreadLocal 中持有一个 Map,key 是线程 ID,value 是我们需要的值。

  • 在 Thread 中持有一个 Map,key 是 ThreadLocal,value 是我们需要的值。

Java 使用的是第二种思路,理由是:

  • 数据亲缘性:ThreadLocal 只是一个工具类,内部不应该存储与线程相关的数据,与线程相关的数据应该放在 Thread 里面。

  • 不容易产生内存泄漏:ThreadLocal 的生命周期往往比线程长,若数据放在 ThreadLocal中,就算 Thread 已经销毁了,相关的数据也不会被回收;若数据放在 Thread 中,Thread 对象可以被回收,那么 ThreadLocalMap 也可以被回收了。

下面是 Java 1.11 精简后的代码:

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
    public Thread() {
        // inheritableThreadLocals 的逻辑
        // 若创建线程的线程存在 inheritableThreadLocals,则通过复制继承
        Thread parent = currentThread();
        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    }
}
// java.lang.ThreadLocal.java
public class ThreadLocal<T> {
    static class ThreadLocalMap {
    
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        
        private Entry[] table;
        
        private Entry getEntry(ThreadLocal<?> key) {}
        private void set(ThreadLocal<?> key, Object value) {}
    }
    
    public T get() {
        ThreadLocalMap map = getMap(Thread.currentThread());
        ...
    }
    
    public void set(T value) {
        ThreadLocalMap map = getMap(Thread.currentThread());
        ...
    }
    
    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
     }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
}

注意事项

内存泄漏

上文提到 Java 设计成 Thread 持有 ThreadLocal 数据的原因之一是不容易产生内存泄漏,但是在某些情况下还是会有内存泄漏的问题。

比如在线程池中使用 ThreadLocal。线程池中的线程存活时间很长,往往与程序的生命周期一致,所以 Thread 持有的 ThreadLocalMap 一直不会被回收。尽管 ThreadLocalMap 中的 Entry 对 ThreadLocal 是弱引用的,只要 ThreadLocal 自己的生命周期结束就可以被回收,但是 Entry 中的 Value 是强引用,所以尽管 Value 的生命周期结束了,也不会被回收,导致内存泄漏。

解决方案:手动释放,比如通过 try-finally。

ThreadLocal tl;

tl.set(obj);
try {
    ...
} finally {
    tl.remove();
}

InheritableThreadLocal

ThreadLocal 创建的线程变量,子线程是无法继承的,也就是说你在线程 A 中创建了 ThreadLocal V,然后又在线程 A 中创建了线程 B,但是在线程 B 中无法访问到 V。

Java 提供了 InheritableThreadLocal 来支持这种需求。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

Last updated