Loading

ThreadLocal

date: 2020-08-19 10:16:00
updated: 2020-08-19 10:16:00

ThreadLocal

参考网址

以数据库连接为例,如果多个线程共享一个连接,有可能一个线程在对数据库进行操作,另一个线程调用了closeConnection操作;如果在每一个线程都new一个连接对象,如果开启关闭数据库操作频繁,会影响到服务器压力,并且影响程序执行性能。
=> ThreadLocal 内部维护一个 ThreadLocalMap 类,保存的是 Entry<Thread K, Object V> 数组,K是线程,V是值,这样每一个线程无论在哪里调用,都会拿到自己线程的值

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

ThreadLocalMap的引用是在Thread里的,所以它里面的Entry数组存放的是一个线程里new出来的多个ThreadLocal对象

Thread维护了ThreadLocalMap,而ThreadLocalMap里维护了Entry,而Entry里存的是以ThreadLocal为key,传入的值为value的键值对。

// java.lang.Thread类里持有ThreadLocalMap的引用
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

// java.lang.ThreadLocal有内部静态类ThreadLocalMap
public class ThreadLocal<T> {
    static class ThreadLocalMap {
        private Entry[] table;
        
        // ThreadLocalMap内部有Entry类,Entry的key是ThreadLocal本身,value是泛型值
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

ThreadLocal内存结构图

主要方法:

  • initialValue:初始化。在get方法里懒加载的。

    • 通常,每个线程最多调用一次此方法。但是如果已经调用了remove(),然后再次调用get()的话,则可以再次触发initialValue。
    • 如果要重写的话一般建议采取匿名内部类的方式重写此方法,否则默认返回的是null。
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() {
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    }
    };
    // Java8的高逼格写法
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
    
  • get:得到这个线程对应的value。如果调用get之前没set过,则get内部会执行initialValue方法进行初始化。

    /**
    * 获取当前线程下的entry里的value值。
    * 先获取当前线程下的ThreadLocalMap,
    * 然后以当前ThreadLocal为key取出map中的value
    */
    public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程对应的ThreadLocalMap对象。
        ThreadLocalMap map = getMap(t);
        // 若获取到了。则获取此ThreadLocalMap下的entry对象,若entry也获取到了,那么直接获取entry对应的value返回即可。
        if (map != null) {
            // 获取此ThreadLocalMap下的entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 若entry也获取到了
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 直接获取entry对应的value返回。
                T result = (T)e.value;
                return result;
            }
        }
        // 若没获取到ThreadLocalMap或没获取到Entry,则设置初始值。 懒加载方式
        return setInitialValue();
    }
    
    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
            // 如果e=null,说明出现碰撞问题,通过开放寻址方法来继续寻找
            return getEntryAfterMiss(key, i, e);
    }
    
    // 通过布长+1或-1来寻找下一个相邻的位置
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
    
    
  • set:为这个线程设置一个新值。

    /**
    * 设置当前线程的线程局部变量的值
    * 实际上ThreadLocal的值是放入了当前线程的一个ThreadLocalMap实例中,所以只能在本线程中访问。
    */
    public void set(T value) {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程对应的ThreadLocalMap实例,注意这里是将t传进去了,t是当前线程,就是说ThreadLocalMap是在线程里持有的引用。
        ThreadLocalMap map = getMap(t);
        // 若当前线程有对应的ThreadLocalMap实例,则将当前ThreadLocal对象作为key,value做为值存到ThreadLocalMap的entry里。
        if (map != null)
            map.set(this, value);
        else
            // 若当前线程没有对应的ThreadLocalMap实例,则创建ThreadLocalMap,并将此线程与之绑定
            createMap(t, value);
    }
    
  • remove:ThreadLocalMap键为弱引用,删除这个线程对应的值,防止内存泄露的最佳手段。

碰撞解决与神奇的 0x61c88647(十进制:1640531527)

两种碰撞类型

- 只有一个ThreadLocal实例的时候(上面推荐的做法),当向thread-local变量中设置多个值的时产生的碰撞,碰撞解决是通过开放定址法, 且是线性探测(linear-probe)
- 多个ThreadLocal实例的时候,最极端的是每个线程都new一个ThreadLocal实例,此时利用特殊的哈希码0x61c88647大大降低碰撞的几率, 同时利用开放定址法处理碰撞

显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。

所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

key.threadLocalHashCode ==> AtomicInteger.getAndAdd(HASH_INCREMENT) 其中 HASH_INCREMENT = 0x61c88647,这个值是 为了让哈希码能均匀的分布在2的N次方的数组里

This number represents the golden ratio (sqrt(5)-1) times two to the power of 31 ((sqrt(5)-1) * (2^31)). The result is then a golden number, either 2654435769 or -1640531527.

魔数0x61c88647的与斐波那契散列有关,0x61c88647对应的十进制为1640531527。斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说(1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647。通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。

posted @ 2020-10-22 11:13  猫熊小才天  阅读(79)  评论(0编辑  收藏  举报