• 你的ThreadLocal线程安全么


    想必很多小伙伴们对ThreadLocal并不陌生,ThreadLocal叫做线程本地变量,也就是ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量。那么,我们使用ThreadLocal一定线程安全么?话不多说,先上结论:

    如果threadlocal.get之后的副本,只在当前线程中使用,那么是线程安全的;如果对其他线程暴露,不一定是线程安全的。

    为了演示下错误的使用方式,先看下如下代码(虽然小伙伴们都不会这样写代码 ^_^):

    static class Container {
        int num;
    }
    public static void main(String[] args) throws InterruptedException {
        ThreadLocal<Container> tl = new ThreadLocal<>();
        tl.set(new Container()); // 先set下ThreadLocal
    
        Container container = tl.get();
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                container.num++;
            }
        };
    
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
    
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    
        System.out.println(tl.get().num);
    }
    

    笔者的一次结果输出为:17581

    结合代码,我们知道,在执行threadlcoal.get获取到线程变量副本之后,不要让其他线程来访问它了,否则就是多线程操作同一个变量,可能造成线程安全问题。

    除了上述讨论的ThreadLocal线程安全性问题之外,ThreadLocal如果使用不当,可能存在内存泄露问题。ThreadLocal变量是保存在Thread.threadLocals中(ThreadLocalMap类型)以Entry类型保存的,其中Entry.key(也就是弱引用referent实际指向对象)为ThreadLocal变量,该变量为弱类型;Entry.value为实际set的value。

    // Entry,里面保存在ThreadLocal变量,也就是key,是弱引用
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
    
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    

    虽然Entry.referent是弱类型,指向ThreadLocal变量,但是如果ThreadLocal变量本身引用不置为null的话,这里的Entry.referent指向对象是不会释放的。比如我们常用的定义方式:

    // 静态变量和对象属性
    static ThreadLocal<String> tls = new ThreadLocal<>();
    ThreadLocal<Integer> tli = new ThreadLocal<>();
    

    类似于静态变量和对象属性这种引用,如果不将tls或tli设置为null,那么ThreadLocal变量无法释放(这不是废话么,人家可是强引用呀),此时的Entry.referent弱类型没啥卵用;只有在tls或tli为null时,Entry.referent弱类型就起作用了,在第一次GC时就会将Entry.referent弱类型指向的对象回收。

    如果Entry.referent弱类型指向的对象回收了(没调用ThreadLocal.remove操作),Entry.value对象还在,并且Entry.value可是强引用的,此时就发生了内存泄露。这也就是ThreadLocal使用不当(没调用ThreadLocal.remove)时产生的内存泄漏问题。不过,伴随着其他ThreadLocal对象的set/get/remove的进行,会清除一部分Entry.referent为null但是Entry.value不为null的对象的,也就是修复内存泄露问题,注意,这个只是清除部分这样的Entry,并不能保证一次就能清除全部这样的Entry,所以还是要遵循ThreadLocal.set,用完之后就remove。

    讨论完了ThreadLocal的潜在问题之后,你是不是意犹未尽,想深入了解下ThreadLocal实现原理?OK,那就搬起小板凳,一起唠唠吧~

    ps:如果小伙伴对ThreadLocal原理已经熟悉了,那么恭喜你,后面的内容可以不看了~

    ThreadLocal实现原理

    ThreadLocal变量主要有get/set/remove三个操作,理解了这三个操作流程,基本上就理解了ThreadLocal实现原理。

    get

    get流程如下:

    1. 获取当前线程的threadLocals(map结构),从threadLocals中获取当前ThreadLocal变量对应的ThreadLocalMap.Entry(pair类型,包含了当前ThreadLocal变量及其对应的value),非空直接返回对应的value
    2. 为空时使用默认值(默认为null)构造ThreadLocalMap.Entry,放到当前线程的threadLocals中,下次再get时直接返回ThreadLocalMap.Entry对应的value即可
    /**
     * 当前线程的threadLocalMap中获取当前ThreadLocal对应的value
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 设置null值,下次直接返回null了
        return setInitialValue();
    }
    
    /**
     * 如果一次找到了entry,直接返回;否则就是set时hash冲突了
     * 遍历后续的slot,进行查找
     * 这里其实JDK可以做个优化,在set之后,将slot位置记录在Threadlocal变量中,下次直接到对应slot位置get即可
     */
    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }
    

    注意:线程的threadLocals是一个基于开放定址法实现的map结构。

    set

    • set操作就是将ThreadLocal变量的值put到当前线程的threadLocals中,ThreadLocal变量及其对应的值会构造成一个ThreadLocalMap.Entry放到threadLocals中。
    • 因为线程的threadLocals是一个基于开放定址法实现的map结构,所以在出现hash冲突后会继续寻找下一个空位进行set操作。
    • 因为是基于开放定址法,如果map中元素过多,会影响get和put性能,所以需要扩容,map的数组结构默认大小为INITIAL_CAPACITY = 16,默认扩容阈值为threshold = INITIAL_CAPACITY * 2 / 3,扩容时按照成倍扩容。
    /**
     * 获取当前线程的threadLocalMap,非空直接set value;
     * 否则新建一个包含value的threadLocalMap。
     * threadLocalMap的key对应程序中定义的ThreadLocal变量,value对应要set的值
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t); // Thread.threadLocals
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    // Entry,里面保存在ThreadLocal变量,也就是key,是弱引用
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
    
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    /**
     * hash码的生成,这里所有的ThreadLocal对象hash生成都是基于static变量nextHashCode来做的
     * 创建ThreadLocal对象时threadLocalHashCode已初始化完成
     */
    private final int threadLocalHashCode = nextHashCode();
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    private static AtomicInteger nextHashCode =
            new AtomicInteger();
    
    /**
     * 当前线程的threadLocalMap非空直接set value
     */
    private void set(ThreadLocal<?> key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
    
        // 如果当前table[i] hash冲突,那么就以i为起点,遍历后续table[i],
        // 这其实就是hash冲突中的开放定址法,另外一种是分离链接法
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
    
            // key已存在,更新vlaue即可
            if (k == key) {
                e.value = value;
                return;
            }
            // key为null,复制value即可
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    
        // 新建Entry,清理一部分Entry.key为null,value不为null的数据,避免内存泄露
        // 超过了threshold时rehash操作
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
    

    remove

    /**
     * 从ThreadLocalMap删除对应key
     */
    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }
    private void remove(ThreadLocal<?> key) {
        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)]) {
            if (e.get() == key) {
                // 清除Entry.key弱引用,设置为null
                e.clear();
                // 清除Entry.value引用,可能还涉及部分key为null的Entry数据清理
                expungeStaleEntry(i);
                return;
            }
        }
    }
    

    小结

    从ThreadLocal的get/set操作流程来看,ThreadLocal的value 是 Lazy Init(延迟初始化的)。ThreadLocal为什么是延迟初始化,这个问题应该是容易理解的,原因是:在没有具体业务场景前提下,这样的做法避免内存浪费。

    ThreadLocal变量默认放在基于开放定址法实现的map结构中,这种结构在hash冲突时会造成多次get/set操作,理论上可以通过记录ThreadLocal变量set时的位置,这样下次直接通过该位置获取对应value即可,可以参考netty的FastThreadLocal,它的实现思路就是这样的,提高了set/get的效率。

     

    最后来一张ThreadLocal的整体图:

    参考资料:

    1、https://luoxn28.github.io/2019/04/27/ni-de-threadlocal-yi-ding-xian-cheng-an-quan-ma/

  • 相关阅读:
    IDEA上传项目至git
    MyBatis 三种批量插入方式的对比 !
    华为 Java 编程军规 !
    Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
    mysql数据库表的查询操作-总结
    idea返回git上历史版本
    JSONObject json对象-JSONArray json数组
    exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but
    第三课
    第二课
  • 原文地址:https://www.cnblogs.com/luoxn28/p/10916253.html
Copyright © 2020-2023  润新知