线程封闭与ThreadLocal

线程封闭与ThreadLocal

多线程访问共享可变数据时,涉及到线程间数据同步问题。然而,并不是所有时候都需要共享数据,所以,线程封闭的概念就提出来了。

通过将数据封闭在线程中而避免使用同步的技术称为线程封闭。

线程封闭的具体体现有:

  • 局部变量
  • ThreadLocal

局部变量

局部变量位于执行线程的栈中,其他线程无法访问这个栈。线程封闭是局部变量的固有属性。

ThreadLocal

java.lang.ThreadLocal,顾名思义,它可以存放线程本地变量。ThreadLocal让每个线程维护变量的一个副本,各线程通过ThreadLocal去访问该变量时会拿到各自的副本,副本之间相互独立,互不影响,这样竞争条件被彻底消除了。

使用示例

下面通过一个例子来验证ThreadLocal的特性。

public class ThreadLocalTest {

    private static final ThreadLocal<String> value = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        value.set("主线程设置的123");
        System.out.println("线程1执行之前,主线程取到的值: " + value.get());
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("线程1取到的值: " + value.get());
                    value.set("线程1设置的值456");
                    System.out.println("重新设置后线程1取到的值: " + value.get());
                    System.out.println("线程1执行结束");
                } finally {
                    value.remove();
                }
            }
        }, "线程1");
        thread.start();
        // 等待线程1执行结束
        thread.join();
        System.out.println("线程1执行之后,主线程取到的值: " + value.get());
        value.remove();
    }
}

这段程序的输出是:

线程1执行之前,主线程取到的值: 主线程设置的123
线程1取到的值: null
重新设置后线程1取到的值: 线程1设置的值456
线程1执行结束
线程1执行之后,主线程取到的值: 主线程设置的123

可以看出,不同的线程通过ThreadLocal进行变量的读写时,是互不干扰的。

原理分析

ThreadLocal这么神奇,它到底是怎么实现的呢?

ThreadLocal有3个核心方法:

  • get()
  • set()
  • remove()

这里主要看get()方法 。

public T get() {
    // 拿到当前线程对应的ThreadLocalMap对象
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 从map中查询对应的变量副本
    if (map != null) {
        // 以ThreadLocal对象为key,从map中获取ThreadLocalMap.Entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果entry不为空,entry的value就是目标变量副本
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 否则,初始化变量副本
    return setInitialValue();
}

get()方法中可以看出,我们希望得到的变量副本存放在ThreadLocalMap中。而ThreadLocalMap是和线程绑定的:

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

Thread类里,有这样一个属性:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap的结构如下:

static class ThreadLocalMap {

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

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

ThreadLocalMap是一个哈希表,它里面存放若干个指向ThreadLocal对象的弱引用,而我们需要的value值就挂靠在这个弱引用上。因此,根据ThreadLocal找到对应的Entry就能拿到目标变量的副本。

这里使用弱引用的目的是希望在ThreadLocal对象被回收后可以自动回收value对象。

接下来看get()方法里的第二个分支,setInitialValue()。进入这个分支说明当前线程对应的ThreadLocalMap还未初始化,或者ThreadLocalMap里面还没有初始化ThreadLocal对象对应的Entry

private T setInitialValue() {
    // 获取初始值(变量副本)
    T value = initialValue();
    // 获取当前线程对应的ThreadLocalMap
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 如果ThreadLocalMap已经初始化,则将ThreadLocal对象和变量副本的映射关系保存在map中
    if (map != null)
        map.set(this, value);
    // 否则,初始化ThreadLocalMap,并保存ThreadLocal对象和变量副本的映射关系
    else
        createMap(t, value);
    // 返回变量副本的值
    return value;
}

其中,initialValue()的实现是:

protected T initialValue() {
    return null;
}

这是一个protected方法,默认返回null值。这意味着,对于一个ThreadLocal对象,线程访问它拿到的默认变量副本是null(这也解释了在前面的示例中线程1一开始拿到的是null值)。我们可以覆盖这个方法,指定一个默认的变量副本,这样可以省去调用get()方法时的一次非空判断。ThreadLocal类里有一个静态内部类SuppliedThreadLocal,它已经帮我们覆盖了默认的initialValue()方法,只需要使用ThreadLocal的静态方法ThreadLocal#withInitial就可以在创建ThreadLocal对象时轻松指定默认值。

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

到这里,我们对get()方法的有了大致的了解:获取当前线程的ThreadLocalMap对象,在ThreadLocalMap里以ThreadLocal对象为Key查询EntryEntry对应的value就是我们希望得到的变量副本。如果查找失败,就初始化变量副本(还可能初始化ThreadLocalMap),并存入ThreadLocalMap里,再将变量副本返回给调用者。

ThreadLocal与使用它的Thread紧密相连:

  • 一个Thread有且仅有一个ThreadLocalMap对象。
  • 一个ThreadLocalMap对象存储多个Entry对象。
  • 一个Entry对象的key的弱引用指向一个ThreadLocal对象。
  • 一个ThreadLocal对象被多个线程所共享。
  • ThreadLocal对象不持有value,value由线程的Entry对象持有。

了解了get()的实现逻辑,set()remove()方法就不难理解了,这里不再展开。

注意事项

ThreadLocal的主要问题是会产生脏数据和内存泄漏。这两个问题通常是在线程池中使用ThreadLocal引发的,因为线程池有线程复用和内存常驻两个特点。

  1. 脏数据

线程复用会产生脏数据。由于线程池会重用Thread对象,那么与Thread绑定的ThreadLocalMap变量也会被重用。如果在实现的线程的run()方法中不显式的调用remove()清理与线程相关的ThreadLocal信息,那么倘若下一个任务不调用set()设置初始值,就有可能get()到重用的线程信息,包括ThreadLocal所关联的线程对象的value值。

  1. 内存泄漏

通常使用static关键字来修饰ThreadLocal,在此场景下,寄希望于ThreadLocal对象失去引用后,触发弱引用机制来回收Entry的value就不现实了。如果不进行remove()操作,那么ThreadLocal对象持有的value是不会被释放的。

以上两个问题解决办法很简单,就是在每次用完ThreadLocal时,必须及时调用remove()方法清理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值