ThreadLocal八股文

这篇具有很好参考价值的文章主要介绍了ThreadLocal八股文。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

1. 为什么要⽤ ThreadLocal?

2. ThreadLocal 的原理是什么?

3. 为什么⽤ ThreadLocal 做 key?

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

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

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

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

8. ThreadLocal 是如何扩容的?

9. ⽗⼦线程如何共享数据?

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

11. ThreadLocal 有哪些⽤途?


1. 为什么要⽤ ThreadLocal?

答:为了解决并发编程时,公共资源数据安全问题。(也可使⽤锁的⽅式)
并发编程是⼀项⾮常重要的技术,它让我们的程序变得更加⾼效。
但在并发的场景中,如果有多个线程同时修改公共变量,可能会出现线程安全问题,即该变量最终结果可能出现异常。 为了 解决线程安全问题 JDK 出现了很多技术⼿段,⽐如:使⽤ synchronized Lock ,给访问 公共资源的代码上锁,保证了代码的 原⼦性 。
但在⾼并发的场景中,如果多个线程同时竞争⼀把锁,这时会存在⼤量的锁等待,可能会浪费很多时间,让系统的响应时间⼀下⼦变慢。
因此,JDK 还提供了另外⼀种⽤空间换时间的新思路: ThreadLocal
它的核⼼思想是:共享变量在每个 线程 都有⼀个 副本 ,每个线程操作的都是⾃⼰的副本,对另外的线程没有影响。

2. ThreadLocal 的原理是什么?

答:在每个 Thread 类中,都有⼀个 ThreadLocalMap 的成员变量,该变量包含了⼀个 Entry 组 ,该数组真正保存了 ThreadLocal 类 set 的数据。
为了搞清楚 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 {
 ...
    }
 ...

}
ThreadLocal 的get ⽅法、 set ⽅法和 setInitialValue ⽅法,其实最终操作的都是
Thre adLocalMap 类中的数据。
其中 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 类,说明
E ntry 的实例都是⼀个弱引⽤。
另外第 6 ⾏代码,调⽤ WeakReference 的构造⽅法,将 ThreadLocal 对象传⼊,说明在 Entry 中
ThreadLocal 也是⼀个弱引⽤。
ThreadLocalMap 内部还包含了⼀个 Entry 数组,其中: Entry = ThreadLocal + value
⽽ThreadLocalMap 被定义成了 Thread 类的成员变量。
public class Thread implements Runnable {
 ...
 ThreadLocal.ThreadLocalMap threadLocals = null;
}
下⾯⽤⼀张图从宏观上,认识⼀下 ThreadLocal 的整体结构:
ThreadLocal八股文

 从上图中看出,在每个 Thread 类中,都有⼀个 ThreadLocalMap 的成员变量,该变量包含了⼀个 Entry 数组 ,该数组真正保存了 ThreadLocal 类 set 的数据。

也可以看到ThreadLocalMap 是同⼀个线程多个 ThreadLocal 共⽤的。
Entry 是由 threadLocal 和 value 组成,其中 threadLocal 对象是弱引⽤,在GC 的时候,会被⾃动
回收。⽽ value 就是 ThreadLocal 类 set 的数据。
下⾯⽤⼀张图总结⼀下引⽤关系:
ThreadLocal八股文

上图中除了 Entry 的 key 对 ThreadLocal 对象是 弱引⽤ ,其他的引⽤都是强引⽤ 。

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

3. 为什么⽤ ThreadLocal 做 key?

答: 如果使⽤线程当key,创建多个ThreadLocal的时, 线程不唯⼀,值会被覆盖。

不知道你有没有思考过这样⼀个问题:ThreadLocalMap 为什么要⽤ ThreadLocal 做 key,⽽不是
⽤Thread 做 key? 如果在你的应⽤中,⼀个线程中只使⽤了⼀个 ThreadLocal 对象,那么使⽤
Thread 做 key 也未尝 不可。
但实际情况中,你的应⽤,⼀个线程中很有可能不只使⽤了⼀个 ThreadLocal 对象。这时使
⽤Thread 做 key 不就出有问题?
public class ThreadLocalService { 
 private static final ThreadLocal<Integer> threadLocal1 = new ThreadLoca
l<>(); 
 private static final ThreadLocal<Integer> threadLocal2 = new ThreadLoca
l<>(); 
 private static final ThreadLocal<Integer> threadLocal3 = new ThreadLoca
l<>();
}
假如使⽤Thread 做 key 时,你的代码中定义了 3 个 ThreadLocal 对象,那么,通过 Thread 对象, 它怎么知道要获取哪个 ThreadLocal 对象呢?
如下图所示:
ThreadLocal八股文
因此,不能使⽤Thread 做 key,⽽应该改成⽤ ThreadLocal 对象做 key,这样才能通过具体
ThreadLocal 对象的get ⽅法,轻松获取到你想要的 ThreadLocal 对象。
如下图所示:
ThreadLocal八股文

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

答: 如果使⽤了线程池,线程池⾥的核⼼线程是⼀直存在的,线程⾥的ThreadLocalMap也会⼀直存在,如果使⽤了弱引⽤,当 ThreadLocal 变量指向 null 之后,在 GC 做垃圾清理的时候,key 会被⾃ 动回收,其值也被设置成 null。
前⾯说过,Entry 的 key,传⼊的是 ThreadLocal 对象,使⽤了WeakReference 对象,即被设计成
了弱引⽤。(弱引⽤:具体来说,当垃圾回收器执⾏垃圾标记时,如果发现⼀个对象只被WeakReference所引⽤,那么这个对象就会被标记为可以被回收的。在下⼀次垃圾回收时,这个对象就会被真正地回收 掉)
那么,为什么要这样设计呢?
假如 key 对 ThreadLocal 对象的弱引⽤,改为强引⽤。
ThreadLocal八股文
我们都知道 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。
如下图所示:
ThreadLocal八股文

接下来,最关键的地⽅来了。

由于当前的 ThreadLocal 变量已经被指向 null 了,但如果直接调⽤它 get set remove ⽅法,很显然会出现 空指针异常 。因为它的⽣命已经结束了,再调⽤它 的⽅法也没啥意义。
此时,如果系统中还定义了另外⼀个 ThreadLocal 变量 b,调⽤了它的 get set remove ,三个⽅法中的任何⼀个⽅法,都会⾃动触发清理机制,将 key 为 null 的 value 值清空。
我们看看 ThreadLocal 中的两个⽅法 cleanSomeSlots expungeStaleEntry ⽅法,在 get、set、
remove ⽅法中,当之前的 key 为 null 都会⾛到这两⽅法上来

 ThreadLocal八股文

 ThreadLocal八股文

如果 key 和 value 都是 null,那么 Entry 对象会被 GC 回收。如果所有的 Entry 对象都被回收了,
ThreadLocalMap 也会被回收了。
这样就能最⼤程度的解决内存泄露问题。
需要特别注意的地⽅是:
  1. key 为 null 的条件是,ThreadLocal 变量指向 null ,并且 key 是弱引⽤。如果 ThreadLocal 变量没有断开对 ThreadLocal 的强引⽤,即 ThreadLocal 变量没有指向 null,GC 就贸然的把弱引⽤的 key 回收了,不就会影响正常⽤户的使⽤?
  2. 如果当前 ThreadLocal 变量指向 null 了,并且 key 也为 null 了,⾃身肯定是不能够触发remove 了,但如果没有其他 ThreadLocal 变量触发 get set remove ⽅法,也会造成内存泄露。
下⾯看看弱引⽤的例⼦:
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 贸然回收了(数据突然没了),可能会导致业务系统出现异常。

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

答:会,假如 ThreadLocalMap 中存在很多 key 为 null 的 Entry,但后⾯的程序,⼀直都没有调⽤过有效的 ThreadLocal 的 get set remove ⽅法。Entry 的 value 值⼀直都没被清空。就有可 能导致内存泄漏。 

如下图所示: 

ThreadLocal八股文
假如 ThreadLocalMap 中存在很多 key 为 null 的 Entry,但后⾯的程序,⼀直都没有调⽤过有效的
ThreadLocal 的get set remove ⽅法。 那么,Entry 的 value 值⼀直都没被清空。
所以会存在这样⼀条 强引⽤链 :Thread 变量 -> Thread 对象 -> ThreadLocalMap -> Entry ->
value -> Object。其结果就是:Entry 和 ThreadLocalMap 将会⻓期存在下去,会导致内存泄露 。

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

答:在finally块调⽤ remove ⽅法。

不是在⼀开始就调⽤ remove ⽅法,⽽是在使⽤完 ThreadLocal 对象之后。列如:

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

public class CurrentUser {
 private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLoc
al();
 
 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 是如何定位数据的?

 答:Hash算法。。。。。。blablabla

 在 ThreadLocal 的 get、set、remove ⽅法中都有这样⼀⾏代码:

int i = key.threadLocalHashCode & (len-1); 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正好是我们所需要寻找的key
 if (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正好是我们所需要寻找的key
 if (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。。。
寻找的⼤致过程如下图所示:
ThreadLocal八股文

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

  1. 通过 key 的 hashCode 取余计算出⼀个下标。
  2. 通过下标,在数组中定位具体 Entry,如果 key 正好是我们所需要的 key,说明找到了,则直接返回数据。
  3. 如果第 2 步没有找到我们想要的数据,则从数组的下标位置,继续往后⾯找。
  4. 如果第 3 步中找 key 的正好是我们所需要的 key,说明找到了,则直接返回数据。
  5. 如果还是没有找到数据,再继续往后⾯找。如果找到最后⼀个位置,还是没有找到数据,则再从头,即下标为 0 的位置,继续从前往后找数据。
  6. 直到找到第⼀个 Entry 为空为⽌。 

以上就是⾮常典型的开放地址法(Open Addressing)解决哈希冲突。

常⻅有哪些⽅法解决哈希冲突呢?
1. 开放地址法(Open Addressing)解决哈希冲突:将哈希桶本身作为存储单元,当某个桶被占⽤时,往后依次查找空桶,直到找到⼀个空桶为⽌。开放地址法的实现较为复杂,需要考虑探测序列 的选取和删除操作的影响,但是可以节省额外的存储空间。
        1.1. 线性探测(Linear Probing):当哈希冲突发⽣时,按照⼀定步⻓(通常为 1)依次往后查找 下⼀个空槽位,直到找到为⽌。
        1.2. ⼆次探测(Quadratic Probing):当哈希冲突发⽣时,按照⼀定步⻓的平⽅(1,4,9, 16,...)依次往后查找下⼀个空槽位,直到找到为⽌。
        1.3. 随机探测(Random Probing):当哈希冲突发⽣时,按照⼀个随机步⻓(通常为⼀个随机 数)依次往后查找下⼀个空槽位,直到找到为⽌。
2. 链地址法( Separate Chaining):将每个哈希桶设计为⼀个链表或者其他的数据结构,当发⽣哈 希冲突时,将新的键值对插⼊到该桶对应的链表中。这种⽅法实现简单,适⽤于各种数据类型和负 载因⼦,但是需要额外的存储空间。
3. 再哈希法、双重散列(Double Hashing):当发⽣哈希冲突时,使⽤另⼀个哈希函数重新计算哈希值,直到找到⼀个空桶为⽌。这种⽅法可以避免探测序列的形成,但是需要选取不同的哈希函 数。
4. 公共溢出区(Overflow Area):将哈希表分为主区和溢出区,当主区某个位置发⽣哈希冲突时,将该键值对插⼊到溢出区中。这种⽅法的实现较为简单,但是查找效率会降低。

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 倍的⼤⼩扩容。
扩容的过程如下图所示:
ThreadLocal八股文

 扩容的关键步骤如下:

  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 InheritableThrea
dLocal<>();
 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 设置的值,拷⻉⼀份到⼦线程中。所以,使⽤ InheritableThreadLocal 要注意时机,在⼦线程执⾏完 init 后,⽗线程变更
InheritableThreadLocal 中的值,⼦线程是不会跟着变化的。

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

在真实的业务场景中,⼀般很少⽤单独的线程 ,绝⼤多数,都是⽤的线程池 。
那么,在线程池中如何共享 ThreadLocal 对象⽣成的数据呢?
因为涉及到不同的线程,如果直接使⽤ ThreadLocal,显然是不合适的。
我们应该使⽤ InheritableThreadLocal,具体代码如下:
private static void fun1() {
 InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLoc
al<>();
 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 TransmittableThrea
dLocal<>();
 threadLocal.set(6);
 System.out.println("⽗线程获取数据:" + threadLocal.get());
 
 ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorServic
e(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 了。
如果你仔细观察这个例⼦,你可能会发现,代码中除了使⽤TransmittableThreadLocal 类之外,
还使⽤了TtlExecutors.getTtlExecutorService ⽅法,去创建 ExecutorService 对象。
这是⾮常重要的地⽅,如果没有这⼀步,TransmittableThreadLocal 在线程池中共享数据将不会
起作⽤。
创建 ExecutorService 对象,底层的 submit ⽅法会TtlRunnable TtlCallable 对象。

以 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 a
fter run!");
 }
}
这段代码的主要逻辑如下:
  1. 把当时的 ThreadLocal 做个备份,然后将⽗类的 ThreadLocal 拷⻉过来。
  2. 执⾏真正的 run ⽅法,可以获取到⽗类最新的 ThreadLocal 数据。
  3. 从备份的数据中,恢复当时的 ThreadLocal 数据。

11. ThreadLocal 有哪些⽤途?

下⾯列举⼏个常⻅的场景: 文章来源地址https://www.toymoban.com/news/detail-446051.html

  1. 在 spring 事务中,保证⼀个线程下,⼀个事务的多个操作拿到的是⼀个 Connection。
  2. 在 hiberate 中管理 session
  3. 在 JDK8 之前,为了解决 SimpleDateFormat 的线程安全问题。
  4. 获取当前登录⽤户上下⽂
  5. 临时保存权限数据。
  6. 使⽤ MDC(⽇志追踪traceId) 保存⽇志信息
等等,还有很多业务场景,这⾥就不⼀⼀列举了。

到了这里,关于ThreadLocal八股文的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C++面试八股文:什么是RAII?

    某日二师兄参加XXX科技公司的C++工程师开发岗位第13面: 面试官:什么是 RAII ? 二师兄: RAII 是 Resource Acquisition Is Initialization 的缩写。翻译成中文是资源获取即初始化。 面试官: RAII 有什么特点和优势? 二师兄:主要的特点是,在对象初始化时获取资源,在对象析构时释放

    2024年02月08日
    浏览(44)
  • C++面试八股文:什么是左值,什么是右值?

    某日二师兄参加XXX科技公司的C++工程师开发岗位第16面: 面试官:什么是左值,什么是右值? 二师兄:简单来说,左值就是可以使用 符号取地址的值,而右值一般不可以使用 符号取地址。 二师兄:一般左值存在内存中,而右值存在寄存器中。 二师兄:严格意义上分,右值

    2024年02月09日
    浏览(32)
  • java八股文面试[多线程]——什么是守护线程

     知识来源: 【2023年面试】什么是守护线程_哔哩哔哩_bilibili

    2024年02月11日
    浏览(31)
  • C++面试八股文:什么是空指针/野指针/悬垂指针?

    某日二师兄参加XXX科技公司的C++工程师开发岗位第30面: 面试官:什么是空指针? 二师兄:一般我们将等于 0 / NULL / nullptr 的指针称为空指针。空指针不能被解引用,但是可以对空指针取地址。 面试官:你知道 0/NULL/nullptr 三者之间的区别吗? 二师兄:虽然三者都能定义空指

    2024年02月11日
    浏览(27)
  • C++面试八股文:override和finial关键字有什么作用?

    某日二师兄参加XXX科技公司的C++工程师开发岗位第22面: (二师兄好苦逼,节假日还在面试。。。) 面试官:C++的继承了解吗? 二师兄:(不好意思,你面到我的强项了。。)了解一些。 面试官:什么是虚函数,为什么需要虚函数? 二师兄:虚函数允许在基类中定义一个函

    2024年02月10日
    浏览(31)
  • 什么叫面试八股文,一篇文章带你入门音视频(1),使用指南

    版税方式:按个收取 备注:WMA的全称是Windows Media Audio,它是微软公司推出的与MP3格式齐名的一种新的音频格式。由于WMA在压缩比和音质方面都超过了MP3,更是远胜于RA(Real Audio),即使在较低的采样频率下也能产生较好的音质,再加上WMA有微软的Windows Media Player做其强大的后盾

    2024年04月25日
    浏览(34)
  • ThreadLocal-为什么会导致内存泄漏

    一个Thread中存在字段为ThreadLocalMap ThreadLocal.ThreadLocalMap threadLocals = null; 也就是说我们使用threadLocal进行set操作和get操作,其实就是操作该Thread的threadLocalMap。 这个map是一个基于数组的key和value的格式,在进行get的时候,会将调用对象this作为key进行查询。 hashMap是通过链表法,

    2024年02月03日
    浏览(47)
  • 【JVM篇】ThreadLocal中为什么要使用弱引用

    ThreadLocal可以在线程中存放线程的本地变量,保证数据的线程安全 ThreadLocal是这样子保存对象的: 在每个线程中,存放了一个ThreadLocalMap对象,本质上就是一个数组实现的哈希表,里面存放多个Entry对象 每个Entry对象继承自弱引用,内部存放ThreadLocal对象,同时用强引用,引用

    2024年02月20日
    浏览(36)
  • 深度学习八股文

    Bert旨在通过联合左侧和右侧的上下文,从未标记文本中预训练出一个深度双向表示模型。因此,BERT可以通过增加一个额外的输出层来进行微调,就可以达到为广泛的任务创建State-of-the-arts 模型的效果,比如QA、语言推理任务。Bert的构成:由12层Transformer Encoder构成。bert的基本

    2024年04月08日
    浏览(33)
  • elasticsearch面试八股文

    es的存储和更新机制 Elasticsearch(ES)是一个开源的分布式搜索和分析引擎,其存储架构和更新机制如下: 存储架构: 索引(Index) :ES的数据组织单元是索引,每个索引可以包含多个类型(Types),类似于数据库中的表。索引是由一个或多个分片(Shard)组成的,用于将数据

    2024年02月16日
    浏览(33)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包