如果转载请声明, 转自[https://www.cnblogs.com/andy-songwei/p/12040372.html] , 谢谢!
本文的主要内容为:
1, 一个生活中的场景
鉴于普罗大众都喜欢看热闹, 咱们先来看个热闹再开工吧!
场景一:
中午了, 张三, 李四和王五一起去食堂大菜吃饭. 食堂刚经营不久, 还很简陋, 负责打菜的只有一位老阿姨.
张三: 我要一份鸡腿.
李四: 我要一份小鸡炖蘑菇.
张三: 我再要一份红烧肉.
王五: 我要一份红烧排骨.
李四: 我不要小鸡炖蘑菇了, 换成红烧鲫鱼.
王五: 我再要一份椒盐虾.
张三: 我再要一份梅菜扣肉.
......
张三: 我点的红烧肉, 为啥给我打红烧鲫鱼?
李四: 我的红烧鲫鱼呢?
王五: 我有点红烧肉吗?
......
李四: 我点了 15 元的菜, 为啥扣我 20?
王五: 我点了 20 元的菜, 只扣了我 15 元, 赚了, 窃喜!
张三: 我已经刷了卡了, 怎么还叫我刷卡?
......
老阿姨毕竟上了年纪, 不那么利索, 这几个小伙子咋咋呼呼, 快言快语, 老阿姨也被搅晕了, 手忙脚乱, 忙中出错, 这仨小伙也是怨声载道.
场景二:
食堂领导看到这个场景, 赶紧要求大家排队, 一个一个来. 后来, 老阿姨轻松多了, 也没有再犯错了.
但是, 新的问题又来了, 打菜的人当中, 很多妹子很磨叽, 点个菜犹犹豫豫想半天.
张三: 太慢了, 我快饿死了!
李四: 再这么慢, 下次去别家!
王五: 我等得花儿都谢啦!
赵六: 啥? 我点了啥菜, 花了多少钱, 其它人怎么都知道? 是阿姨多嘴了, 还是其它人偷偷关注我很久了? 太不安全了, 一点隐私都没有, 以后不来了.
......
场景三:
领导听到这些怨言, 心里很不是滋味, 大手一挥: 扩大经营, 以后为你们每一个人开一个流动窗口并请一位私人阿姨, 只为你一个人服务!
从此, 再也没有怨言, 阿姨也没有再犯错了, 皆大欢喜......
场景一就像多个线程同时去操作一个数据, 最终的结果就是混乱. 于是出现了同步锁 synchronized, 同一时刻只运行一个线程操作, 就像场景二, 大家先来后到排队, 混乱的问题解决了. 但是此时一个线程在操作的时候, 其它线程只能闲等着, 而且这些数据是共享的, 每个线程希望拥有只能自己操作的私人数据, ThreadLocal 就正好满足了这个需求.
所以, 相比于 synchronized,Threadlocal 通过牺牲空间来换取时间和效率.
2,ThreadLocal 简介
ThreadLocal 官方的介绍为:
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*/
大致意思是: ThreadLocal 提供了线程本地变量. 这些变量与一般变量相比, 其不同之处在于, 通过它的 get()和 set()方法, 每个线程可以访问自己独立拥有的初始变量副本. 翻译成人话就是, ThreadLocal 为每一个线程开辟了一个独立的存储器, 只有对应的线程才能够访问其数据, 其它线程则无法访问. 对应于前文的场景, 就像食堂为每一个人安排了一个窗口和专属阿姨为其打菜, 这个过程中, 这个窗口和阿姨就是其专属的独立的资源, 其他人就无从知道他点了什么菜, 花了多少钱.
3,ThreadLocal 的简单使用示例
是骡子是马, 先拉出来溜溜! 先直观看看它的能耐, 再来了解它丰富的内心:
- // ========= 实例 3.1========
- private ThreadLocal<String> mThreadLocal = new ThreadLocal<>();
- private void testThreadLocal() throws InterruptedException {
- mThreadLocal.set("main-thread");
- Log.i("threadlocaldemo", "result-1=" + mThreadLocal.get());
- Thread thread_1 = new Thread() {
- @Override
- public void run() {
- super.run();
- mThreadLocal.set("thread_1");
- Log.i("threadlocaldemo", "result-2=" + mThreadLocal.get());
- }
- };
- thread_1.start();
- // 该句表示 thread_1 执行完后才会继续执行
- thread_1.join();
- Thread thread_2 = new Thread() {
- @Override
- public void run() {
- super.run();
- Log.i("threadlocaldemo", "result-3=" + mThreadLocal.get());
- }
- };
- thread_2.start();
- // 该句表示 thread_2 执行完后才会继续执行
- thread_2.join();
- Log.i("threadlocaldemo", "result-4=" + mThreadLocal.get());
- }
在主线程中调用这个方法, 运行结果:
- 12-13 13:42:50.117 25626-25626/com.example.demos I/threadlocaldemo: result-1=main-thread
- 12-13 13:42:50.119 25626-25689/com.example.demos I/threadlocaldemo: result-2=thread_1
- 12-13 13:42:50.119 25626-25690/com.example.demos I/threadlocaldemo: result-3=null
- 12-13 13:42:50.120 25626-25626/com.example.demos I/threadlocaldemo: result-4=main-thread
看到这个结果会不会惊掉下巴呢? 明明在第 9 行中 set 了值, 第 10 行中也得到了对应的值, 但第 20 行的 get 得到的却是 null, 第 26 行得到的是第 3 行 set 的值. 这就是 ThreadLocal 的神奇功效, 主线程 set 的值, 只能在主线程 get 到; thread_1 内部 set 的值, thread_1 中才能 get;thread_2 中没有 set, 所以 get 到的就是 null.
而实现这, 不要 999, 也不要 99, 只要 3...... 三步即可:
- ThreadLocal<T> mThreadLocal = new ThreadLocal<>();
- mThreadLocal.set(T);
- mThreadLocal.get();
就是这么方便, 就是这么简洁!
4, 提供的 4 个主要接口
ThreadLocal 以其使用简单, 风格简洁让人一见倾心. 它对外提供的接口很少, 当前 SDK 中, 主要有 4 个:
- public void set(T value) { }
- public T get() { }
- public void remove() { }
- protected T initialValue() { }
为了保持对这些方法说明的原滋原味, 我们直接通过源码中对其的注释说明来认识它们.
- (1)set()
- /**
- * Sets the current thread's copy of this thread-local variable
- * to the specified value. Most subclasses will have no need to
- * override this method, relying solely on the {@link #initialValue}
- * method to set the values of thread-locals.
- *
- * @param value the value to be stored in the current thread's copy of
- * this thread-local.
- */
- public void set(T value)
设置当前线程的 ThreadLocal 值为指定的 value. 大部分子类没有必要重写该方法, 可以依赖 initialValue()方法来设置 ThreadLocal 的值.
- (2)get()
- /**
- * Returns the value in the current thread's copy of this
- * thread-local variable. If the variable has no value for the
- * current thread, it is first initialized to the value returned
- * by an invocation of the {@link #initialValue} method.
- *
- * @return the current thread's value of this thread-local
- */
- public T get()
用于获取当前线程所对应的 ThreadLocal 值. 如果当前线程下, 该变量没有值, 会通过调用 initialValue()方法返回的值对其进行初始化.
- (3)remove()
- /**
- * Removes the current thread's value for this thread-local
- * variable. If this thread-local variable is subsequently
- * {@linkplain #get read} by the current thread, its value will be
- * reinitialized by invoking its {@link #initialValue} method,
- * unless its value is {@linkplain #set set} by the current thread
- * in the interim. This may result in multiple invocations of the
- * {@code initialValue} method in the current thread.
- *
- * @since 1.5
- */
- public void remove()
该接口是从 JDK1.5 开始提供的, 用于删除当前线程对应的 ThreadLocal 值, 从而减少内存占用. 在同一线程中, 如果该方法被调用了, 随后再调用 get()方法时, 会使得 initialValue()被调用, 从而 ThreadLocal 的值被重新初始化, 除非此时在调用 get()前调用了 set()来赋值. 该方法可能导致 initialValue()被多次调用. 该方法可以不用显示调用, 因为当线程结束后, 系统会自动回收线程局部变量值. 所以该方法不是必须调用的, 只不过显示调用可以加快内存回收.
- (4)initialValue()
- /**
- * Returns the current thread's"initial value" for this
- * thread-local variable. This method will be invoked the first
- * time a thread accesses the variable with the {@link #get}
- * method, unless the thread previously invoked the {@link #set}
- * method, in which case the {@code initialValue} method will not
- * be invoked for the thread. Normally, this method is invoked at
- * most once per thread, but it may be invoked again in case of
- * subsequent invocations of {@link #remove} followed by {@link #get}.
- *
- * <p>This implementation simply returns {@code null}; if the
- * programmer desires thread-local variables to have an initial
- * value other than {@code null}, {@code ThreadLocal} must be
- * subclassed, and this method overridden. Typically, an
- * anonymous inner class will be used.
- *
- * @return the initial value for this thread-local
- */
- protected T initialValue() {
- return null;
- }
返回当前线程对应的 ThreadLocal 的初始值. 当当前线程是通过 get()方法第一次对 ThreadLocal 进行访问时, 该方法将会被调用, 除非当前线程之前调用过 set()方法, 在这种情况下 initialValue()方法将不会被当前线程所调用. 一般而言, 该方法最多只会被每个线程调用一次, 除非随后在当前线程中调用 remove()方法, 然后调用 get()方法. 该实现会简单地返回 null; 如果程序员希望 ThreadLocal 拥有一个初始值, 而不是 null,ThreadLocal 需要定义一个子类, 并且在子类中重写 initialValue()方法. 比较典型的做法是使用一个匿名内部类. 该方法由 protected 修饰, 可见其这样设计通常是为了供用户重写, 从而自定义初始值. 后面会再通过实例来演示该方法的使用.
5,ThreadLocal 工作机制
ThreadLocal 使用起来非常简单, 但它是如何实现为每一个 Thread 保存一份独立的数据的呢? 我们先结合实例 3.1 来看 set()方法都做了些什么:
- //=========ThreadLocal======= 源码 5.1
- 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, 如果 map 不为 null, 就往 map 中插入指定值, 注意这的 key 是 ThreadLocal 实例; 如果 map 为 null, 就创建一个 map. 看看第 4 行 getMap(t)做了啥:
- //=========ThreadLocal======= 源码 5.2
- /**
- * Get the map associated with a ThreadLocal.
- * ......
- */
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
- /**
- * ThreadLocalMap is a customized hash map suitable only for
- * maintaining thread local values......
- */
- static class ThreadLocalMap {
- ......
- }
- //==========Thread========
- ThreadLocal.ThreadLocalMap threadLocals = null;
getMap()返回的是指定线程 (也就是当前线程) 的 threadLocals 变量, 这个变量是 ThreadLocal.ThreadLocalMap 类型的, 而 ThreadLocalMap 是一个仅适用于维护线程本地变量值的自定义的 HashMap. 简单来说, 就是返回当前线程下的一个自定义 HashMap.
下面我抽取了 ThreadLocalMap 的部分代码, 先来总体上认识它(这里我们不需要读懂其中的每一行代码, 知道它里面主要做了哪些事就可以了):
- //========= 源码 5.3========
- 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;
- }
- }
- /**
- * The initial capacity -- MUST be a power of two.
- */
- private static final int INITIAL_CAPACITY = 16;
- /**
- * The table, resized as necessary.
- * table.length MUST always be a power of two.
- */
- private Entry[] table;
- /**
- * The number of entries in the table.
- */
- private int size = 0;
- /**
- * The next size value at which to resize.
- */
- private int threshold; // Default to 0
- /**
- * Set the resize threshold to maintain at worst a 2/3 load factor.
- */
- private void setThreshold(int len) {
- threshold = len * 2 / 3;
- }
- ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
- table = new Entry[INITIAL_CAPACITY];
- int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
- table[i] = new Entry(firstKey, firstValue);
- size = 1;
- setThreshold(INITIAL_CAPACITY);
- }
- /**
- * Get the entry associated with key.
- * ......
- */
- private Entry getEntry(ThreadLocal<?> key) {
- int i = key.threadLocalHashCode & (table.length - 1);
- Entry e = table[i];
- if (e != null && e.get() == key)
- return e;
- else
- return getEntryAfterMiss(key, i, e);
- }
- /**
- * Set the value associated with key.
- * ......
- */
- private void set(ThreadLocal<?> key, Object value) {
- // We don't use a fast path as with get() because it is at
- // least as common to use set() to create new entries as
- // it is to replace existing ones, in which case, a fast
- // path would fail more often than not.
- Entry[] tab = table;
- int len = tab.length;
- int i = key.threadLocalHashCode & (len-1);
- for (Entry e = tab[i];
- e != null;
- e = tab[i = nextIndex(i, len)]) {
- ThreadLocal<?> k = e.get();
- if (k == key) {
- e.value = value;
- return;
- }
- if (k == null) {
- replaceStaleEntry(key, value, i);
- return;
- }
- }
- tab[i] = new Entry(key, value);
- int sz = ++size;
- if (!cleanSomeSlots(i, sz) && sz>= threshold)
- rehash();
- }
- /**
- * Remove the entry for key.
- */
- private void remove(ThreadLocal<?> key) {
- Entry[] tab = table;
- int len = tab.length;
- int i = key.threadLocalHashCode & (len-1);
- for (Entry e = tab[i];
- e != null;
- e = tab[i = nextIndex(i, len)]) {
- if (e.get() == key) {
- e.clear();
- expungeStaleEntry(i);
- return;
- }
- }
- }
- /**
- * Double the capacity of the table.
- */
- private void resize() {
- ......
- }
- }
- View Code
这里面维护了一个 Entry[] table 数组, 初始容量为 16, 当数据超过当前容量的 2/3 时, 就开始扩容, 容量增大一倍. 每一个 Entry 的 K 为 ThreadLocal 对象, V 为要存储的值. 每一个 Entry 在数组中的位置, 是根据其 K(即 ThreadLocal 对象)的 hashCode & (len - 1)来确定, 如第 44 行所示, 这里 K 的 hashCode 是系统给出的一个算法计算得到的. 如果碰到 K 的 hashCode 值相同, 即 hash 碰撞的场景, 会采用尾插法形成链表. 当对这个 map 进行 set,get,remove 操作的时候, 也是通过 K 的 hashCode 来确定该 Entry 在 table 中的位置的, 采用 hashCode 来查找数据, 效率比较高. 这也是 HashMap 底层实现的基本原理, 如果研究过 HashMap 源码, 这段代码就应该比较容易理解了.
继续看源码 5.1, 第一次调用的时候, 显然 map 应该是 null, 就要执行第 8 行 createMap 了,
- //==========ThreadLocal========= 源码 5.4
- void createMap(Thread t, T firstValue) {
- t.threadLocals = new ThreadLocalMap(this, firstValue);
- }
结合 ThreadLocalMap 源码第 41 行的构造方法, 就清楚了这个方法创建了一个 ThreadLocalMap 对象, 并存储了一个 Entry < 当前的 ThreadLocal 对象, value>. 此时, 在当前的线程下拥有了一个 ThreadLocalMap, 这个 ThreadLocalMap 中维护了一个容量为 16 的 table,table 中存储了一个以当前的 ThreadLocal 对象为 K,value 值为 V 的 Entry.Thread,ThreadLocalMap,ThreadLocal,Entry 之间的关系可以表示为下图:
图 5.1
而如果当前 Thread 的 map 已经存在了, 源码 5.1 就会执行第 6 行了, 进而执行 ThreadLocalMap 中的 set 方法. 结合前面对 ThreadLocalMap 的介绍, 想必这个 set 方法也容易理解了, 大致过程是:
1)根据 Thread 找到 map;
2)通过传入的 this(即 ThreadLocal 对象), 得到 hashCode;
3)根据 hashCode & (len - 1)确定对应 Entry 在 table 中的位置;
4)如果该 Entry 存在, 则替换 Value, 否则新建(ThreadLocalMap 源码第 78~92 行表示在具有相同 hashCode 的 Entry 链表上找到对应的 Entry, 这和 hash 碰撞有关).
在调用 ThreadLocal 的 get 方法时又做了什么呢? 看看其源码:
- //=========ThreadLocal====== 源码 5.5
- 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();
- }
现在, 第 12 行及以前的代码应该很容易理解了, 结合 ThreadLocalMap 中的 get 源码, 我们再梳理一下:
1)根据 Thread 找到自己的 map;
2)在 map 中通过 this(即 ThreadLocal 对象)得到 hashCode;
3)通过 hashCode & (len-1)找到对应 Entry 在 table 中的位置;
4)返回 Entry 的 value.
而如果 map 为 null, 或者在 map 中找到的 Entry 为 null, 那么就执行第 20 行了.
- //==========ThreadLocal======== 源码 5.6
- 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;
- }
- protected T initialValue() {
- return null;
- }
第 13 行的 initialValue()方法, 前面介绍过, 可以让子类重写, 即给 ThreadLocal 指定初始值; 如果没有重写, 那返回值就是 null. 第 4~9 行前面也介绍过了, 使用或者创建 map 来存入该值.
最后还一个 remove()方法
- //======ThreadLocal======
- public void remove() {
- ThreadLocalMap m = getMap(Thread.currentThread());
- if (m != null)
- m.remove(this);
- }
结合 ThrealLocalMap 中的 remove 方法, 完成对 ThreadLocal 值的删除. 其大致流程为:
1)根据当前 Thread 找到其 map;
2)根据 ThreadLocal 对象得到 hashCode;
3)通过 hashCode & (len -1)找到在 table 中的位置;
4)在 table 中查找对应的 Entry, 如果存在则删除.
总结: 通过对提供的 4 个接口方法的分析, 我们应该就能清楚了, ThreadLocal 之所以能够为每一个线程维护一个副本, 是因为每个线程都拥有一个 map, 这个 map 就是每个线程的专属空间. 也就是存在下面的关系图(不用怀疑, 该图和图 5.1 相比, 只是少了容量大小):
结合这一节对 ThreadLocal 机制的介绍, 实例 3.1 执行后的就存在如下的数据结构了:
6,ThreadLocal 在 Looper 中的使用
ThreadLocal 在系统源码中有很多地方使用, 最典型的地方就是 Handler 的 Looper 中了. 这里结合 Looper 中的源码, 来了解一下 ThreadLocal 在系统源码中的使用.
我们知道, 在一个 App 进程启动的时候, 会在 ActiivtyThread 类的 main 方法, 也就是 App 的入口方法中, 会为主线程准备一个 Looper, 如下代码所示:
- //======ActivityTread====== 源码 6.1
- public static void main(String[] args) {
- ......
- Looper.prepareMainLooper();
- ......
- }
而在子线程中实例 Handler 的时候, 总是需要显示调用 Looper.prepare()方法来为当前线程生成一个 Looper 对象, 以及通过 Looper.myLooper()来得到自己线程的 Looper 来传递给 Handler.
Looper 中相关的关键源码如下:
- //==========Looper======== 源码 6.2
- // sThreadLocal.get() will return null unless you've called prepare().
- static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
- private static Looper sMainLooper;
- /**
- * Initialize the current thread as a looper, marking it as an
- * application's main looper. The main looper for your application
- * is created by the Android environment, so you should never need
- * to call this function yourself. See also: {@link #prepare()}
- */
- public static void prepareMainLooper() {
- prepare(false);
- synchronized (Looper.class) {
- if (sMainLooper != null) {
- throw new IllegalStateException("The main Looper has already been prepared.");
- }
- sMainLooper = myLooper();
- }
- }
- /**
- * Return the Looper object associated with the current thread. Returns
- * null if the calling thread is not associated with a Looper.
- */
- public static @Nullable Looper myLooper() {
- return sThreadLocal.get();
- }
- /** Initialize the current thread as a looper.
- * ......
- */
- public static void prepare() {
- prepare(true);
- }
- private static void prepare(boolean quitAllowed) {
- if (sThreadLocal.get() != null) {
- throw new RuntimeException("Only one Looper may be created per thread");
- }
- sThreadLocal.set(new Looper(quitAllowed));
- }
- /**
- * Returns the application's main looper, which lives in the main thread of the application.
- */
- public static Looper getMainLooper() {
- synchronized (Looper.class) {
- return sMainLooper;
- }
- }
我们可以看到不少 ThreadLocal 的影子, Looper 也正是通过 ThreadLocal 来为每个线程维护一份 Looper 实例的. 通过我们前文的介绍, 这里应该能够轻而易举理解其中的运作机制了吧, 这里就再不啰嗦了.
7, 实践是检验真理的唯一标准
前面介绍了 ThreadLocal 提供的四个接口, 以及详细讲解了它的工作原理. 现在我们将实例 3.1 做一些修改, 将各个接口的功能都包含进来, 并稍微增加一点复杂度, 如果能够看懂这个实例, 就算是真的理解 ThreadLocal 了.
- //========= 实例 7.1=======
- private ThreadLocal<String> mStrThreadLocal = new ThreadLocal<String>() {
- @Override
- protected String initialValue() {
- Log.i("threadlocaldemo", "initialValue");
- return "initName";
- }
- };
- private ThreadLocal<Long> mLongThreadLocal = new ThreadLocal<>();
- private void testThreadLocal() throws InterruptedException {
- mStrThreadLocal.set("main-thread");
- mLongThreadLocal.set(Thread.currentThread().getId());
- Log.i("threadlocaldemo", "result-1:name=" + mStrThreadLocal.get() + ";id=" + mLongThreadLocal.get());
- Thread thread_1 = new Thread() {
- @Override
- public void run() {
- super.run();
- mStrThreadLocal.set("thread_1");
- mLongThreadLocal.set(Thread.currentThread().getId());
- Log.i("threadlocaldemo", "result-2:name=" + mStrThreadLocal.get() + ";id=" + mLongThreadLocal.get());
- }
- };
- thread_1.start();
- // 该句表示 thread_1 执行完后才会继续执行
- thread_1.join();
- Thread thread_2 = new Thread() {
- @Override
- public void run() {
- super.run();
- Log.i("threadlocaldemo", "result-3:name=" + mStrThreadLocal.get() + ";id=" + mLongThreadLocal.get());
- }
- };
- thread_2.start();
- // 该句表示 thread_2 执行完后才会继续执行
- thread_2.join();
- mStrThreadLocal.remove();
- Log.i("threadlocaldemo", "result-4:name=" + mStrThreadLocal.get() + ";id=" + mLongThreadLocal.get());
- }
在主线程中运行该方法, 执行结果为:
- 12-14 16:25:40.662 4844-4844/com.example.demos I/threadlocaldemo: result-1:name=main-thread;id=2
- 12-14 16:25:40.668 4844-5351/com.example.demos I/threadlocaldemo: result-2:name=thread_1;id=926
- 12-14 16:25:40.669 4844-5353/com.example.demos I/threadlocaldemo: initialValue
- 12-14 16:25:40.669 4844-5353/com.example.demos I/threadlocaldemo: result-3:name=initName;id=null
- 12-14 16:25:40.669 4844-4844/com.example.demos I/threadlocaldemo: initialValue
- 12-14 16:25:40.669 4844-4844/com.example.demos I/threadlocaldemo: result-4:name=initName;id=2
此时存在的数据结构为:
对于这份 log 和数据结构图, 这里就不再一一讲解了, 如果前面都看懂了, 这些都是小菜一碟.
结语
对 ThreadLocal 的讲解这里就结束了, 能读到这里, 也足以说明你是人才, 一定前途无量, 祝你好运, 早日走上人生巅峰!
由于经验和水平有限, 有描述不当或不准确的地方, 还请不吝赐教, 谢谢!
来源: https://www.cnblogs.com/andy-songwei/p/12040372.html