赌十包辣条,你一定没见过这么透彻的ThreadLocal讲解

如果转载请声明,转自【https://www.cnblogs.com/andy-songwei/p/12040372.html】,谢谢!

本文的主要内容为:

1、一个生活中的场景

鉴于普罗大众都喜欢看热闹,咱们先来看个热闹再开工吧!

场景一: 

 中午了, 张三、李四和王五一起去食堂大菜吃饭。食堂刚经营不久,还很简陋,负责打菜的只有一位老阿姨。

 张三:我要一份鸡腿。

 李四:我要一份小鸡炖蘑菇。

 张三:我再要一份红烧肉。

 王五:我要一份红烧排骨。

 李四:我不要小鸡炖蘑菇了,换成红烧鲫鱼。

 王五:我再要一份椒盐虾。

 张三:我再要一份梅菜扣肉。

 ......

 张三:我点的红烧肉,为啥给我打红烧鲫鱼?

 李四:我的红烧鲫鱼呢?

 王五:我有点红烧肉吗?

 ......

 李四:我点了15元的菜,为啥扣我20?

 王五:我点了20元的菜,只扣了我15元,赚了,窃喜!

 张三:我已经刷了卡了,怎么还叫我刷卡?

 ......

 老阿姨毕竟上了年纪,不那么利索,这几个小伙子咋咋呼呼,快言快语,老阿姨也被搅晕了,手忙脚乱,忙中出错,这仨小伙也是怨声载道。

 场景二:

 食堂领导看到这个场景,赶紧要求大家排队,一个一个来。后来,老阿姨轻松多了,也没有再犯错了。

 但是,新的问题又来了,打菜的人当中,很多妹子很磨叽,点个菜犹犹豫豫想半天。

 张三:太慢了,我快饿死了!

 李四:再这么慢,下次去别家!

 王五:我等得花儿都谢啦!

 赵六:啥?我点了啥菜,花了多少钱,其它人怎么都知道?是阿姨多嘴了,还是其它人偷偷关注我很久了?太不安全了,一点隐私都没有,以后不来了。

 ......

 场景三:

  领导听到这些怨言,心里很不是滋味,大手一挥:扩大经营,以后为你们每一个人开一个流动窗口并请一位私人阿姨,只为你一个人服务!

  从此,再也没有怨言,阿姨也没有再犯错了,皆大欢喜......

场景一就像多个线程同时去操作一个数据,最终的结果就是混乱。于是出现了同步锁synchronized,同一时刻只运行一个线程操作,就像场景二,大家先来后到排队,混乱的问题解决了。但是此时一个线程在操作的时候,其它线程只能闲等着,而且这些数据是共享的,每个线程希望拥有只能自己操作的私人数据,ThreadLocal就正好满足了这个需求。

所以,相比于synchronized,Threadlocal通过牺牲空间来换取时间和效率。

2、ThreadLocal简介 

ThreadLocal官方的介绍为:

1 /**
2  * This class provides thread-local variables.  These variables differ from
3  * their normal counterparts in that each thread that accesses one (via its
4  * {@code get} or {@code set} method) has its own, independently initialized
5  * copy of the variable.  {@code ThreadLocal} instances are typically private
6  * static fields in classes that wish to associate state with a thread (e.g.,
7  * a user ID or Transaction ID).
8  */

大致意思是:ThreadLocal提供了线程本地变量。这些变量与一般变量相比,其不同之处在于,通过它的get()和set()方法,每个线程可以访问自己独立拥有的初始变量副本。翻译成人话就是,ThreadLocal为每一个线程开辟了一个独立的存储器,只有对应的线程才能够访问其数据,其它线程则无法访问。对应于前文的场景,就像食堂为每一个人安排了一个窗口和专属阿姨为其打菜,这个过程中,这个窗口和阿姨就是其专属的独立的资源,其他人就无从知道他点了什么菜,花了多少钱。

3、ThreadLocal的简单使用示例 

是骡子是马,先拉出来溜溜!先直观看看它的能耐,再来了解它丰富的内心:

 1 // =========实例3.1========
 2 private ThreadLocal<String> mThreadLocal = new ThreadLocal<>();
 3 private void testThreadLocal() throws InterruptedException {
 4     mThreadLocal.set("main-thread");
 5     Log.i("threadlocaldemo", "result-1=" + mThreadLocal.get());
 6     Thread thread_1 = new Thread() {
 7         @Override
 8         public void run() {
 9             super.run();
10             mThreadLocal.set("thread_1");
11             Log.i("threadlocaldemo", "result-2=" + mThreadLocal.get());
12         }
13     };
14     thread_1.start();
15     //该句表示thread_1执行完后才会继续执行
16     thread_1.join();
17     Thread thread_2 = new Thread() {
18         @Override
19         public void run() {
20             super.run();
21             Log.i("threadlocaldemo", "result-3=" + mThreadLocal.get());
22         }
23     };
24     thread_2.start();
25     //该句表示thread_2执行完后才会继续执行
26     thread_2.join();
27     Log.i("threadlocaldemo", "result-4=" + mThreadLocal.get());
28 }

在主线程中调用这个方法,运行结果:

1 12-13 13:42:50.117 25626-25626/com.example.demos I/threadlocaldemo: result-1=main-thread
2 12-13 13:42:50.119 25626-25689/com.example.demos I/threadlocaldemo: result-2=thread_1
3 12-13 13:42:50.119 25626-25690/com.example.demos I/threadlocaldemo: result-3=null
4 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......三步即可:

1 ThreadLocal<T> mThreadLocal = new ThreadLocal<>();
2 mThreadLocal.set(T);
3 mThreadLocal.get();

就是这么方便,就是这么简洁!

4、提供的4个主要接口

ThreadLocal以其使用简单,风格简洁让人一见倾心。它对外提供的接口很少,当前SDK中,主要有4个:

1 public void set(T value) { }
2 public T get() { }
3 public void remove() { }
4 protected T initialValue() { }  

为了保持对这些方法说明的原滋原味,我们直接通过源码中对其的注释说明来认识它们。

(1)set()

 1 /**
 2  * Sets the current thread‘s copy of this thread-local variable
 3  * to the specified value.  Most subclasses will have no need to
 4  * override this method, relying solely on the {@link #initialValue}
 5  * method to set the values of thread-locals.
 6  *
 7  * @param value the value to be stored in the current thread‘s copy of
 8  *        this thread-local.
 9  */
10 public void set(T value)

设置当前线程的ThreadLocal值为指定的value。大部分子类没有必要重写该方法,可以依赖initialValue()方法来设置ThreadLocal的值。

(2)get()

1 /**
2  * Returns the value in the current thread‘s copy of this
3  * thread-local variable.  If the variable has no value for the
4  * current thread, it is first initialized to the value returned
5  * by an invocation of the {@link #initialValue} method.
6  *
7  * @return the current thread‘s value of this thread-local
8  */
9 public T get()

用于获取当前线程所对应的ThreadLocal值。如果当前线程下,该变量没有值,会通过调用initialValue()方法返回的值对其进行初始化。

(3)remove()

 1 /**
 2  * Removes the current thread‘s value for this thread-local
 3  * variable.  If this thread-local variable is subsequently
 4  * {@linkplain #get read} by the current thread, its value will be
 5  * reinitialized by invoking its {@link #initialValue} method,
 6  * unless its value is {@linkplain #set set} by the current thread
 7  * in the interim.  This may result in multiple invocations of the
 8  * {@code initialValue} method in the current thread.
 9  *
10  * @since 1.5
11  */
12  public void remove()

该接口是从JDK1.5开始提供的,用于删除当前线程对应的ThreadLocal值,从而减少内存占用。在同一线程中,如果该方法被调用了,随后再调用get()方法时,会使得initialValue()被调用,从而ThreadLocal的值被重新初始化,除非此时在调用get()前调用了set()来赋值。该方法可能导致initialValue()被多次调用。该方法可以不用显示调用,因为当线程结束后,系统会自动回收线程局部变量值。所以该方法不是必须调用的,只不过显示调用可以加快内存回收。

(4)initialValue()

 1 /**
 2  * Returns the current thread‘s "initial value" for this
 3  * thread-local variable.  This method will be invoked the first
 4  * time a thread accesses the variable with the {@link #get}
 5  * method, unless the thread previously invoked the {@link #set}
 6  * method, in which case the {@code initialValue} method will not
 7  * be invoked for the thread.  Normally, this method is invoked at
 8  * most once per thread, but it may be invoked again in case of
 9  * subsequent invocations of {@link #remove} followed by {@link #get}.
10  *
11  * <p>This implementation simply returns {@code null}; if the
12  * programmer desires thread-local variables to have an initial
13  * value other than {@code null}, {@code ThreadLocal} must be
14  * subclassed, and this method overridden.  Typically, an
15  * anonymous inner class will be used.
16  *
17  * @return the initial value for this thread-local
18  */
19 protected T initialValue() {
20     return null;
21 }

返回当前线程对应的ThreadLocal的初始值。当当前线程是通过get()方法第一次对ThreadLocal进行访问时,该方法将会被调用,除非当前线程之前调用过set()方法,在这种情况下initialValue()方法将不会被当前线程所调用。一般而言,该方法最多只会被每个线程调用一次,除非随后在当前线程中调用remove()方法,然后调用get()方法。该实现会简单地返回null;如果程序员希望ThreadLocal拥有一个初始值,而不是null,ThreadLocal需要定义一个子类,并且在子类中重写initialValue()方法。比较典型的做法是使用一个匿名内部类。该方法由protected修饰,可见其这样设计通常是为了供用户重写,从而自定义初始值。后面会再通过实例来演示该方法的使用。

5、ThreadLocal工作机制

ThreadLocal使用起来非常简单,但它是如何实现为每一个Thread保存一份独立的数据的呢?我们先结合实例3.1来看set()方法都做了些什么:

1 //=========ThreadLocal=======源码5.1
2 public void set(T value) {
3     Thread t = Thread.currentThread();
4     ThreadLocalMap map = getMap(t);
5     if (map != null)
6         map.set(this, value);
7     else
8         createMap(t, value);
9 }

首先就是获取当前的线程,然后根据当前线程来获取一个ThreadLocalMap,如果map不为null,就往map中插入指定值,注意这的key是ThreadLocal实例;如果map为null,就创建一个map。看看第4行getMap(t)做了啥:

 1 //=========ThreadLocal=======源码5.2
 2 /**
 3  * Get the map associated with a ThreadLocal.
 4  * ......
 5  */
 6 ThreadLocalMap getMap(Thread t) {
 7     return t.threadLocals;
 8 }
 9
10 /**
11  * ThreadLocalMap is a customized hash map suitable only for
12  * maintaining thread local values......
13  */
14 static class ThreadLocalMap {
15      ......
16 }
17
18 //==========Thread========
19 ThreadLocal.ThreadLocalMap threadLocals = null;

getMap()返回的是指定线程(也就是当前线程)的threadLocals变量,这个变量是ThreadLocal.ThreadLocalMap类型的,而ThreadLocalMap是一个仅适用于维护线程本地变量值的自定义的HashMap。简单来说,就是返回当前线程下的一个自定义HashMap。

下面我抽取了ThreadLocalMap的部分代码,先来总体上认识它(这里我们不需要读懂其中的每一行代码,知道它里面主要做了哪些事就可以了):

  1 //=========源码5.3========
  2 static class ThreadLocalMap {
  3
  4     static class Entry extends WeakReference<ThreadLocal<?>> {
  5         /** The value associated with this ThreadLocal. */
  6         Object value;
  7
  8         Entry(ThreadLocal<?> k, Object v) {
  9             super(k);
 10             value = v;
 11         }
 12     }
 13
 14     /**
 15      * The initial capacity -- MUST be a power of two.
 16      */
 17     private static final int INITIAL_CAPACITY = 16;
 18
 19     /**
 20      * The table, resized as necessary.
 21      * table.length MUST always be a power of two.
 22      */
 23     private Entry[] table;
 24
 25     /**
 26      * The number of entries in the table.
 27      */
 28     private int size = 0;
 29
 30     /**
 31      * The next size value at which to resize.
 32      */
 33     private int threshold; // Default to 0
 34
 35     /**
 36      * Set the resize threshold to maintain at worst a 2/3 load factor.
 37      */
 38     private void setThreshold(int len) {
 39         threshold = len * 2 / 3;
 40     }
 41
 42     ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
 43         table = new Entry[INITIAL_CAPACITY];
 44         int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
 45         table[i] = new Entry(firstKey, firstValue);
 46         size = 1;
 47         setThreshold(INITIAL_CAPACITY);
 48     }
 49
 50     /**
 51      * Get the entry associated with key.
 52      * ......
 53      */
 54     private Entry getEntry(ThreadLocal<?> key) {
 55         int i = key.threadLocalHashCode & (table.length - 1);
 56         Entry e = table[i];
 57         if (e != null && e.get() == key)
 58             return e;
 59         else
 60             return getEntryAfterMiss(key, i, e);
 61     }
 62
 63     /**
 64      * Set the value associated with key.
 65      * ......
 66      */
 67     private void set(ThreadLocal<?> key, Object value) {
 68
 69         // We don‘t use a fast path as with get() because it is at
 70         // least as common to use set() to create new entries as
 71         // it is to replace existing ones, in which case, a fast
 72         // path would fail more often than not.
 73
 74         Entry[] tab = table;
 75         int len = tab.length;
 76         int i = key.threadLocalHashCode & (len-1);
 77
 78         for (Entry e = tab[i];
 79              e != null;
 80              e = tab[i = nextIndex(i, len)]) {
 81             ThreadLocal<?> k = e.get();
 82
 83             if (k == key) {
 84                 e.value = value;
 85                 return;
 86             }
 87
 88             if (k == null) {
 89                 replaceStaleEntry(key, value, i);
 90                 return;
 91             }
 92         }
 93
 94         tab[i] = new Entry(key, value);
 95         int sz = ++size;
 96         if (!cleanSomeSlots(i, sz) && sz >= threshold)
 97             rehash();
 98     }
 99
100     /**
101      * Remove the entry for key.
102      */
103     private void remove(ThreadLocal<?> key) {
104         Entry[] tab = table;
105         int len = tab.length;
106         int i = key.threadLocalHashCode & (len-1);
107         for (Entry e = tab[i];
108              e != null;
109              e = tab[i = nextIndex(i, len)]) {
110             if (e.get() == key) {
111                 e.clear();
112                 expungeStaleEntry(i);
113                 return;
114             }
115         }
116     }
117
118     /**
119      * Double the capacity of the table.
120      */
121     private void resize() {
122        ......
123     }
124 }

这里面维护了一个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了,

1 //==========ThreadLocal=========源码5.4
2 void createMap(Thread t, T firstValue) {
3     t.threadLocals = new ThreadLocalMap(this, firstValue);
4 }

结合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方法时又做了什么呢?看看其源码:

 1 //=========ThreadLocal======源码5.5
 2 public T get() {
 3     Thread t = Thread.currentThread();
 4     ThreadLocalMap map = getMap(t);
 5     if (map != null) {
 6         ThreadLocalMap.Entry e = map.getEntry(this);
 7         if (e != null) {
 8             @SuppressWarnings("unchecked")
 9             T result = (T)e.value;
10             return result;
11         }
12     }
13     return setInitialValue();
14 }

现在,第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行了。

 1 //==========ThreadLocal========源码5.6
 2 private T setInitialValue() {
 3     T value = initialValue();
 4     Thread t = Thread.currentThread();
 5     ThreadLocalMap map = getMap(t);
 6     if (map != null)
 7         map.set(this, value);
 8     else
 9         createMap(t, value);
10     return value;
11 }
12
13 protected T initialValue() {
14     return null;
15 }
第13行的initialValue()方法,前面介绍过,可以让子类重写,即给ThreadLocal指定初始值;如果没有重写,那返回值就是null。第4~9行前面也介绍过了,使用或者创建map来存入该值。

最后还一个remove()方法

1 //======ThreadLocal======
2 public void remove() {
3     ThreadLocalMap m = getMap(Thread.currentThread());
4     if (m != null)
5         m.remove(this);
6 }

结合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,如下代码所示:

1 //======ActivityTread======源码6.1
2 public static void main(String[] args) {
3       ......
4       Looper.prepareMainLooper();
5       ......
6 }

而在子线程中实例Handler的时候,总是需要显示调用Looper.prepare()方法来为当前线程生成一个Looper对象,以及通过Looper.myLooper()来得到自己线程的Looper来传递给Handler。

Looper中相关的关键源码如下:

 1 //==========Looper========源码6.2
 2
 3 // sThreadLocal.get() will return null unless you‘ve called prepare().
 4 static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
 5 private static Looper sMainLooper;
 6
 7 /**
 8  * Initialize the current thread as a looper, marking it as an
 9  * application‘s main looper. The main looper for your application
10  * is created by the Android environment, so you should never need
11  * to call this function yourself.  See also: {@link #prepare()}
12  */
13 public static void prepareMainLooper() {
14     prepare(false);
15     synchronized (Looper.class) {
16         if (sMainLooper != null) {
17             throw new IllegalStateException("The main Looper has already been prepared.");
18         }
19         sMainLooper = myLooper();
20     }
21 }
22
23 /**
24  * Return the Looper object associated with the current thread.  Returns
25  * null if the calling thread is not associated with a Looper.
26  */
27 public static @Nullable Looper myLooper() {
28     return sThreadLocal.get();
29 }
30
31 /** Initialize the current thread as a looper.
32   * ......
33   */
34 public static void prepare() {
35     prepare(true);
36 }
37 private static void prepare(boolean quitAllowed) {
38     if (sThreadLocal.get() != null) {
39         throw new RuntimeException("Only one Looper may be created per thread");
40     }
41     sThreadLocal.set(new Looper(quitAllowed));
42 }
43
44 /**
45  * Returns the application‘s main looper, which lives in the main thread of the application.
46  */
47 public static Looper getMainLooper() {
48     synchronized (Looper.class) {
49         return sMainLooper;
50     }
51 }

我们可以看到不少ThreadLocal的影子,Looper也正是通过ThreadLocal来为每个线程维护一份Looper实例的。通过我们前文的介绍,这里应该能够轻而易举理解其中的运作机制了吧,这里就再不啰嗦了。

7、实践是检验真理的唯一标准

前面介绍了ThreadLocal提供的四个接口,以及详细讲解了它的工作原理。现在我们将实例3.1做一些修改,将各个接口的功能都包含进来,并稍微增加一点复杂度,如果能够看懂这个实例,就算是真的理解ThreadLocal了。

 1 //=========实例7.1=======
 2 private ThreadLocal<String> mStrThreadLocal = new ThreadLocal<String>() {
 3     @Override
 4     protected String initialValue() {
 5         Log.i("threadlocaldemo", "initialValue");
 6         return "initName";
 7     }
 8 };
 9 private ThreadLocal<Long> mLongThreadLocal = new ThreadLocal<>();
10 private void testThreadLocal() throws InterruptedException {
11     mStrThreadLocal.set("main-thread");
12     mLongThreadLocal.set(Thread.currentThread().getId());
13     Log.i("threadlocaldemo", "result-1:name=" + mStrThreadLocal.get() + ";id=" + mLongThreadLocal.get());
14     Thread thread_1 = new Thread() {
15         @Override
16         public void run() {
17             super.run();
18             mStrThreadLocal.set("thread_1");
19             mLongThreadLocal.set(Thread.currentThread().getId());
20             Log.i("threadlocaldemo", "result-2:name=" + mStrThreadLocal.get() + ";id=" + mLongThreadLocal.get());
21         }
22     };
23     thread_1.start();
24     //该句表示thread_1执行完后才会继续执行
25     thread_1.join();
26     Thread thread_2 = new Thread() {
27         @Override
28         public void run() {
29             super.run();
30             Log.i("threadlocaldemo", "result-3:name=" + mStrThreadLocal.get() + ";id=" + mLongThreadLocal.get());
31         }
32     };
33     thread_2.start();
34     //该句表示thread_2执行完后才会继续执行
35     thread_2.join();
36     mStrThreadLocal.remove();
37     Log.i("threadlocaldemo", "result-4:name=" + mStrThreadLocal.get() + ";id=" + mLongThreadLocal.get());
38 }

在主线程中运行该方法,执行结果为:

1 12-14 16:25:40.662 4844-4844/com.example.demos I/threadlocaldemo: result-1:name=main-thread;id=2
2 12-14 16:25:40.668 4844-5351/com.example.demos I/threadlocaldemo: result-2:name=thread_1;id=926
3 12-14 16:25:40.669 4844-5353/com.example.demos I/threadlocaldemo: initialValue
4 12-14 16:25:40.669 4844-5353/com.example.demos I/threadlocaldemo: result-3:name=initName;id=null
5 12-14 16:25:40.669 4844-4844/com.example.demos I/threadlocaldemo: initialValue
6 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

时间: 2024-08-04 05:30:58

赌十包辣条,你一定没见过这么透彻的ThreadLocal讲解的相关文章

赌一包辣条,这10个java错误你绝对犯过!

Java十大低级错误互联网发展如此迅猛,编程语言不断壮大,java作为互联网的一份子,在编程语言中有很重的分量.随着学java的人越来越多,技术也越来越参差不齐,以下是我归纳10个java常见错误!1.方法和变量命名随意而不规范,没有类注释.方法注释或注释不规范,代码修改后,不同步修改注释,导致注释与代码不符. 2.数据类不重载toString()方法,编程规范要求"所有的数据类必须重载toString() 方法,返回该类有意义的内容." 3.对方法的调用不进行空指针判断而造成空指针异

你没见过吧?16款形态各异的加载进度条设计

互联网连接越来越快,但难免有一些时刻需要我们等待.在这种情况下,创意的设计师尽力减轻用户等待的痛苦,苦思敏想设计各种创意的进度条(或加载条)效果 ,让用户等待的过程变得更加愉悦. 您可能感兴趣的相关文章 22套 Web & Mobile PSD 用户界面素材 45套精美的手机界面设计素材和设计工具 分享30套精美的Web和手机开发UI素材 60个精美的免费移动开发PSD素材资源 45套新鲜出炉的精美 PSD 网页设计素材 Loading by pearlsomani Flat Loading B

有多久没见你

有多久没见你 以为你在那里 原来就住在我的心底 陪伴着我的呼吸 有多远的距离 以为闻不到你的气息 谁知道你背影这么长 回头就看到你

java学习笔记-尚硅谷0918班宋*:我想说我已经好久没见过这样的老师了

浑浑噩噩的睡过大学,昏昏沉沉的工作四年,仿佛从离开高中学校的那一天就进入了休眠状态.来尚硅谷java培训之前纠结了很久,有理解也有不支持,对于我来说学习编程不只是为了就业,很重要的一点是我在寻找一种能让自己坚持做的事情,而这种坚持必源于热爱.一点不矫情的讲,我选择学习来北京java培训学些java编程是因为我喜欢. 从基础阶段来看,学校做的很好,宋老师讲课幽默.生动.专业功底扎实,这些都是显而易见的:更可贵的是宋老师的教师作风,例如会叫睡觉的同学站起来,我想说我已经好久没见过这样的老师了(偷笑)

“没见过他这样的人”

今天加完班到地铁里晚上九点半了,地铁里人还很多,我挤到角落位置安静的站着. 几个站之后一对老年夫妇挤到了我身边.我听到他俩的对话: “年轻人都玩手机.” “嗯.” “没见过他这样的人.” “是啊,在地铁里看书,手上还拿笔划划,写写.” …… 我听到这两老人的对话很好奇,他们在嘀咕谁呢? 我抬头扫视了一圈,没看到有谁在地铁里看书的,也没看到地铁里看书还拿笔做笔记的! 我也没见过这样的人! 接着看书,做笔记…… Today, after work to the subway is about hal

2019左其盛好书榜,没见过更好的榜单(截至3月31日)

各位书友如果看到更好的好书榜,欢迎向我推荐. 见了不少好书榜,没见过比我这个更好的.以下4条大部分好书榜一条都做不到: 1:我这个好书榜中的书都是我认真读过的: 2:给出具体的星级,好书又分了3.5星.4星.4.5星.5星四个档次: 3:我有过多年的技术类.管理类.产品类岗位的工作经验,市面上其他榜单有不少是专职编辑制定的: 4:除了好书榜之外,我还有差评榜(为此当然会得罪许多人). 此外其实还有一条就是制榜者的洞察力,不过这一条相对比较虚我就不说了. 各位书友如果看到更好的好书榜,欢迎向我推荐

放电见什那大马保交打十包现相业权rjhf

來料話已最條何標調們酸素流米天黨被計研教然百物養科壓住十段邊采應電西機何勞老了利強把的何任器國場而現深五形小路此南事電放象是千色分狀當件身圓育國種民通相響形方你造運馬然行完常石會車電張道熱但求況門無內流麼頭命建纔員可寫真們特政二主果道商於表前習省驗米計采號則收調加克證許中具參相乾集積江們進每發想王然局農應滿必性邊度不新寫其再廠全習關做理現纔比任標布候利引治起於那收需作准元近七些林領油制日生定成主可北金天達種非值離等據九被時道我稱線家明走制卻通線很縣回界九高應業質劃須裝查見麼議有至性常速能步團新切

吐槽Windows 8,就没见过这么烂的平板操作系统

本文带有严重个人情感色彩,反感者慎入. CSDN 博文大赛得了个奖品,联想的平板电脑, MIIX2 8 ,系统是 Windows 8 . 今天媳妇再次使用它,惹得我再次吐槽. 一句话,Windows 8 这系统设计得,简直不是给人用的,它是真心给你找麻烦的. 举几个例子吧. 关于账号 作为一个平板电脑,为毛非要我设置一个帐号?我是电脑用户吗?我需要吗?你真觉得我需要吗?你为什么要强奸我呢? 我是有可能需要一个账号,可为什么不能默认不用帐号就可以使用,让有需要的人到"高级"设置中去为安全

偶然看到的面试算法题_最短时间找出十包粉末中的两蓝粉末。

题目:有4个杯子,10包粉末,其中有2包溶于水变蓝,其余无色,粉末溶于水2min才能显现颜色.求找出两包蓝色粉末的最短时间.假设水和粉末用不完. 解:以下给出四种解法,标记10包粉末为(1,2 ... ) 杯子为[1,2,3,4]首先我想会不会是有某种算法,dp 二分..@[email protected]..没有,懵懵的. 法一:这是我最初想到的比较傻的方法 第一趟:[12,34,56,78] 每个杯子分别放两包加水融化,剩下两包不管.可能的情况: (1)0个杯子变色,说明剩下两包就是蓝粉末