侧边栏壁纸
博主头像
林雷博主等级

斜月沉沉藏海雾,碣石潇湘无限路

  • 累计撰写 132 篇文章
  • 累计创建 47 个标签
  • 累计收到 3 条评论

目 录CONTENT

文章目录

13、ThreadLocal线程局部变量

林雷
2023-07-29 / 0 评论 / 0 点赞 / 149 阅读 / 13,924 字

20180903

一 ThreadLocal源码分析

ThreadLocal可以理解为线程本地变量,它会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就可以了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。
ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,它的key指向ThreadLocal本身的弱引用;在Thread类中有ThreadLocalMap的引用,即每个线程都有一份自己的ThreadLocal。
弱引用是为了防止内存泄漏,如果强引用那么ThreadLocal对象除非线程结束,否则始终无法被回收,弱引用则会在下一次GC的时候被回收。但是这样也会存在内存泄漏的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key为null,但是value有值的entry对象,这个对象也就永远无法被访问,除非线程结束运行。
ThreadLocal是一个线程的本地变量,也就意味着这个变量是线程独有的,是不能与其他线程共享的,也就避免了资源竞争带来的多线程安全问题。

1.1 示例

ThreadLocal示例如下:

public class ThreadLocalTest {
    public static void main(String[] args) {
        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
        threadLocal.set(2);
        System.out.println(threadLocal.get());
        threadLocal.remove();
    }
}

打印的结果是2,示例很简单,描述了ThreadLocal的三个方法:

  • set:存储一个值,这个值只能在当前线程中获取到,也就是main线程
  • get:获取值
  • remove:删除当前线程的值

1.2 使用场景

ThreadLocal在很多情况都可以使用,比较经典的使用场景是Web应用程序,每个用户请求页面时,都会创建一个任务,然后使用线程池执行任务,类似:

public void task(User user) {
    checkPermission(user);
    doWork(user);
    afterWorker(user);
}

上述需要将User传递到所有地方,而这种在一个线程中,横跨多个方法,需要传递的对象,我们通常称之为上下文。而给每个方法都增加一个上下文参数很麻烦,这时我们就可以使用ThreadLocal,它可以在一个线程中传递同一个对象
ThreadLocal实例通常是以静态方法初始化的:

static ThreadLocal<?> threadLocal = new ThreadLocal<>();

上述使用场景就可以改变成如下:

static ThreadLocal<User> threadLocal = new ThreadLocal<>();
public void task(User user) {
    try {
        threadLocal.set(user);
        checkPermission();
        doWork(user);
        afterWorker(user);
    } finally {
        threadLocal.remove();
    }
}

public void checkPermission() {
    User user = threadLocal.get();
}

public void doWork() {
    User user = threadLocal.get();
}

public void afterWorker() {
    User user = threadLocal.get();
}

注意,调用是在同一个线程完成的

1.3 ThreadLocal简介

ThreadLocal表示线程局部变量,每个Thread对象都有一个ThreadLocal.ThreadLocalMap对象的引用,使用 Thread.threadLocals 表示:

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal引用示意图如下:
ThreadLocal-1.3-1

ThreadLocalMap使用的是ThreadLocal.ThreadLocalMap.Entry 数组来存储数据,Entry对象持有key和value,key是ThreadLocal本身,key也是一个WeakReference,而WeakReference如果没有强引用指向的话,那么下次GC一定会被回收;value是ThreadLocal对应的类型T对象,也就是用户调用set(T) 方法设置的值。
所以从这里可以看出,ThreadLocal如果不及时回收内存的话(remove()方法)可能存在内存泄漏的可能,因为在线程中,当没有强引用指向ThreadLocal本身的时候,GC后会回收ThreadLocal.ThreadLocalMap.Entry.key,而对应的value将永远无法获取到,如果线程的生命周期不结束的话,那么value在内存中永远无法获取,线程超过一定数量之后就会占用更高的内存,最终导致无法释放而泄漏。
但是好在,ThreadLocal为我们做了很多工作,就是在set、get等操作时会回收一部分内存,但是依然存在内存泄漏的可能。下面我们进行具体分析。

ThreadLocal有三个全局字段:

/** ThreadLocal的hashCode */
private final int threadLocalHashCode = nextHashCode();

/** 下一个hashCode, 更新是原子的, 默认是0 */
private static AtomicInteger nextHashCode = new AtomicInteger();

/** 两个hashCode的增量 */
private static final int HASH_INCREMENT = 0x61c88647;

这三个全局字段,主要创建Entry的时候,通过上述原子类获取其在Entry数组中的位置。nextHashCode() 代码如下:

/**
 * 获取下一个hashCode
 * @return hashCode
 */
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

可以通过两种方式创建ThreadLocal,如下:

/**
 * 创建一个线程局部变量。初始化值为Supplier
 * @param supplier Supplier
 * @param <S> 类型
 * @return ThreadLocal
 */
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

/**
 * 构造器
 * @see #withInitial(Supplier)
 */
public ThreadLocal() {
}

一种是空构造,一种是静态方法withInitial(Supplier),withInitial(Supplier) 是可以创建初始值的SuppliedThreadLocal,SuppliedThreadLocal是ThreadLocal的子类,重写了initialValue() 方法

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
    /** 初始值的提供器 */
    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

稍后我们看 initialValue() 函数在什么情况下使用。

1.4 ThreadLocalMap介绍

通过上述分析,可以得知Thread主要使用ThreadLocalMap存储数据的,而ThreadLocalMap使用Entry存放key和value。
ThreadLocal.ThreadLocalMap 相关的全局变量如下:

/** 初始容量, 一定是2的幂 */
private static final int INITIAL_CAPACITY = 16;

/** Entry数组 */
private ThreadLocalMap.Entry[] table;

/** table大小 */
private int size = 0;

/** 阈值, 类似Map的负载因子 */
private int threshold;

使用Entry数组来表示key和value的桶,Entry对象是ThreadLocal.ThreadLocalMap的内部类,如下:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** 关联的值 */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);//弱引用
        value = v;//值
    }
}

Entry是一个弱引用,弱引用指向的是ThreadLocal本身,即用户创建的ThreadLocal实例,如果外层没有强引用指向ThreadLocal的话,下一次GC后Entry的key就会被回收了

ThreadLocalMap的构造函数如下:

/**
 * 构造器
 * @param firstKey key
 * @param firstValue value
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new ThreadLocalMap.Entry[INITIAL_CAPACITY];//创建Entry数组, 并指定大小为INITIAL_CAPACITY
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//计算在数组中的位置
    table[i] = new ThreadLocalMap.Entry(firstKey, firstValue);//创建Entry
    size = 1;//size变成1
    setThreshold(INITIAL_CAPACITY);//设置阈值
}

/**
 * 构造器.将{@code parentMap}的数据复制过来
 * @param parentMap ThreadLocalMap
 */
private ThreadLocalMap(ThreadLocalMap parentMap) {
    ThreadLocalMap.Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new ThreadLocalMap.Entry[len];

    for (int j = 0; j < len; j++) {
        ThreadLocalMap.Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                Object value = key.childValue(e.value);
                ThreadLocalMap.Entry c = new ThreadLocalMap.Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

第二个构造函数是私有的,只能在当前类中使用;第一个构造函数会创建Entry数组,key是ThreadLocal,使用threadLocalHashCode生成唯一的hashCode,最后将其存放到数组的指定位置上。

1.5 set存放数据

set(T) 原型如下:

/**
 * 存储值
 * @param value value
 */
public void set(T value) {
    Thread t = Thread.currentThread();//当前线程
    ThreadLocalMap map = getMap(t);//获取ThreadLocalMap
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

getMap(Thread) 从当前线程中获取ThreadLocalMap,如果存在则使用使用ThreadLocalMap存放值,如果不存在使用 createMap(Thread, T) 创建ThreadLocalMap。
getMap(Thread) 代码如下:

/**
 * 获取线程关联的ThreadLocalMap对象
 * @param t 线程
 * @return ThreadLocalMap
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

从Thread对象中获取threadLocals 变量的值。

createMap(Thread, T) 代码如下:

/**
 * 创建一个ThreadLocalMap
 * @param t 当前线程
 * @param firstValue value
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

直接使用ThreadLocalMap的构造函数创建ThreadLocalMap,其中key是this,即用户创建的ThreadLocal对象本身,最后将创建的ThreadLocalMap赋值给Thread.threadLocals变量,所以每个线程ThreadLocal都是私有的。

1.5.1 ThreadLocalMap.set存放数据

在set(T) 中会调用ThreadLocalMap.set(ThreadLocal, T),代码如下:

/**
 * 存放值
 * @param key key
 * @param value value
 */
private void set(ThreadLocal<?> key, Object value) {
    ThreadLocalMap.Entry[] tab = table;//Entry数组
    int len = tab.length;//数组长度
    int i = key.threadLocalHashCode & (len - 1);//计算key在数组中的位置

    /*
     * 这个循环的目的是找到数组中不为空的元素与当前的key相等直接赋值, 否则循环不做操作。
     * 主要是解决连续调用set()操作的这种情况
     */
    for (ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();//获取key

        if (k == key) {//当前key与数组中的key相同, 则直接赋值覆盖并return
            e.value = value;
            return;
        }

        if (k == null) {//如果位置找到元素, 但是由于key是弱引用被GC回收了, 则做进一步操作
            replaceStaleEntry(key, value, i);//这里有存值的操作
            return;
        }
    }

    tab[i] = new ThreadLocalMap.Entry(key, value);//当前数组没有key与当前key相同则直接赋值
    int sz = ++size;

    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

在比较理想的情况(规范使用的情况)下,指定的位置是没有元素的,也就是不会进入 for(){} 代码块,直接在指定的位置上赋值;但是由于存在hash冲突,类似如下操作:

threadLocal.set(1);
threadLocal.set(2);
threadLocal.set(3);

上述操作,多次调用了set(T)而没有remove()的情况下,就会存在hash冲突,即Entry数组的指定位置上已经有值了。为了解决这种情况,使用了 for() {} 循环解决,在循环体中:

  • 如果是同一个ThreadLocal对象,那么直接将用户最新的value赋值给对应的Entry.value
  • 如果出现了GC回收,外面没有强引用的情况下,其Entry.key是弱引用被回收了,就会调用replaceStaleEntry(ThreadLocal, T, int) 方法,在该方法中会做清理动作和赋值动作

如果循环体中没有执行return操作,那么证明当前Entry数组中没有与当前ThreadLocal一样的key,即出现了hash冲突;也没有出现key被回收的情况(外部有强引用指向ThreadLocal),那么代码中 “i” 的值就是往数组的下一个没有元素的位置上递进,最终在对应的位置上存值。
所以说ThreadLocalMap解决hash冲突的方式是往下一个位置上存值,而在get() 操作时也是需要注意的地方

replaceStaleEntry(ThreadLocal, T, int) 代码如下

/**
 * 替换Entry数组指定槽位上的旧的Entry元素
 * @param key key
 * @param value value
 * @param staleSlot 这个位置表示当前元素不为空, 但是元素Entry的key是空的
 */
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    ThreadLocalMap.Entry[] tab = table;//Entry数组
    int len = tab.length;//数组长度
    ThreadLocalMap.Entry e;

    //从staleSlot位置往前寻找, 找到对应位置上没有元素为止; slotToExpunge是元素为空的位置的后面元素的key为空的位置 Start
    int slotToExpunge = staleSlot;//这个槽位的数据是需要清理的
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;
    //从staleSlot位置往前寻找, 找到对应位置上没有元素为止; slotToExpunge是元素为空的位置的后面元素的key为空的位置 End

    //往后遍历, 槽位不为空的地方, 如果key与当前的key相等的话, 则赋值并清除相关数据后退出, 否则做清除动作 Start
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();//获取key

        if (k == key) {//Entry数组中的key与我们要存值的key相同, 做赋值操作
            e.value = value;//赋值

            /*
             * 数据交换, 因为正常通过key计算出来的hashCode是在staleSlot上, 所以
             * staleSlot位置才应该保存值, 而从i往后的其实是需要被清理的
             */
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);//清理
            return;
        }

        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
    //往后遍历, 槽位不为空的地方, 如果key与当前的key相等的话, 则赋值并清除相关数据后退出, 否则做清除动作 End

    //赋值 Start
    tab[staleSlot].value = null;
    tab[staleSlot] = new ThreadLocalMap.Entry(key, value);//存放value
    //赋值 End

    //再做一次清理操作 Start
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    //再做一次清理操作 End
}

而清理操作主要调用的方法是expungeStaleEntry(int) ,代码如下:

/**
 * 由于Entry在Entry(table[])中可能是空的或者对应的key可能是空的(被GC清理了),
 * 所以这个方法主要是清理这种情况的
 * @param staleSlot 索引
 * @return index, 主要是staleSlot的下一个空槽的索引
 */
private int expungeStaleEntry(int staleSlot) {
    ThreadLocalMap.Entry[] tab = table;//当前的hash表
    int len = tab.length;//长度

    //清空当前槽的value(该value的key可能是空的或者当前槽就是空的) Start
    tab[staleSlot].value = null;//清除value
    tab[staleSlot] = null;//清除槽
    size--;//递减
    //清空当前槽的value(该value的key可能是空的或者当前槽就是空的) End

    //从当前staleSlot往前移动, 如果槽中存在值(Entry)并且Entry中的key(GC)被回收的话则清除 Start
    ThreadLocalMap.Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);(e = tab[i]) != null; i = nextIndex(i, len)) {//继续往下一个指针移动
        ThreadLocal<?> k = e.get();//获取ThreadLocal
        if (k == null) {//key为空了, 证明触发了GC, 那么这个value永远获取不到, 所以需要清除这个value和槽的值
            e.value = null;//清除value
            tab[i] = null;//清除对应的槽
            size--;//大小递减
        } else {//key(ThreadLocal)不为空, 证明没有触发GC
            int h = k.threadLocalHashCode & (len - 1);//通过hash计算存储位置
            if (h != i) {//如果不相同的话, 说明两个ThreadLocal并不是同一个ThreadLocal
                tab[i] = null;//清空槽

                while (tab[h] != null)//遍历(往前遍历)到h的索引的槽位不为空
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    //从当前staleSlot往前移动, 如果槽中存在值(Entry)并且Entry中的key(GC)被回收的话则清除 End
    return i;
}

cleanSomeSlots(int) 最终调用的也是expungeStaleEntry(int) 方法,cleanSomeSlots(int) 代码如下:

/**
 * 清理槽位
 * @param i 当前位置, 从这个位置的下一个元素开始清理
 * @param n 长度
 * @return true/false
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        ThreadLocalMap.Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ((n >>>= 1) != 0);
    return removed;
}

如果ThreadLocal出现了hash冲突的情况,会做清理动作,而清理动作也是耗时的,所以在使用ThreadLocal时,一定注意set(T) 和remove()同时使用

1.6 get获取数据

get() 代码如下:

/**
 * 获取value。如果没有值的话, 则会使用初始化的值
 * @return 值
 */
public T get() {
    Thread t = Thread.currentThread();//当前线程
    ThreadLocalMap map = getMap(t);//获取对应的ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);//通过ThreadLocal获取Entry
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;//获取value
            return result;
        }
    }
    return setInitialValue();//设置初始值并返回
}

getMap(Thread) 在上述已经分析过了,表示从Thread中获取ThreadLocalMap对象。如果用户没有调用set(T) 函数而是直接使用get() 函数获取,那么就会走到 setInitialValue() 函数获取值,否则是从ThreadLocalMap获取。

1.6.1 getEntry获取指定的Entry

ThreadLocalMap.getEntry(ThreadLocal) 代码如下:

/**
 * 获取Entry
 * @param key key
 * @return Entry
 */
private ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);//计算位置
    ThreadLocalMap.Entry e = table[i];//获取
    if (e != null && e.get() == key)//由于没有链表结构, 当key相等时才是要取的, 否则进入getEntryAfterMiss()方法进一步处理
        return e;
    else
        return getEntryAfterMiss(key, i, e);//进一步处理(Entry为空或者key不等)
}

通过ThreadLocal.threadLocalHashCode 获取对应的数组索引,然后直接从数组中获取Entry,如果没有出现hash冲突,那么此时可以直接返回了;但是出现了hash冲突,会调用 getEntryAfterMiss(ThreadLocal, int, Entry) 方法:

/**
 * 当在hash槽里面没有获取到key则调用该方法
 * @param key key
 * @param i 计算出来的数组中的位置
 * @param e Entry。可能为空
 * @return Entry
 */
private ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> key, int i, ThreadLocalMap.Entry e) {
    ThreadLocalMap.Entry[] tab = table;//数组
    int len = tab.length;//长度

    while (e != null) {
        ThreadLocal<?> k = e.get();//获取key, 这个key可能是null(因为key是弱引用)
        if (k == key)//能找到直接返回
            return e;
        if (k == null)//key是空的, 证明已经被垃圾回收过
            expungeStaleEntry(i);//清空Entry数组的槽位为空或者key为空的数据
        else//其他情况, 重新获取索引, 从table数组中获取数据继续循环
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;//e为空的话直接返回null
}

出现了hash冲突,其实就是往下一个元素上找,找到了就返回,如果出现GC回收了key,那么调用expungeStaleEntry(int) 做清理动作。

有人会有疑问,如果get和set都做了清理动作,那么还存在ThreadLocal内存泄漏吗?

其实还是存在的,比如说这种情况,外部有强引用指向了当前的ThreadLocal,那么GC是不会回收Entry中的key值的,因为外部有强引用,所以不会进入expungeStaleEntry(int) 方法,因为进入这个方法的前提是key被GC回收了,也就是不会做清理动作,所以还是存在内存泄漏的可能的。

1.6.2 setInitialValue设置并获取初始值

如果从当前线程中没有获取的ThreadLocalMap,可能是因为用户在没有调用set(T) 情况下直接调用get() 函数,从而进入setInitialValue() 方法:

/**
 * 设置初始值
 * @return 初始值
 */
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

在setInitialValue() 方法中会调用initialValue() 方法获取值,而 initialValue() 方法在上述介绍时,使用withInitial(Supplier) 静态方法创建的ThreadLocal中有指定,当然用户也可以自己继承ThreadLocal实现自定义的ThreadLocal,然后重新当前的方法获取初始值。

1.7 remove移除数据

remove() 代码如下:

/**
 * 删除操作
 */
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());//获取当前线程关联的ThreadLocalMap对象
    if (m != null)
        m.remove(this);//删除
}

/**
 * ThreadLocal.ThreadLocalMap
 * 删除操作
 * @param key key
 */
private void remove(ThreadLocal<?> key) {
    ThreadLocalMap.Entry[] tab = table;//Entry数组
    int len = tab.length;//长度
    int i = key.threadLocalHashCode & (len - 1);//计算key在数组中的位置
    for (ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();//直接清除弱引用(WeakReference)
            expungeStaleEntry(i);//清除数据
            return;
        }
    }
}

最终调用的是expungeStaleEntry(int) 方法做清理动作。

1.8 小结

ThreadLocal的总结如下:

  • ThreadLocal使用ThreadLocal内部类ThreadLocalMap存储数据,表示线程局部变量;在Thread类中使用threadLocals变量引用ThreadLocal.ThreadLocalMap,所以在每个Thread内部都是单独的ThreadLocal.ThreadLocalMap
  • ThreadLocalMap使用的是ThreadLocalMap.Entry数组作为存储,而Entry数组是WeakReference的子类,弱引用指向的是Entry的key,Entry的key是当前ThreadLocal本身实例(this),value是用户存储的值,弱引用的特殊性,在外部没有强引用的情况下下一次GC会被回收。使用弱引用的目的是在一定程度上防止线程过多和线程执行时间过长导致内存泄漏的情况。
  • ThreadLocal在开发规范中,要做到 set(T) 至少与 remove() 结对编程,并将将remove() 操作放到finally{} 代码块中;如果没有执行remove() 而连续set(T)情况的话,会出现:
    • 内存泄漏,外部强引用指向了ThreadLocal,GC无法回收ThreadLocal,在Thread生命周期不结束的情况下会出现内存泄漏的可能
    • set和get操作更耗时,因为在set和get函数中,当GC清理了ThreadLocal弱引用后,会调用expungeStaleEntry(int) 清理操作,而清理操作本身就是遍历数组耗时的操作
  • 在线程池中,因为同一个线程是一直在消费BlockingQueue任务的,所以如果在set(T) 之后没有调用remove() 方法,那么会存在上一次set(T) 的值被下一个Runnable获取到的可能,所以一定要注意 set(T) 和remove() 结对编程。
0

评论区