ThreadLocal夺命11连问

前言

前一段时间,有同事使用ThreadLocal踩坑了,正好引起了我的兴趣。

所以近期,我抽空把ThreadLocal的源码再研究了一下,越看越有意思,发现里面的东西还真不少。

我把精华浓缩了一下,汇集成了下面11个问题,看看你能顶住第几个?213aac0ffd7c1bf3f9f6d25112b18e08.png

1. 为什么要用ThreadLocal?

并发编程是一项非常重要的技术,它让我们的程序变得更加高效。

但在并发的场景中,如果有多个线程同时修改公共变量,可能会出现线程安全问题,即该变量最终结果可能出现异常。

为了解决线程安全问题,JDK出现了很多技术手段,比如:使用synchronizedLock,给访问公共资源的代码上锁,保证了代码的原子性

但在高并发的场景中,如果多个线程同时竞争一把锁,这时会存在大量的锁等待,可能会浪费很多时间,让系统的响应时间一下子变慢。

因此,JDK还提供了另外一种用空间换时间的新思路:ThreadLocal

它的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。

例如:

@Service
public class ThreadLocalService {private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();public void add() {threadLocal.set(1);doSamething();Integer integer = threadLocal.get();}
}

2. ThreadLocal的原理是什么?

为了搞清楚ThreadLocal的底层实现原理,我们不得不扒一下源码。

ThreadLocal的内部有一个静态的内部类叫:ThreadLocalMap

public class ThreadLocal<T> {...public T get() {//获取当前线程Thread t = Thread.currentThread();//获取当前线程的成员变量ThreadLocalMap对象ThreadLocalMap map = getMap(t);if (map != null) {//根据threadLocal对象从map中获取Entry对象ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")//获取保存的数据T result = (T)e.value;return result;}}//初始化数据return setInitialValue();}private T setInitialValue() {//获取要初始化的数据T value = initialValue();//获取当前线程Thread t = Thread.currentThread();//获取当前线程的成员变量ThreadLocalMap对象ThreadLocalMap map = getMap(t);//如果map不为空if (map != null)//将初始值设置到map中,key是this,即threadLocal对象,value是初始值map.set(this, value);else//如果map为空,则需要创建新的map对象createMap(t, value);return value;}public void set(T value) {//获取当前线程Thread t = Thread.currentThread();//获取当前线程的成员变量ThreadLocalMap对象ThreadLocalMap map = getMap(t);//如果map不为空if (map != null)//将值设置到map中,key是this,即threadLocal对象,value是传入的value值map.set(this, value);else//如果map为空,则需要创建新的map对象createMap(t, value);}static class ThreadLocalMap {...}...
}

ThreadLocalget方法、set方法和setInitialValue方法,其实最终操作的都是ThreadLocalMap类中的数据。

其中ThreadLocalMap类的内部如下:

static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}...private Entry[] table;...
}

ThreadLocalMap里面包含一个静态的内部类Entry,该类继承于WeakReference类,说明Entry是一个弱引用。

ThreadLocalMap内部还包含了一个Entry数组,其中:Entry = ThreadLocal + value

ThreadLocalMap被定义成了Thread类的成员变量。

public class Thread implements Runnable {...ThreadLocal.ThreadLocalMap threadLocals = null;
}

下面用一张图从宏观上,认识一下ThreadLocal的整体结构:864f7dab186223a71c404ba3e7fb916f.png从上图中看出,在每个Thread类中,都有一个ThreadLocalMap的成员变量,该变量包含了一个Entry数组,该数组真正保存了ThreadLocal类set的数据。

Entry是由threadLocal和value组成,其中threadLocal对象是弱引用,在GC的时候,会被自动回收。而value就是ThreadLocal类set的数据。

下面用一张图总结一下引用关系:d8d300c2416fce6b944ea84943624e0e.png上图中除了Entry的key对ThreadLocal对象是弱引用,其他的引用都是强引用

需要特别说明的是,上图中ThreadLocal对象我画到了堆上,其实在实际的业务场景中不一定在堆上。因为如果ThreadLocal被定义成了static的,ThreadLocal的对象是类共用的,可能出现在方法区。

3. 为什么用ThreadLocal做key?

不知道你有没有思考过这样一个问题:ThreadLocalMap为什么要用ThreadLocal做key,而不是用Thread做key?

如果在你的应用中,一个线程中只使用了一个ThreadLocal对象,那么使用Thread做key也未尝不可。

@Service
public class ThreadLocalService {private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
}

但实际情况中,你的应用,一个线程中很有可能不只使用了一个ThreadLocal对象。这时使用Thread做key不就出有问题?

@Service
public class ThreadLocalService {private static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();private static final ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();private static final ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
}

假如使用Thread做key时,你的代码中定义了3个ThreadLocal对象,那么,通过Thread对象,它怎么知道要获取哪个ThreadLocal对象呢?

如下图所示:344e016002935c1ef584d3d8903bede2.png

因此,不能使用Thread做key,而应该改成用ThreadLocal对象做key,这样才能通过具体ThreadLocal对象的get方法,轻松获取到你想要的ThreadLocal对象。

如下图所示:a7ebfcc35a24afa90c125d66fceb0676.png

4. Entry的key为什么设计成弱引用?

前面说过,Entry的key,传入的是ThreadLocal对象,使用了WeakReference对象,即被设计成了弱引用。

那么,为什么要这样设计呢?

假如key对ThreadLocal对象的弱引用,改为强引用。3776cbbc30d2976ebc3fa136cfd31f5b.png我们都知道ThreadLocal变量对ThreadLocal对象是有强引用存在的。

即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal还是强引用。

此时,如果执行该代码的线程使用了线程池,一直长期存在,不会被销毁。

就会存在这样的强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。

那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。

为了解决这个问题,JDK的开发者们把Entry的key设计成了弱引用

弱引用的对象,在GC做垃圾清理的时候,就会被自动回收了。

如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null。

如下图所示:508dd769b6bc4ee04c4fb66166a67744.png接下来,最关键的地方来了。

由于当前的ThreadLocal变量已经被指向null了,但如果直接调用它的getsetremove方法,很显然会出现空指针异常。因为它的生命已经结束了,再调用它的方法也没啥意义。

此时,如果系统中还定义了另外一个ThreadLocal变量b,调用了它的getsetremove,三个方法中的任何一个方法,都会自动触发清理机制,将key为null的value值清空。

如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了。

这样就能最大程度的解决内存泄露问题。

需要特别注意的地方是:

  1. key为null的条件是,ThreadLocal变量指向null,并且key是弱引用。如果ThreadLocal变量没有断开对ThreadLocal的强引用,即ThreadLocal变量没有指向null,GC就贸然的把弱引用的key回收了,不就会影响正常用户的使用?

  2. 如果当前ThreadLocal变量指向null了,并且key也为null了,但如果没有其他ThreadLocal变量触发getsetremove方法,也会造成内存泄露。

下面看看弱引用的例子:

public static void main(String[] args) {WeakReference<Object> weakReference0 = new WeakReference<>(new Object());System.out.println(weakReference0.get());System.gc();System.out.println(weakReference0.get());
}

打印结果:

java.lang.Object@1ef7fe8e
null

传入WeakReference构造方法的是直接new处理的对象,没有其他引用,在调用gc方法后,弱引用对象会被自动回收。

但如果出现下面这种情况:

public static void main(String[] args) {Object object = new Object();WeakReference<Object> weakReference1 = new WeakReference<>(object);System.out.println(weakReference1.get());System.gc();System.out.println(weakReference1.get());
}

执行结果:

java.lang.Object@1ef7fe8e
java.lang.Object@1ef7fe8e

先定义了一个强引用object对象,在WeakReference构造方法中将object对象的引用作为参数传入。这时,调用gc后,弱引用对象不会被自动回收。

我们的Entry对象中的key不就是第二种情况吗?在Entry构造方法中传入的是ThreadLocal对象的引用。

如果将object强引用设置为null:

public static void main(String[] args) {Object object = new Object();WeakReference<Object> weakReference1 = new WeakReference<>(object);System.out.println(weakReference1.get());System.gc();System.out.println(weakReference1.get());object=null;System.gc();System.out.println(weakReference1.get());
}

执行结果:

java.lang.Object@6f496d9f
java.lang.Object@6f496d9f
null

第二次gc之后,弱引用能够被正常回收。

由此可见,如果强引用和弱引用同时关联一个对象,那么这个对象是不会被GC回收。也就是说这种情况下Entry的key,一直都不会为null,除非强引用主动断开关联。

此外,你可能还会问这样一个问题:Entry的value为什么不设计成弱引用?

答:Entry的value假如只是被Entry引用,有可能没被业务系统中的其他地方引用。如果将value改成了弱引用,被GC贸然回收了(数据突然没了),可能会导致业务系统出现异常。

而相比之下,Entry的key,管理的地方就非常明确了。

这就是Entry的key被设计成弱引用,而value没被设计成弱引用的原因。

5. ThreadLocal真的会导致内存泄露?

通过上面的Entry对象中的key设置成弱引用,并且使用getsetremove方法清理key为null的value值,就能彻底解决内存泄露问题?

答案是否定的。

如下图所示:be39e480093e16804c72067326e07b47.png假如ThreadLocalMap中存在很多key为null的Entry,但后面的程序,一直都没有调用过有效的ThreadLocal的getsetremove方法。

那么,Entry的value值一直都没被清空。

所以会存在这样一条强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object。

其结果就是:Entry和ThreadLocalMap将会长期存在下去,会导致内存泄露

6. 如何解决内存泄露问题?

前面说过的ThreadLocal还是会导致内存泄露的问题,我们有没有解决办法呢?

答:有办法,调用ThreadLocal对象的remove方法。

不是在一开始就调用remove方法,而是在使用完ThreadLocal对象之后。列如:

先创建一个CurrentUser类,其中包含了ThreadLocal的逻辑。

public class CurrentUser {private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal();public static void set(UserInfo userInfo) {THREA_LOCAL.set(userInfo);}public static UserInfo get() {THREA_LOCAL.get();}public static void remove() {THREA_LOCAL.remove();}
}

然后在业务代码中调用相关方法:

public void doSamething(UserDto userDto) {UserInfo userInfo = convert(userDto);try{CurrentUser.set(userInfo);...//业务代码UserInfo userInfo = CurrentUser.get();...} finally {CurrentUser.remove();}
}

需要我们特别注意的地方是:一定要在finally代码块中,调用remove方法清理没用的数据。如果业务代码出现异常,也能及时清理没用的数据。

remove方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。

7. ThreadLocal是如何定位数据的?

前面说过ThreadLocalMap对象底层是用Entry数组保存数据的。

那么问题来了,ThreadLocal是如何定位Entry数组数据的?

在ThreadLocal的get、set、remove方法中都有这样一行代码:

int i = key.threadLocalHashCode & (len-1);

通过key的hashCode值,数组的长度减1。其中key就是ThreadLocal对象,数组的长度减1,相当于除以数组的长度减1,然后取模

这是一种hash算法。

接下来给大家举个例子:假设len=16,key.threadLocalHashCode=31,

于是: int i = 31 & 15 = 15

相当于:int i = 31 % 16 = 15

计算的结果是一样的,但是使用与运算效率跟高一些。

为什么与运算效率更高?

答:因为ThreadLocal的初始大小是16,每次都是按2倍扩容,数组的大小其实一直都是2的n次方。这种数据有个规律就是高位是0,低位都是1。在做与运算时,可以不用考虑高位,因为与运算的结果必定是0。只需考虑低位的与运算,所以效率更高。

如果使用hash算法定位具体位置的话,就可能会出现hash冲突的情况,即两个不同的hashCode取模后的值相同。

ThreadLocal是如何解决hash冲突的呢?

我们看看getEntry是怎么做的:

private Entry getEntry(ThreadLocal<?> key) {//通过hash算法获取下标值int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];//如果下标位置上的key正好是我们所需要寻找的keyif (e != null && e.get() == key)//说明找到数据了,直接返回return e;else//说明出现hash冲突了,继续往后找return getEntryAfterMiss(key, i, e);
}

再看看getEntryAfterMiss方法:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;//判断Entry对象如果不为空,则一直循环while (e != null) {ThreadLocal<?> k = e.get();//如果当前Entry的key正好是我们所需要寻找的keyif (k == key)//说明这次真的找到数据了return e;if (k == null)//如果key为空,则清理脏数据expungeStaleEntry(i);else//如果还是没找到数据,则继续往后找i = nextIndex(i, len);e = tab[i];}return null;
}

关键看看nextIndex方法:

private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);
}

当通过hash算法计算出的下标小于数组大小,则将下标值加1。否则,即下标大于等于数组大小,下标变成0了。下标变成0之后,则循环一次,下标又变成1。。。

寻找的大致过程如下图所示:1149c8de8b2919b46b61be0beeb79da7.png如果找到最后一个,还是没有找到,则再从头开始找。0325a9f996b1da64193db46da3cd77e4.png不知道你有没有发现,它构成了一个:环形

ThreadLocal从数组中找数据的过程大致是这样的:

  1. 通过key的hashCode取余计算出一个下标。

  2. 通过下标,在数组中定位具体Entry,如果key正好是我们所需要的key,说明找到了,则直接返回数据。

  3. 如果第2步没有找到我们想要的数据,则从数组的下标位置,继续往后面找。

  4. 如果第3步中找key的正好是我们所需要的key,说明找到了,则直接返回数据。

  5. 如果还是没有找到数据,再继续往后面找。如果找到最后一个位置,还是没有找到数据,则再从头,即下标为0的位置,继续从前往后找数据。

  6. 直到找到第一个Entry为空为止。

8. ThreadLocal是如何扩容的?

从上面得知,ThreadLocal的初始大小是16。那么问题来了,ThreadLocal是如何扩容的?

set方法中会调用rehash方法:

private void set(ThreadLocal<?> key, Object value) {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();
}

注意一下,其中有个判断条件是:sz(之前的size+1)如果大于或等于threshold的话,则调用rehash方法。

threshold默认是0,在创建ThreadLocalMap时,调用它的构造方法:

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);
}

调用setThreshold方法给threshold设置一个值,而这个值INITIAL_CAPACITY是默认的大小16。

private void setThreshold(int len) {threshold = len * 2 / 3;
}

也就是第一次设置的threshold = 16 * 2 / 3, 取整后的值是:10。

换句话说当sz大于等于10时,就可以考虑扩容了。

rehash代码如下:

private void rehash() {//先尝试回收一次key为null的值,腾出一些空间expungeStaleEntries();if (size >= threshold - threshold / 4)resize();
}

在真正扩容之前,先尝试回收一次key为null的值,腾出一些空间。

如果回收之后的size大于等于threshold的3/4时,才需要真正的扩容。

计算公式如下:

16 * 2 * 4 / 3 * 4 - 16 * 2 / 3 * 4 = 8

也就是说添加数据后,新的size大于等于老size的1/2时,才需要扩容。

private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;//按2倍的大小扩容int newLen = oldLen * 2;Entry[] newTab = new Entry[newLen];int count = 0;for (int j = 0; j < oldLen; ++j) {Entry e = oldTab[j];if (e != null) {ThreadLocal<?> k = e.get();if (k == null) {e.value = null; // Help the GC} else {int h = k.threadLocalHashCode & (newLen - 1);while (newTab[h] != null)h = nextIndex(h, newLen);newTab[h] = e;count++;}}}setThreshold(newLen);size = count;table = newTab;
}

resize中每次都是按2倍的大小扩容。

扩容的过程如下图所示:b8e0a25396f2455f3e9b4d7830157221.png扩容的关键步骤如下:

  1. 老size + 1 = 新size

  2. 如果新size大于等于老size的2/3时,需要考虑扩容。

  3. 扩容前先尝试回收一次key为null的值,腾出一些空间。

  4. 如果回收之后发现size还是大于等于老size的1/2时,才需要真正的扩容。

  5. 每次都是按2倍的大小扩容。

9. 父子线程如何共享数据?

前面介绍的ThreadLocal都是在一个线程中保存和获取数据的。

但在实际工作中,有可能是在父子线程中共享数据的。即在父线程中往ThreadLocal设置了值,在子线程中能够获取到。

例如:

public class ThreadLocalTest {public static void main(String[] args) {ThreadLocal<Integer> threadLocal = new ThreadLocal<>();threadLocal.set(6);System.out.println("父线程获取数据:" + threadLocal.get());new Thread(() -> {System.out.println("子线程获取数据:" + threadLocal.get());}).start();}
}

执行结果:

父线程获取数据:6
子线程获取数据:null

你会发现,在这种情况下使用ThreadLocal是行不通的。main方法是在主线程中执行的,相当于父线程。在main方法中开启了另外一个线程,相当于子线程。

显然通过ThreadLocal,无法在父子线程中共享数据。

那么,该怎么办呢?

答:使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类。

修改代码之后:

public class ThreadLocalTest {public static void main(String[] args) {InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();threadLocal.set(6);System.out.println("父线程获取数据:" + threadLocal.get());new Thread(() -> {System.out.println("子线程获取数据:" + threadLocal.get());}).start();}
}

执行结果:

父线程获取数据:6
子线程获取数据:6

果然,在换成InheritableThreadLocal之后,在子线程中能够正常获取父线程中设置的值。

其实,在Thread类中除了成员变量threadLocals之外,还有另一个成员变量:inheritableThreadLocals。

Thread类的部分代码如下:

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

最关键的一点是,在它的init方法中会将父线程中往ThreadLocal设置的值,拷贝一份到子线程中。

感兴趣的小伙伴,可以找我私聊。或者看看我后面的文章,后面还会有专栏。

10. 线程池中如何共享数据?

在真实的业务场景中,一般很少用单独的线程,绝大多数,都是用的线程池

那么,在线程池中如何共享ThreadLocal对象生成的数据呢?

因为涉及到不同的线程,如果直接使用ThreadLocal,显然是不合适的。

我们应该使用InheritableThreadLocal,具体代码如下:

private static void fun1() {InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();threadLocal.set(6);System.out.println("父线程获取数据:" + threadLocal.get());ExecutorService executorService = Executors.newSingleThreadExecutor();threadLocal.set(6);executorService.submit(() -> {System.out.println("第一次从线程池中获取数据:" + threadLocal.get());});threadLocal.set(7);executorService.submit(() -> {System.out.println("第二次从线程池中获取数据:" + threadLocal.get());});
}

执行结果:

父线程获取数据:6
第一次从线程池中获取数据:6
第二次从线程池中获取数据:6

由于这个例子中使用了单例线程池,固定线程数是1。

第一次submit任务的时候,该线程池会自动创建一个线程。因为使用了InheritableThreadLocal,所以创建线程时,会调用它的init方法,将父线程中的inheritableThreadLocals数据复制到子线程中。所以我们看到,在主线程中将数据设置成6,第一次从线程池中获取了正确的数据6。

之后,在主线程中又将数据改成7,但在第二次从线程池中获取数据却依然是6。

因为第二次submit任务的时候,线程池中已经有一个线程了,就直接拿过来复用,不会再重新创建线程了。所以不会再调用线程的init方法,所以第二次其实没有获取到最新的数据7,还是获取的老数据6。

那么,这该怎么办呢?

答:使用TransmittableThreadLocal,它并非JDK自带的类,而是阿里巴巴开源jar包中的类。

可以通过如下pom文件引入该jar包:

<dependency><groupId>com.alibaba</groupId><artifactId>transmittable-thread-local</artifactId><version>2.11.0</version><scope>compile</scope>
</dependency>

代码调整如下:

private static void fun2() throws Exception {TransmittableThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();threadLocal.set(6);System.out.println("父线程获取数据:" + threadLocal.get());ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));threadLocal.set(6);ttlExecutorService.submit(() -> {System.out.println("第一次从线程池中获取数据:" + threadLocal.get());});threadLocal.set(7);ttlExecutorService.submit(() -> {System.out.println("第二次从线程池中获取数据:" + threadLocal.get());});}

执行结果:

父线程获取数据:6
第一次从线程池中获取数据:6
第二次从线程池中获取数据:7

我们看到,使用了TransmittableThreadLocal之后,第二次从线程中也能正确获取最新的数据7了。

nice。

如果你仔细观察这个例子,你可能会发现,代码中除了使用TransmittableThreadLocal类之外,还使用了TtlExecutors.getTtlExecutorService方法,去创建ExecutorService对象。

这是非常重要的地方,如果没有这一步,TransmittableThreadLocal在线程池中共享数据将不会起作用。

创建ExecutorService对象,底层的submit方法会TtlRunnableTtlCallable对象。

以TtlRunnable类为例,它实现了Runnable接口,同时还实现了它的run方法:

public void run() {Map<TransmittableThreadLocal<?>, Object> copied = (Map)this.copiedRef.get();if (copied != null && (!this.releaseTtlValueReferenceAfterRun || this.copiedRef.compareAndSet(copied, (Object)null))) {Map backup = TransmittableThreadLocal.backupAndSetToCopied(copied);try {this.runnable.run();} finally {TransmittableThreadLocal.restoreBackup(backup);}} else {throw new IllegalStateException("TTL value reference is released after run!");}
}

这段代码的主要逻辑如下:

  1. 把当时的ThreadLocal做个备份,然后将父类的ThreadLocal拷贝过来。

  2. 执行真正的run方法,可以获取到父类最新的ThreadLocal数据。

  3. 从备份的数据中,恢复当时的ThreadLocal数据。

11. ThreadLocal有哪些用途?

最后,一起聊聊ThreadLocal有哪些用途?

老实说,使用ThreadLocal的场景挺多的。

下面列举几个常见的场景:

  1. 在spring事务中,保证一个线程下,一个事务的多个操作拿到的是一个Connection。

  2. 在hiberate中管理session。

  3. 在JDK8之前,为了解决SimpleDateFormat的线程安全问题。

  4. 获取当前登录用户上下文。

  5. 临时保存权限数据。

  6. 使用MDC保存日志信息。

等等,还有很多业务场景,这里就不一一列举了。

由于篇幅有限,今天的内容先分享到这里。希望你看了这篇文章,会有所收获。

接下来留几个问题给大家思考一下:

  1. ThreadLocal变量为什么建议要定义成static的?

  2. Entry数组为什么要通过hash算法计算下标,即直线寻址法,而不直接使用下标值?

  3. 强引用和弱引用有什么区别?

  4. Entry数组大小,为什么是2的N次方?

  5. 使用InheritableThreadLocal时,如果父线程中重新set值,在子线程中能够正确的获取修改后的新值吗?

敬请期待我的下一篇文章,谢谢。

cd3f560e1865b18e19ff4c8e3a08a955.gif

往期推荐

3cfc177f63ed2c5fc907a66acb8ed77d.png

下个十年高性能 JSON 库来了:fastjson2!


0f7cea42230de6b86425a667e958cb32.png

一文详解读写锁


5aab2400ad1e2eea086f2cef7b4d896d.png

梳理50道经典计算机网络面试题


b49a6e336c56a45f4f8294e8367a1cf9.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544175.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring Boot 优雅配置多数据源

大约在19年的这个时候&#xff0c;老同事公司在做医疗系统&#xff0c;需要和HIS系统对接一些信息&#xff0c;比如患者、医护、医嘱、科室等信息。但是起初并不知道如何与HIS无缝对接&#xff0c;于是向我取经。最终经过讨论采用了视图对接的方式&#xff0c;大致就是HIS系统提…

(转)新ITC提交APP常见问题与解决方法(Icon Alpha,Build version,AppIcon120x120)(2014-11-17)...

1&#xff09;ICON无法上传&#xff0c;提示图片透明&#xff08;有Alpha通道&#xff09;苹果现在不接受png里的Alpha了&#xff0c;提交的图标带有Alpha通道就提示&#xff1a;简单处理&#xff1a;用自带的预览打开&#xff0c;导出时不勾选Alpha&#xff0c;仍保存为png格式…

Spring 夺命 35 问!

有人说&#xff0c;“Java程序员都是Spring程序员”&#xff0c;可以看出Spring在Java世界里举足轻重的作用。基础1.Spring是什么&#xff1f;特性&#xff1f;有哪些模块&#xff1f;Spring Logo一句话概括&#xff1a;Spring 是一个轻量级、非入侵式的控制反转 (IoC) 和面向切…

Android百度地图开发03之地图控制 + 定位

前两篇关于百度地图的blog写的是&#xff0c;一些基本图层的展示 和 覆盖物的添加地理编码和反地理编码。 接下来&#xff0c;这篇blog主要说一些关于地图控制方面的内容和定位功能。 百度地图提供的关于地图的操作主要有&#xff1a;单击、双击、长按、缩放、旋转、俯视等。 地…

IDEA 版 Postman 面世了,功能真心强大!

IDEA是最常用的开发工具&#xff0c;很多程序员都想把它打造成一站式开发平台&#xff0c;于是安装了各种各样的插件。最近发现了一款IDEA插件RestfulFastRequest&#xff0c;细节做的真心不错&#xff0c;说它是IDEA版的Postman也不为过&#xff0c;推荐给大家&#xff01;Res…

DNS子域授权

转载于:https://blog.51cto.com/changeflyhigh/1697257

mongo数据库插入数据_深入研究Mongo数据库

mongo数据库插入数据More popularly known as "mongoDB". It is a no-sql based database. 俗称“ mongoDB” 。 这是一个基于无SQL的数据库。 BASIC STRUCTURE OF MONGO DB MONGO DB的基本结构 A COLLECTION IN MONGODB having 3 DOCUMENTS MONGODB中有3个文档的集…

五分钟,手撸一个Spring容器!

Spring是我们最常用的开源框架&#xff0c;经过多年发展&#xff0c;Spring已经发展成枝繁叶茂的大树&#xff0c;让我们难以窥其全貌。这节&#xff0c;我们回归Spring的本质&#xff0c;五分钟手撸一个Spring容器&#xff0c;揭开Spring神秘的面纱&#xff01;从什么是IOC开始…

Spring Cloud OpenFeign 的 5 个优化小技巧!

作者 | 磊哥来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;OpenFeign 是 Spring 官方推出的一种声明式服务调用和负载均衡组件。它的出现就是为了替代已经进入停更维护状态的 Feign&am…

Java常用类:7000字一次性帮你总结好啦!

来源&#xff1a;cnblogs.com/lwtyyds/p/15678152.html常用类概述内部类内部类的分类&#xff1a;1.成员内部类&#xff08;非静态内部类&#xff09;2.局部内部类4.匿名内部类Object类Object类常用方法&#xff1a;1.equals方法2.hashCode方法3.toString方法4.finzlize方法包装…

CentOS6.4系统启动失败故障排查

转&#xff1a;http://www.centoscn.com/CentosBug/osbug/2014/1028/4011.html 操作系统启动失败如下图报错&#xff1a; 故障现象&#xff1a; 从图中可以看到&#xff0c;操作系统启动的过程中&#xff0c;fsck在执行文件系统检测时出现了错误&#xff0c;并且是在检查/dev/m…

Linux内存管理--物理内存分配【转】

转自&#xff1a;http://blog.csdn.net/myarrow/article/details/8682819 1. First Fit分配器 First Fit分配器是最基本的内存分配器&#xff0c;它使用bitmap而不是空闲块列表来表示内存。在bitmap中&#xff0c;如果page对应位为1&#xff0c;则表示此page已经被分配&#xf…

JDK的一个Bug,监听文件变更要小心了

背景 在某些业务场景下&#xff0c;我们需要自己实现文件内容变更监听的功能&#xff0c;比如&#xff1a;监听某个文件是否发生变更&#xff0c;当变更时重新加载文件的内容。看似比较简单的一个功能&#xff0c;但如果在某些JDK版本下&#xff0c;可能会出现意想不到的Bug。本…

推荐 17 个压箱底的常用类库

前言在java的庞大体系中&#xff0c;其实有很多不错的小工具&#xff0c;也就是我们平常说的&#xff1a;轮子。如果在我们的日常工作当中&#xff0c;能够将这些轮子用户&#xff0c;再配合一下idea的快捷键&#xff0c;可以极大得提升我们的开发效率。今天我决定把一些压箱底…

02、django中的上下文

2019独角兽企业重金招聘Python工程师标准>>> 1、譬如设置网站的名称,setting中设置变量&#xff1a; # setting.py SITE_NAME "我的小站"2、在view中写函数将该变量转换成字典,做返回值 from django.conf import settings def site_key(request):# 这里使…

实战:10 种实现延迟任务的方法,附代码!

作者 | 磊哥来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;这篇文章的诞生要感谢一位读者&#xff0c;是他让这篇优秀的文章有了和大家见面的机会&#xff0c;重点是优秀文章&#xff…

面渣逆袭:Redis连环五十二问!三万字+八十图详解!

基础1.说说什么是Redis?Redis图标Redis是一种基于键值对&#xff08;key-value&#xff09;的NoSQL数据库。比一般键值对数据库强大的地方&#xff0c;Redis中的value支持string&#xff08;字符串&#xff09;、hash&#xff08;哈希&#xff09;、 list&#xff08;列表&…

EasyExcel太方便易用了,强烈推荐!

背景 系统中经常要导出大量的数据&#xff0c;格式基本上都是Excel&#xff0c;然而每次导表都是对系统内存的一次挑战。在Java领域&#xff0c;生成或解析Excel的框架比较有名的当属Apache的poi和jxl了。但使用它们&#xff0c;会面临着严重的内存损耗问题。如果系统的并发量还…

【端午】送3本书!

白天在公司搬砖&#xff0c;晚上到家赶紧给小伙伴们安排一波福利&#xff0c;这次送的书是 H 大新出的《深入理解Java核心技术&#xff1a;写给Java工程师的干货笔记&#xff08;基础篇&#xff09;》。书中介绍了普通Java工程师必须要学习的相关知识点&#xff0c;包括面向对象…

面试突击51:为什么单例一定要加 volatile?

.作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;单例模式的实现方法有很多种&#xff0c;如饿汉模式、懒汉模式、静态内部类和枚举等&#xff0c;当面试官问到“为什…