Java 进阶之路

ThreadLocal 线程本地存储

2020-03-16  本文已影响0人  弄浪的鱼
ThreadLocal | 线程本地存储

并发场景下,多个线程同时读写共享变量就有可能产生并发安全问题。反过来也可以说,不存在共享变量,就不会出现线程安全问题。Java中有两种常用的避免共享变量的方法,使用局部变量,以及使用 ThreadLocal。

局部变量存在于每个线程内部的调用栈中,多个线程之间互相访问不到对方的局部变量,这就叫做线程封闭。如下图所示,局部变量存在于线程各自的调用栈中,线程之间互不打扰。

局部变量,线程封闭

采用局部变量的方案,的确避免了变量被多个线程共享,同时它也禁止同一个线程中不同方法共享这个变量。然而,单线程中不同的方法之间共享变量是不会导致线程安全问题的。


如果想让同一个线程,不同的方法共享变量就可以使用 ThreadLocal,Java 提供的线程本地存储方案。ThreadLocal 可以保证同一个变量,该线程中的方法看到的值是一样,不同线程之间却是隔离。


ThreadLocal 的使用方法

常规使用 ThreadLocal 的方式很简单,创建一个 ThreadLocal 对象,然后调用它的 set(value)方法设置值,再调用 get() 方法获取这个 ThreadLocal 对象对应的value。

// 创建一个 ThreadLocal
ThreadLocal<String> tl = new ThreadLocal<>();
// set方法
tl.set("深页");
// get方法
tl.get();

ThreadLocal 类的注释中还带有为每个线程分配自增 id 的示例代码。withInitial()方法会调用initialValue()方法,为 ThreadLocal 设置 get() 的初始值。执行下面的代码,可以看到每个类都有自己的id,并且id的自增的。

public class ThreadId {

    // Integer类型的原子类,用来分配Id,保证其本身是线程安全的
    private static final AtomicInteger nextId = new AtomicInteger();

    // 创建一个 ThreadLocal 变零,并且为其赋值
    private static ThreadLocal <Integer> threadId = ThreadLocal.withInitial(
            () -> nextId.getAndIncrement()
    );

    // 获取id,即从ThreadLocal中获取对应的值
    public static int getId() {
        return threadId.get();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    System.out.println(Thread.currentThread().getName()
                            + ": " + ThreadId.getId()))
                    .start();
        }
    }
}

ThreadLocal还有一个经典的使用案例,就是将线程不安全的 SimpleDateFormat 类封装成线程安全的,原理其实和上面的例子是一样:

static class SafeDateFormat {
    static final ThreadLocal<DateFormat> tl = ThreadLocal.withInitial(
            ()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    );
    
    static DateFormat get() {
        return tl.get();
    }
}

ThreadLocal的底层原理

先来看 set() 方法:

  1. 首先获取当前线程,然后通过当前线程获取线程持有的局部变量 threadLocals
  2. 如果返回的 map 不是空的就设置值
  3. 如果返回的 map 是空的,就调用构造方法初始化 map 并为其设置值
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

再看 get() 方法:

public T get() {
    Thread t = Thread.currentThread();
    // getMap()返回当前线程的threadLocals
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // map存在返回当前ThreadLocal对应的value值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // map不存在就初始化
    return setInitialValue();
}

看过上面两个方法,可以看到它们除了涉及到 Thread 类,还涉及到了一个类 ThreadLocalMap。那么 Thread、ThreadLocal、ThreadLocalMap 之间是什么关系呢?

ThreadLocal 的数据结构

ThreadLocalMap 是 ThreadLocal 的静态内部类,ThreadLocalMap 的底层是一个 Entry[] table 数组,Entry 是 ThreadLocalMap 的静态内部类,以 ThreadLocal 作为 key,以设置的值作为 value,如下所示:

static class Entry extends WeakReference <ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

Thread 持有一个 ThreadLocalMap 的引用 threadLocals

ThreadLocal.ThreadLocalMap threadLocals = null;

所以说,我们通过当前线程 Thread t 可以到 t 持有的 ThreadLocalMap,并且通过 ThreadLocal 对象返回其对应的 value。

ThreadLocal 内存泄露问题

使用 Thread.start() 方法是不会产生内存泄露的问题的,只有当我们在线程池中使用 ThreadLocal 才有可能产生内存泄露问题。

内存泄露的本质是长生命周期的对象,持有短生命周期对象。当短生命周期的对象使用结束之后,理应被垃圾回收器回收,但是它却被一个更长生命周期的对象引用。通过可达性分析算法,该短生命周期的对象被一个GC Root引用,理应被回收的它就无法被回收。

那为什么在线程池中使用ThreadLocal就可能发生内存泄露的问题呢?我们就从长生命周期的对象,持有短生命周期对象这个角度进行分析。

图片来自于网络

线程池作为一种池化资源技术,目的是避免线程的频繁创建和销毁。一般来说,线程池中的线程生命周期都很长,是和应用程序同生共死的。这就意味着,被 Thread 持有的 ThreadLocalMap 一直都不会被回收。

ThreadLocalMap 底层是一个 Entry 数组,Entry是<ThreadLocal,value>对结构。 Entry 对 ThreadLocal 是弱引用(WeakReference),所以ThreadLocal 生命周期之后,是结束是可以被回收掉的。但是 Entry 对 value 强引用的,所以即便 Value 的生命周期结束了,Value 也是无法被回收的,从而导致内存泄露。

InheritableThreadLocal 与继承性

使用 ThreadLocal 还有这样一种需求,ThreadLocal 创建了线程变量 V,然后希望该线程创建的子线程也能访问到父线程的线程变量 V。

为此 Java 提供了 InheritableThreadLocal 来支持这种特性,InheritableThreadLocal 继承自 ThreadLocal,用法其实和 ThreadLocal 一样。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

最后做一个小结,多线程同时读写共享变量就有可能产生并发问题。一种解决并发问题的思路就是避免变量被共享。与之对应的技术有线程隔离(局部变量),以及线程本地存储ThreadLocal

相比于使用局部变量,ThreadLocal 存储的变量可以供线程中的方法共享,单线程对共享变量的读写必定是线程安全的。

上一篇下一篇

猜你喜欢

热点阅读