文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

抛出这8个问题,检验一下你到底会不会ThreadLocal,来摸个底~

2024-12-11 19:04

关注
  1.  和Synchronized的区别
  2.  存储在jvm的哪个区域
  3.  真的只是当前线程可见吗
  4.  会导致内存泄漏么
  5.  为什么用Entry数组而不是Entry对象
  6.  你学习的开源框架哪些用到了ThreadLocal
  7.  ThreadLocal里的对象一定是线程安全的吗
  8.  笔试题

一、概述

1、官方术语

ThreadLocal类是用来提供线程内部的局部变量。让这些变量在多线程环境下访问(get/set)时能保证各个线程里的变量相对独立于其他线程内的变量。

2、大白话

ThreadLocal是一个关于创建线程局部变量的类。

通常情况下,我们创建的成员变量都是线程不安全的。因为他可能被多个线程同时修改,此变量对于多个线程之间彼此并不独立,是共享变量。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程无法访问和修改。也就是说:将线程公有化变成线程私有化。

二、应用场景

比如: 

  1.   
  2. public class ThreadLocalTest05 {  
  3.     public static String dateToStr(int millisSeconds) {  
  4.         Date date = new Date(millisSeconds);  
  5.         SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();  
  6.         return simpleDateFormat.format(date);  
  7.     }  
  8.     private static final ExecutorService executorService = Executors.newFixedThreadPool(100);  
  9.     public static void main(String[] args) {  
  10.         for (int i = 0; i < 3000; i++) {  
  11.             int j = i 
  12.             executorService.execute(() -> {  
  13.                 String date = dateToStr(j * 1000);  
  14.                 // 从结果中可以看出是线程安全的,时间没有重复的。  
  15.                 System.out.println(date);  
  16.             });  
  17.         }  
  18.         executorService.shutdown();  
  19.     }  
  20.  
  21. class ThreadSafeFormatter {  
  22.     public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() {  
  23.         @Override  
  24.         protected SimpleDateFormat initialValue() {  
  25.             return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");  
  26.         }  
  27.     }; 
  28.     // java8的写法,装逼神器  
  29. //    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =  
  30. //            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));  

细心的朋友已经发现了,这TM也是每个线程都创建一个SimpleDateFormat啊,跟直接在方法内部new没区别,错了,大错特错!1个请求进来是一个线程,他可能贯穿了N个方法,你这N个方法假设有3个都在使用dateToStr(),你直接new的话会产生三个SimpleDateFormat对象,而用ThreadLocal的话只会产生一个对象,一个线程一个。

再细化一点就是:

三、核心知识

1、类关系

每个Thread对象中都持有一个ThreadLocalMap的成员变量。每个ThreadLocalMap内部又维护了N个Entry节点,也就是Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。

核心源码如下 

  1. // java.lang.Thread类里持有ThreadLocalMap的引用  
  2. public class Thread implements Runnable {  
  3.     ThreadLocal.ThreadLocalMap threadLocals = null 
  4.  
  5. // java.lang.ThreadLocal有内部静态类ThreadLocalMap  
  6. public class ThreadLocal<T> {  
  7.     static class ThreadLocalMap {  
  8.         private Entry[] table;      
  9.         // ThreadLocalMap内部有Entry类,Entry的key是ThreadLocal本身,value是泛型值  
  10.         static class Entry extends WeakReference<ThreadLocal>> {  
  11.             Object value;  
  12.             Entry(ThreadLocal> k, Object v) {  
  13.                 super(k);  
  14.                 vvalue = v;  
  15.             }  
  16.         }  
  17.     }  

2、类关系图

ThreadLocal内存结构图。

3、主要方法

3.1、initialValue

3.1.1、什么意思

见名知意,初始化一些value(泛型值)。懒加载的。

3.1.2、触发时机

调用get方法之前没有调用set方法,则get方法内部会触发initialValue,也就是说get的时候如果没拿到东西,则会触发initialValue。

3.1.3、补充说明

比如: 

  1. public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() {  
  2.     @Override  
  3.     protected SimpleDateFormat initialValue() {  
  4.         return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");  
  5.     }  
  6. };  
  7. // Java8的高逼格写法  
  8. public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =  
  9.             ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")); 

3.1.4、源码 

  1. // 由子类提供实现。  
  2. // protected的含义就是交给子类干的。  
  3. protected T initialValue() {  
  4.     return null;  

3.2、get

3.2.1、什么意思

获取当前线程下的ThreadLocal中的值。

3.2.2、源码 

  1.   
  2. public T get() {  
  3.     // 获取当前线程  
  4.     Thread t = Thread.currentThread();  
  5.     // 获取当前线程对应的ThreadLocalMap对象。  
  6.     ThreadLocalMap map = getMap(t);  
  7.     // 若获取到了。则获取此ThreadLocalMap下的entry对象,若entry也获取到了,那么直接获取entry对应的value返回即可。  
  8.     if (map != null) {  
  9.         // 获取此ThreadLocalMap下的entry对象  
  10.         ThreadLocalMap.Entry e = map.getEntry(this);  
  11.         // 若entry也获取到了  
  12.         if (e != null) {  
  13.             @SuppressWarnings("unchecked")  
  14.             // 直接获取entry对应的value返回。 
  15.             T result = (T)e.value;  
  16.             return result;  
  17.         }  
  18.     }  
  19.     // 若没获取到ThreadLocalMap或没获取到Entry,则设置初始值。  
  20.     // 知识点:我早就说了,初始值方法是延迟加载,只有在get才会用到,这下看到了吧,只有在这获取没获取到才会初始化,下次就肯定有值了,所以只会执行一次!!!  
  21.     return setInitialValue();  

3.3、set

3.3.1、什么意思

其实干的事和initialValue是一样的,都是set值,只是调用时机不同。set是想用就用,api摆在这里,你想用就调一下set方法。很自由。

3.3.2、源码 

  1.   
  2. public void set(T value) {  
  3.     // 获取当前线程  
  4.     Thread t = Thread.currentThread();  
  5.     // 获取当前线程对应的ThreadLocalMap实例,注意这里是将t传进去了,t是当前线程,就是说ThreadLocalMap是在线程里持有的引用。  
  6.     ThreadLocalMap map = getMap(t);  
  7.     // 若当前线程有对应的ThreadLocalMap实例,则将当前ThreadLocal对象作为key,value做为值存到ThreadLocalMap的entry里。  
  8.     if (map != null)  
  9.         map.set(this, value);  
  10.     else  
  11.         // 若当前线程没有对应的ThreadLocalMap实例,则创建ThreadLocalMap,并将此线程与之绑定  
  12.         createMap(t, value);  

3.4、remove

3.4.1、什么意思

将当前线程下的ThreadLocal的值删除,目的是为了减少内存占用。主要目的是防止内存泄漏。内存泄漏问题下面会说。

3.4.2、源码 

  1.   
  2. public void remove() {  
  3.     // 获取当前线程的ThreadLocalMap对象,并将其移除。  
  4.     ThreadLocalMap m = getMap(Thread.currentThread());  
  5.     if (m != null)  
  6.         // 直接移除以当前ThreadLocal为key的value  
  7.         m.remove(this);  

4、ThreadLocalMap

为啥单独拿出来说下,我就是想强调一点:这个东西是归Thread类所有的。它的引用在Thread类里,这也证实了一个问题:ThreadLocalMap类内部为什么有Entry数组,而不是Entry对象?

因为你业务代码能new好多个ThreadLocal对象,各司其职。但是在一次请求里,也就是一个线程里,ThreadLocalMap是同一个,而不是多个,不管你new几次ThreadLocal,ThreadLocalMap在一个线程里就一个,因为再说一次,ThreadLocalMap的引用是在Thread里的,所以它里面的Entry数组存放的是一个线程里你new出来的多个ThreadLocal对象。

核心源码如下: 

  1. // 在你调用ThreadLocal.get()方法的时候就会调用这个方法,它的返回是当前线程里的threadLocals的引用。  
  2. // 这个引用指向的是ThreadLocal里的ThreadLocalMap对象  
  3. ThreadLocalMap getMap(Thread t) {  
  4.     return t.threadLocals;  
  5.  
  6. public class Thread implements Runnable {  
  7.     // ThreadLocal.ThreadLocalMap  
  8.     ThreadLocal.ThreadLocalMap threadLocals = null 

四、完整源码

1、核心源码 

  1. // 本地线程。Thread:线程。Local:本地  
  2. public class ThreadLocal<T> {  
  3.     // 构造器  
  4.  public ThreadLocal() {}  
  5.     // 初始值,用来初始化值用的,比如:ThreadLocal<Integer> count = new ThreadLocal<>();  
  6.     // 你想Integer value = count.get(); value++;这样是报错的,因为count现在还没值,取出来的是个null,所以你需要先重写此方法为value赋上初始值,本身方法是protected也代表就是为了子类重写的。 
  7.      // 此方法是一个延迟调用方法,在线程第一次调用get的时候才执行,下面具体分析源码就知道了。  
  8.  protected T initialValue() {}  
  9.     // 创建ThreadLocalMap,ThreadLocal底层其实就是一个map来维护的。 
  10.   void createMap(Thread t, T firstValue) {}  
  11.     // 返回该当前线程对应的线程局部变量值。  
  12.  public T get() {}  
  13.     // 获取ThreadLocalMap  
  14.  ThreadLocalMap getMap(Thread t) {}  
  15.     // 设置当前线程的线程局部变量的值  
  16.  public void set(T value) {}  
  17.     // 将当前线程局部变量的值删除,目的是为了减少内存占用。其实当线程结束后对应该线程的局部变量将自动被垃圾回收,所以无需我们调用remove,我们调用remove无非也就是加快内存回收速度。  
  18.  public void remove() {}  
  19.     // 设置初始值,调用initialValue  
  20.  private T setInitialValue() {} 
  21.     // 静态内部类,一个map来维护的!!!  
  22.  static class ThreadLocalMap {  
  23.         // ThreadLocalMap的静态内部类,继承了弱引用,这正是不会造成内存泄漏根本原因  
  24.         // Entry的key为ThreadLocal并且是弱引用。value是值  
  25.   static class Entry extends WeakReference<ThreadLocal>> {}  
  26.  }  

2、set() 

  1.   
  2. public void set(T value) {  
  3.     // 获取当前线程  
  4.     Thread t = Thread.currentThread();  
  5.     // 获取当前线程对应的ThreadLocalMap实例  
  6.     ThreadLocalMap map = getMap(t);  
  7.     // 若当前线程有对应的ThreadLocalMap实例,则将当前ThreadLocal对象作为key,value做为值存到ThreadLocalMap的entry里。  
  8.     if (map != null)  
  9.         map.set(this, value);  
  10.     else  
  11.         // 若当前线程没有对应的ThreadLocalMap实例,则创建ThreadLocalMap,并将此线程与之绑定  
  12.         createMap(t, value);  

3、getMap() 

  1. // 在你调用ThreadLocal.get()方法的时候就会调用这个方法,它的返回是当前线程里的threadLocals的引用。  
  2. // 这个引用指向的是ThreadLocal里的ThreadLocalMap对象  
  3. ThreadLocalMap getMap(Thread t) {  
  4.     return t.threadLocals;  
  5.  
  6. public class Thread implements Runnable {  
  7.     // ThreadLocal.ThreadLocalMap  
  8.     ThreadLocal.ThreadLocalMap threadLocals = null 

4、map.set() 

  1. // 不多BB,就和HashMap的set一个道理,只是赋值key,value。  
  2. // 需要注意的是这里key是ThreadLocal对象,value是值  
  3. private void set(ThreadLocal> key, Object value) {} 

5、createMap() 

  1.   
  2. void createMap(Thread t, T firstValue) {  
  3.     t.threadLocals = new ThreadLocalMap(this, firstValue);  
  4.  
  5. // ThreadLocalMap构造器。  
  6. ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {  
  7.     table = new Entry[INITIAL_CAPACITY];  
  8.     int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  
  9.     // 重点看这里!!!!!!  
  10.     // new了一个ThreadLocalMap的内部类Entry,且将key和value传入。 
  11.     // key是ThreadLocal对象。  
  12.     table[i] = new Entry(firstKey, firstValue);  
  13.     size = 1 
  14.     setThreshold(INITIAL_CAPACITY);  
  15.  
  16.  

6、get() 

  1.   
  2. public T get() {  
  3.     // 获取当前线程  
  4.     Thread t = Thread.currentThread();  
  5.     // 获取当前线程对应的ThreadLocalMap对象。  
  6.     ThreadLocalMap map = getMap(t);  
  7.     // 若获取到了。则获取此ThreadLocalMap下的entry对象,若entry也获取到了,那么直接获取entry对应的value返回即可。  
  8.     if (map != null) {  
  9.         // 获取此ThreadLocalMap下的entry对象  
  10.         ThreadLocalMap.Entry e = map.getEntry(this);  
  11.         // 若entry也获取到了  
  12.         if (e != null) {  
  13.             @SuppressWarnings("unchecked")  
  14.             // 直接获取entry对应的value返回。  
  15.             T result = (T)e.value;  
  16.             return result;  
  17.         }  
  18.     }  
  19.     // 若没获取到ThreadLocalMap或没获取到Entry,则设置初始值。  
  20.     // 知识点:我早就说了,初始值方法是延迟加载,只有在get才会用到,这下看到了吧,只有在这获取没获取到才会初始化,下次就肯定有值了,所以只会执行一次!!!  
  21.     return setInitialValue();  

7、setInitialValue() 

  1. // 设置初始值  
  2. private T setInitialValue() {  
  3.     // 调用初始值方法,由子类提供。  
  4.     T value = initialValue();  
  5.     // 获取当前线程  
  6.     Thread t = Thread.currentThread();  
  7.     // 获取map  
  8.     ThreadLocalMap map = getMap(t);  
  9.     // 获取到了  
  10.     if (map != null)  
  11.         // set  
  12.         map.set(this, value);  
  13.     else  
  14.         // 没获取到。创建map并赋值  
  15.         createMap(t, value);  
  16.     // 返回初始值。  
  17.     return value;  

8、initialValue() 

  1. // 由子类提供实现。  
  2. // protected  
  3. protected T initialValue() {  
  4.     return null;  

9、remove() 

  1.   
  2. public void remove() {  
  3.     // 获取当前线程的ThreadLocalMap对象,并将其移除。 
  4.     ThreadLocalMap m = getMap(Thread.currentThread());  
  5.     if (m != null)  
  6.         m.remove(this);  

10、小结

只要捋清楚如下几个类的关系,ThreadLocal将变得so easy!

Thread、ThreadLocal、ThreadLocalMap、Entry

一句话总结就是:Thread维护了ThreadLocalMap,而ThreadLocalMap里维护了Entry,而Entry里存的是以ThreadLocal为key,传入的值为value的键值对。

五、答疑(面试题)

1、和Synchronized的区别

问:他和线程同步机制(如:Synchronized)提供一样的功能,这个很吊啊。

答:放屁!同步机制保证的是多线程同时操作共享变量并且能正确的输出结果。ThreadLocal不行啊,他把共享变量变成线程私有了,每个线程都有独立的一个变量。举个通俗易懂的案例:网站计数器,你给变量count++的时候带上synchronized即可解决。ThreadLocal的话做不到啊,他没发统计,他只能说能统计每个线程登录了多少次。

2、存储在jvm的哪个区域

问:线程私有,那么就是说ThreadLocal的实例和他的值是放到栈上咯?

答:不是。还是在堆的。ThreadLocal对象也是对象,对象就在堆。只是JVM通过一些技巧将其可见性变成了线程可见。

3、真的只是当前线程可见吗

问:真的只是当前线程可见吗?

答:貌似不是,貌似通过InheritableThreadLocal类可以实现多个线程访问ThreadLocal的值,但是我没研究过,知道这码事就行了。

4、会导致内存泄漏么

问:会导致内存泄漏么?

答:分析一下:

    1、ThreadLocalMap.Entry的key会内存泄漏吗?

    2、ThreadLocalMap.Entry的value会内存泄漏吗?

先看下key-value的核心源码 

  1. static class Entry extends WeakReference<ThreadLocal>> {  
  2.     Object value;  
  3.     Entry(ThreadLocal> k, Object v) {  
  4.         super(k);  
  5.         vvalue = v;  
  6.     }  

先看继承关系,发现是继承了弱引用,而且key直接是交给了父类处理super(key),父类是个弱引用,所以key完全不存在内存泄漏问题,因为他不是强引用,它可以被GC回收的。

弱引用的特点:如果这个对象只被弱引用关联,没有任何强引用关联,那么这个对象就可以被GC回收掉。弱引用不会阻止GC回收。这是jvm知识。

再看value,发现value是个强引用,但是想了下也没问题的呀,因为线程终止了,我管你强引用还是弱引用,都会被GC掉的,因为引用链断了(jvm用的可达性分析法,线程终止了,根节点就断了,下面的都会被回收)。

这么分析一点毛病都没有,但是忘了一个主要的角色,那就是线程池,线程池的存在核心线程是不会销毁的,只要创建出来他会反复利用,生命周期不会结束掉,但是key是弱引用会被GC回收掉,value强引用不会回收,所以形成了如下场面:

Thread->ThreadLocalMap->Entry(key为null)->value

由于value和Thread还存在链路关系,还是可达的,所以不会被回收,这样越来越多的垃圾对象产生却无法回收,早晨内存泄漏,时间久了必定OOM。

解决方案ThreadLocal已经为我们想好了,提供了remove()方法,这个方法是将value移出去的。所以用完后记得remove()。

5、为什么用Entry数组而不是Entry对象

    这个其实主要想考ThreadLocalMap是在Thread里持有的引用。

问:ThreadLocalMap内部的table为什么是数组而不是单个对象呢?

答:因为你业务代码能new好多个ThreadLocal对象,各司其职。但是在一次请求里,也就是一个线程里,ThreadLocalMap是同一个,而不是多个,不管你new几次ThreadLocal,ThreadLocalMap在一个线程里就一个,因为ThreadLocalMap的引用是在Thread里的,所以它里面的Entry数组存放的是一个线程里你new出来的多个ThreadLocal对象。

6、你学习的开源框架哪些用到了ThreadLocal

Spring框架。

DateTimeContextHolder

RequestContextHolder

7、ThreadLocal里的对象一定是线程安全的吗

未必,如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()获取的还是这个共享对象本身,还是有并发访问线程不安全问题。

8、笔试题

问:下面这段程序会输出什么?为什么? 

  1. public class TestThreadLocalNpe {  
  2.     private static ThreadLocal<Long> threadLocal = new ThreadLocal();  
  3.     public static void set() {  
  4.         threadLocal.set(1L); 
  5.      } 
  6.     public static long get() {  
  7.         return threadLocal.get();  
  8.     }  
  9.     public static void main(String[] args) throws InterruptedException {  
  10.         new Thread(() -> {  
  11.             set();  
  12.             System.out.println(get());  
  13.         }).start();  
  14.         // 目的就是为了让子线程先运行完  
  15.         Thread.sleep(100);  
  16.         System.out.println(get());  
  17.     }  

答: 

  1.  
  2. Exception in thread "main" java.lang.NullPointerException  
  3.  at com.chentongwei.study.thread.TestThreadLocalNpe.get(TestThreadLocalNpe.java:16)  
  4.  at com.chentongwei.study.thread.TestThreadLocalNpe.main(TestThreadLocalNpe.java:26) 

为什么?

为什么输出个1,然后空指针了?

首先输出1是没任何问题的,其次主线程空指针是为什么?

如果你这里回答 

  1.  

那我恭喜你,你连ThreadLocal都不知道是啥,这明显两个线程,子线程和主线程。子线程设置1,主线程肯定拿不到啊,ThreadLocal和线程是嘻嘻相关的。这个不多费口舌。

说说为什么是空指针?

因为你get方法用的long而不是Long,那也应该返回null啊,大哥,long是基本类型,默认值是0,没有null这一说法。ThreadLocal里的泛型是Long,get却是基本类型,这需要拆箱操作的,也就是会执行null.longValue()的操作,这绝逼空指针了。

看似一道Javase的基础题目,实则隐藏了很多知识。

六、ThreadLocal工具类 

  1. package com.duoku.base.util;  
  2. import com.google.common.collect.Maps;  
  3. import org.springframework.core.NamedThreadLocal;  
  4. import java.util.Map;  
  5.   
  6. public class ThreadLocalUtil {  
  7.     private static final ThreadLocal<Map<String, Object>> threadLocal = new NamedThreadLocal("xxx-threadlocal") {  
  8.         @Override  
  9.         protected Map<String, Object> initialValue() {  
  10.             return Maps.newHashMap();  
  11.         }  
  12.     };  
  13.     public static Map<String, Object> getThreadLocal(){  
  14.         return threadLocal.get();  
  15.     }  
  16.     public static <T> T get(String key) {  
  17.         Map map = threadLocal.get();  
  18.         // todo:copy a new one  
  19.         return (T)map.get(key);  
  20.     }  
  21.     public static <T> T get(String key,T defaultValue) {  
  22.         Map map = threadLocal.get();  
  23.         return (T)map.get(key) == null ? defaultValue : (T)map.get(key);  
  24.     }  
  25.     public static void set(String key, Object value) {  
  26.         Map map = threadLocal.get();  
  27.         map.put(key, value);  
  28.     }  
  29.     public static void set(Map<String, Object> keyValueMap) {  
  30.         Map map = threadLocal.get();  
  31.         map.putAll(keyValueMap);  
  32.     }  
  33.     public static void remove() {  
  34.         threadLocal.remove();  
  35.     }  

琐碎时间想看一些技术文章,可以去公众号菜单栏翻一翻我分类好的内容,应该对部分童鞋有帮助。 

 

来源:Java知音内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯