关于java:阿里面试官问我ThreadLocal我一口气给他说了四种

34次阅读

共计 9867 个字符,预计需要花费 25 分钟才能阅读完成。

微信搜一搜【java 金融】关注这个多年开发教训老码农。

什么是 ThreadLocal

ThreadLocal类顾名思义能够了解为线程本地变量。也就是说如果定义了一个 ThreadLocal,每个线程往这个ThreadLocal 中读写是线程隔离,相互之间不会影响的。它提供了一种将可变数据通过每个线程有本人的独立正本从而实现线程关闭的机制。

理论利用

理论开发中咱们真正应用 ThreadLocal 的场景还是比拟少的,大多数应用都是在框架外面。最常见的应用场景的话就是用它来解决数据库连贯、Session治理等保障每一个线程中应用的数据库连贯是同一个。还有一个用的比拟多的场景就是用来解决 SimpleDateFormat 解决线程不平安的问题,不过当初 java8 提供了 DateTimeFormatter 它是线程平安的,感兴趣的同学能够去看看。还能够利用它进行优雅的传递参数,传递参数的时候,如果父线程生成的变量或者参数间接通过 ThreadLocal 传递到子线程参数就会失落,这个前面会介绍一个其余的 ThreadLocal 来专门解决这个问题的。

ThreadLocal api 介绍

ThreadLocal 的 API 还是比拟少的就几个 api 咱们看下这几个 api 的应用,应用起来也超级简略

 private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(()->"java 金融");  
    public static void main(String[] args) {System.out.println("获取初始值:"+threadLocal.get());  
        threadLocal.set("关注:【java 金融】");  
        System.out.println("获取批改后的值:"+threadLocal.get());  
        threadLocal.remove();}

输入后果:

获取初始值:java 金融  
获取批改后的值:关注:【java 金融】

是不是炒鸡简略,就几行代码就把所有 api 都笼罩了。上面咱们就来简略看看这几个 api 的源码吧。

成员变量

 /** 初始容量,必须为 2 的幂  
         * The initial capacity -- MUST be a power of two.  
         */  
        private static final int INITIAL_CAPACITY = 16;  
  
        /** Entry 表,大小必须为 2 的幂  
         * 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

这里会有一个面试常常问到的问题: 为什么 entry 数组的大小,以及初始容量都必须是 2 的幂?对于 firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 以及很多源码外面都是应用 hashCode &(-1)来代替 hashCode%。这种写法益处如下:

  • 应用位运算代替取模,晋升计算效率。
  • 为了使不同 hash 值产生碰撞的概率更小,尽可能促使元素在哈希表中平均地散列。

set 办法

 public void set(T value) {Thread t = Thread.currentThread();  
        ThreadLocalMap map = getMap(t);  
        if (map != null)  
            map.set(this, value);  
        else  
            createMap(t, value);  
    }

set办法还是比较简单的,咱们能够重点看下这个办法外面的 ThreadLocalMap,它既然是个 map(留神不要与 java.util.map 一概而论,这里指的是概念上的 map),必定是有本人的 key 和 value 组成,咱们依据源码能够看出它的key 是其实能够把它简略看成是 ThreadLocal,然而实际上 ThreadLocal 中寄存的是ThreadLocal 的弱援用,而它的 value 的话是咱们理论 set 的值

 static class Entry extends WeakReference<ThreadLocal<?>> {  
            /** The value associated with this ThreadLocal. */  
            Object value; // 理论寄存的值  
  
            Entry(ThreadLocal<?> k, Object v) {super(k);  
                value = v;  
            }  
        }

Entry就是是 ThreadLocalMap 里定义的节点,它继承了 WeakReference 类,定义了一个类型为 Objectvalue,用于寄存塞到 ThreadLocal 里的值。咱们再来看下这个 ThreadLocalMap 是位于哪里的?咱们看到 ThreadLocalMap 是位于Thread 外面的一个变量,而咱们的值又是放在 ThreadLocalMap,这样的话咱们就实现了每个线程间的隔离。上面两张图的根本就把ThreadLocal 的构造给介绍分明了。 接下来咱们再看下 ThreadLocalMap 外面的数据结构,咱们晓得 HaseMap 解决 hash 抵触是由链表和红黑树(jdk1.8)来解决的,然而这个咱们看到 ThreadLocalMap 只有一个数组,它是怎么来解决 hash 抵触呢?ThreadLocalMap采纳 「线性探测」 的形式,什么是线性探测呢?就是根 「据初始key 的 hashcode 值确定元素在 table 数组中的地位,如果发现这个地位上曾经有其余 key 值的元素被占用,则利用固定的算法寻找肯定步长的下个地位,顺次判断,直至找到可能寄存的地位」ThreadLocalMap解决 Hash 抵触的形式就是简略的步长加 1 或减1,寻找下一个相邻的地位。

 /**  
         * Increment i modulo len.  
         */  
        private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);  
        }  
  
        /**  
         * Decrement i modulo len.  
         */  
        private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);  
        }

这种形式的话如果一个线程外面有大量的 ThreadLocal 就会产生性能问题,因为每次都须要对这个 table 进行遍历,清空有效的值。所以咱们在应用的时候尽可能的应用少的 ThreadLocal,不要在线程外面创立大量的ThreadLocal,如果须要设置不同的参数类型咱们能够通过ThreadLocal 来寄存一个 ObjectMap这样的话,能够大大减少创立 ThreadLocal 的数量。伪代码如下:

public final class HttpContext {private HttpContext() { }  
    private static final ThreadLocal<Map<String, Object>> CONTEXT = ThreadLocal.withInitial(() -> new ConcurrentHashMap(64));  
    public static <T> void add(String key, T value) {if(StringUtils.isEmpty(key) || Objects.isNull(value)) {throw new IllegalArgumentException("key or value is null");  
        }  
        CONTEXT.get().put(key, value);  
    }  
    public static <T> T get(String key) {return (T) get().get(key);  
    }  
    public static Map<String, Object> get() {return CONTEXT.get();  
    }  
    public static void remove() {CONTEXT.remove();  
    }  
}  

这样的话咱们如果须要传递不同的参数,能够间接应用一个 ThreadLocal 就能够代替多个 ThreadLocal 了。如果感觉不想这么玩,我就是要创立多个 ThreadLocal,我的需要就是这样,而且性能还得要好,这个能不能实现列?能够应用nettyFastThreadLocal能够解决这个问题,不过要配合使 FastThreadLocalThread 或者它子类的线程线程效率才会更高,更多对于它的应用能够自行查阅材料哦。

上面咱们先来看下它的这个哈希函数

 // 生成 hash code 间隙为这个魔数,能够让生成进去的值或者说 ThreadLocal 的 ID 较为平均地散布在 2 的幂大小的数组中。private static final int HASH_INCREMENT = 0x61c88647;  
  
    /**  
     * Returns the next hash code.  
     */  
    private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);  
    }

能够看出,它是在上一个被结构出的 ThreadLocalID/threadLocalHashCode的根底上加上一个魔数 0x61c88647 的。这个魔数的选取与斐波那契散列无关,0x61c88647对应的十进制为 1640531527. 当咱们应用0x61c88647 这个魔数累加对每个 ThreadLocal 调配各自的 ID 也就是 threadLocalHashCode 再与 2 的幂(数组的长度)取模,失去的后果散布很平均。咱们能够来也演示下通过这个魔数

public class MagicHashCode {  
    private static final int HASH_INCREMENT = 0x61c88647;  
  
    public static void main(String[] args) {hashCode(16); // 初始化 16  
        hashCode(32); // 后续 2 倍扩容  
        hashCode(64);  
    }  
  
    private static void hashCode(Integer length) {  
        int hashCode = 0;  
        for (int i = 0; i < length; i++) {  
            hashCode = i * HASH_INCREMENT + HASH_INCREMENT;// 每次递增 HASH_INCREMENT  
            System.out.print(hashCode & (length - 1));  
            System.out.print(" ");  
        }  
        System.out.println();}  
}  

运行后果:

7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0   
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0   
7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0 

不得不拜服下这个作者,通过应用了斐波那契散列法,来保障哈希表的离散度,让后果很平均。可见 「代码要写的好,数学还是少不了」 啊。其余的源码就不剖析了,大家感兴趣能够自行去查看下。

ThreadLocal 的内存泄露

对于 ThreadLocal 是否会引起内存透露也是一个比拟有争议性的问题。首先咱们须要晓得什么是内存泄露?

在 Java 中,内存透露就是存在一些被调配的对象,这些对象有上面两个特点,首先,这些对象是可达的,即在有向图中,存在通路能够与其相连;其次,这些对象是无用的,即程序当前不会再应用这些对象。如果对象满足这两个条件,这些对象就能够断定为 Java 中的内存透露,这些对象不会被 GC 所回收,然而它却占用内存。

ThreadLocal的内存泄露状况:

  • 线程的生命周期很长,当 ThreadLocal 没有被内部强援用的时候就会被 GC 回收(给 ThreadLocal 置空了):ThreadLocalMap会呈现一个 keynullEntry,但这个Entryvalue将永远没方法被拜访到(后续在也无奈操作 set、get 等办法了)。如果当这个线程始终没有完结,那这个 keynullEntry 因为也存在强援用(Entry.value),而 Entry 被以后线程的 ThreadLocalMap 强援用(Entry[] table),导致这个 Entry.value 永远无奈被GC,造成内存透露。上面咱们来演示下这个场景
 public static void main(String[] args) throws InterruptedException {ThreadLocal<Long []> threadLocal = new ThreadLocal<>();  
        for (int i = 0; i < 50; i++) {run(threadLocal);  
        }  
        Thread.sleep(50000);  
        // 去除强援用  
        threadLocal = null;  
        System.gc();  
        System.runFinalization();  
        System.gc();}  
  
    private static void run(ThreadLocal<Long []> threadLocal) {new Thread(() -> {threadLocal.set(new Long[1024 * 1024 *10]);  
            try {Thread.sleep(1000000000);  
            } catch (InterruptedException e) {e.printStackTrace();  
            }  
        }).start();}

通过 jdk 自带的工具 jconsole.exe 会发现即便执行了gc 内存也不会缩小,因为 key 还被线程强援用着。效果图如下:

  • 针对于这种状况 ThreadLocalMap在设计中,曾经思考到这种状况的产生,你只有调用 了 set()、get()、remove()办法都会调用 cleanSomeSlots()、expungeStaleEntry() 办法去革除 keynullvalue。这是一种被动的清理形式,然而如果ThreadLocalset(),get(),remove()办法没有被调用,就会导致 value 的内存透露。它的文档举荐咱们应用 static 润饰的 ThreadLocal,导致ThreadLocal 的生命周期和持有它的类一样长,因为 ThreadLocal 有强援用在,意味着这个 ThreadLocal 不会被 GC。在这种状况下,咱们如果不手动删除,Entrykey永远不为 null,弱援用也就失去了意义。所以咱们在应用的时候尽可能养成一个好的习惯,应用实现后手动调用下remove 办法。其实理论生产环境中咱们手动 remove 大多数状况并不是为了防止这种 keynull的状况,更多的时候,是为了保障业务以及程序的正确性。比方咱们下单申请后通过 ThreadLocal 构建了订单的上下文申请信息,而后通过线程池异步去更新用户积分,这时候如果更新实现,没有进行 remove 操作,即便下一次新的订单会笼罩原来的值然而也是有可能会导致业务问题。如果不想手动清理是否还有其余形式解决下列?FastThreadLocal 能够去理解下,它提供了主动回收机制。
  • 在线程池的场景,程序不进行,线程始终在复用的话,根本不会销毁,其实实质就跟下面例子是一样的。如果线程不复用,用完就销毁了就不会存在泄露的状况。因为线程完结的时候会 jvm 被动调用 exit 办法清理。
 /**  
         * This method is called by the system to give a Thread  
         * a chance to clean up before it actually exits.  
         */  
        private void exit() {if (group != null) {group.threadTerminated(this);  
                group = null;  
            }  
            /* Aggressively null out all reference fields: see bug 4006245 */  
            target = null;  
            /* Speed the release of some of these resources */  
            threadLocals = null;  
            inheritableThreadLocals = null;  
            inheritedAccessControlContext = null;  
            blocker = null;  
            uncaughtExceptionHandler = null;  
        }

InheritableThreadLocal

文章结尾有提到过父子之间线程的变量传递失落的状况。然而 InheritableThreadLocal 提供了一种父子线程之间的数据共享机制。能够解决这个问题。

 static ThreadLocal<String> threadLocal = new ThreadLocal<>();  
    static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();  
  
    public static void main(String[] args) throws InterruptedException {threadLocal.set("threadLocal 主线程的值");  
        Thread.sleep(100);  
        new Thread(() -> System.out.println("子线程获取 threadLocal 的主线程值:" + threadLocal.get())).start();  
        Thread.sleep(100);  
        inheritableThreadLocal.set("inheritableThreadLocal 主线程的值");  
        new Thread(() -> System.out.println("子线程获取 inheritableThreadLocal 的主线程值:" + inheritableThreadLocal.get())).start();}

输入后果

线程获取 threadLocal 的主线程值:null  
子线程获取 inheritableThreadLocal 的主线程值:inheritableThreadLocal 主线程的值  

然而 InheritableThreadLocal 和线程池应用的时候就会存在问题,因为子线程只有在线程对象创立的时候才会把父线程 inheritableThreadLocals 中的数据复制到本人的 inheritableThreadLocals 中。这样就实现了父线程和子线程的上下文传递。然而线程池的话,线程会复用,所以会存在问题。如果要解决这个问题能够有什么方法列?大家能够思考下,或者在下方留言哦。如果切实不想思考的话,能够参考下阿里巴巴的 transmittable-thread-local 哦。

总结

  • 大略介绍了 ThreadLocal 的常见用法,以及大抵实现原理,以及对于 ThreadLocal 的内存泄露问题,以及对于应用它须要留神的事项,以及如何解决父子线程之间的传递。介绍了 ThreadLocal、InheritableThreadLocal、FastThreadLocal、transmittable-thread-local 各种应用场景,以及须要留神的事项。本文重点介绍了ThreadLocal,如果把这个弄清楚了,其余几种 ThreadLocal 就更好了解了。

完结

  • 因为本人满腹经纶,难免会有纰漏,如果你发现了谬误的中央,还望留言给我指出来, 我会对其加以修改。
  • 如果你感觉文章还不错,你的转发、分享、赞叹、点赞、留言就是对我最大的激励。
  • 感谢您的浏览, 非常欢送并感谢您的关注。 伟人的肩膀摘苹果:https://zhuanlan.zhihu.com/p/… https://www.cnblogs.com/aspirant/p/8991010.html https://www.cnblogs.com/jiang… https://blog.csdn.net/hewenbo111/article/details/80487252

正文完
 0