48

这4种ThreadLocal你都知道吗?

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzIyMjQwMTgyNA%3D%3D&%3Bmid=2247484611&%3Bidx=1&%3Bsn=e428f6da86192e5992591167de2f4f7e
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

点击蓝色字关注我们!

什么是ThreadLocal

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

实际应用

实际开发中我们真正使用 ThreadLocal 的场景还是比较少的,大多数使用都是在框架里面。最常见的使用场景的话就是用它来解决数据库连接、 Session 管理等保证每一个线程中使用的数据库连接是同一个。还有一个用的比较多的场景就是用来解决 SimpleDateFormat 解决线程不安全的问题,不过现在 java8 提供了 DateTimeFormatter 它是线程安全的,感兴趣的同学可以去看看。还可以利用它进行优雅的传递参数,传递参数的时候,如果父线程生成的变量或者参数直接通过 ThreadLocal 传递到子线程参数就会丢失,这个后面会介绍一个其他的 ThreadLocal 来专门解决这个问题的。

ThreadLocal api介绍

ThreadLocal的API还是比较少的就几个api VvaiY3E.png!mobile 我们看下这几个 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 的结构给介绍清楚了。 m6vqQn3.png!mobilezMbABjn.png!mobile 接下来我们再看下 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();
    }
}

运行结果:

不得不佩服下这个作者,通过使用了斐波那契散列法,来保证哈希表的离散度,让结果很均匀。可见 「代码要写的好,数学还是少不了」 啊。其他的源码就不分析了,大家感兴趣可以自行去查看下。

ThreadLocal的内存泄露

关于 ThreadLocal 是否会引起内存泄漏也是一个比较有争议性的问题。首先我们需要知道什么是内存泄露?

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

ThreadLocal 的内存泄露情况:

  • ThreadLocal
    GC
    ThreadLocal
    ThreadLocalMap
    key
    null
    Entry
    Entry
    value
    set、get
    key
    null
    Entry
    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还被线程强引用着。效果图如下: F7Rzmqu.png!mobile

  • ThreadLocalMap
    了set()、get()、remove()
    cleanSomeSlots()、expungeStaleEntry()
    key
    null
    value
    ThreadLocal
    set(),get(),remove()
    value
    static
    ThreadLocal
    ThreadLocal
    ThreadLocal
    ThreadLocal
    GC
    Entry
    key
    null
    remove
    remove
    key
    null
    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
    
  • 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。

  • 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。

  • 感谢您的阅读,十分欢迎并感谢您的关注。

    YBZby2R.png!mobile

  • 巨人的肩膀摘苹果:

    https://zhuanlan.zhihu.com/p/40515974 https://www.cnblogs.com/aspirant/p/8991010.html https://www.cnblogs.com/jiangxinlingdu/p/11123538.html https://blog.csdn.net/hewenbo111/article/details/80487252

YveMzm.gif!mobile

yyARvqu.png!mobile

yIRnAzE.png!mobile

rAjQvyF.png!mobile

最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构、等等。

获取方式:点“ 在看 ”,关注公众号并回复 666  领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK