• Java 线程 — ThreadLocal


    ThreadLocal

    先来看看ThreadLocal的注释:
    This class provides** thread-local variables**. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

    翻译过来就是:ThreadLocal提供了线程级的变量,这个变量和其他的使用set、get访问的变量不一样,ThreadLocal针对每个线程会为该变量维护经过单独初始化的副本。ThreadLocal实例希望定义为private static。

    从注释可以看出ThreadLocal解决的问题是:

    • 单线程的变量共享

    单线程变量共享

    如果是变量共享为什么不用一个全局变量就好呢?主要是因为ThreadLocal为每个线程维护经过单独初始化的变量副本,但是普通的变量访问到的就是同一个。

    上面的注释也说了,ThreadLocal会为每个线程维护经过单独初始化的变量副本,每个线程访问的都是自己的副本,所以各个线程之间不会相互影响,达到隔离的作用

    线程同步解决的是多线程访问共享资源的问题,但是ThreadLocal本身并不是用来多线程之间共享的,只是用来单线程共享的,所以ThreadLocal和线程同步根本不是一回事儿

    实现原理

    • 每个Thread都有一个ThreadLocal.ThreadLocalMap,因为Thread类里面有一个该类的对象,用来存放该线程中所有的ThreadLocal类型的变量

    • ThreadLocalMap里面有一个Entry(继承自WeakReference,一个对象保存一个键值对)数组,根据key(这里就是ThreadLocal变量本身)的哈希值将value(这里就是需要保存的数据)散列到数组中

    • 初次调用threadLocal.get的时候如果ThreadLocalMap尚未初始化,会调用createMap初始化

    ThreadLocalMap初始化

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        // 如果map未初始化
        return setInitialValue();
    }
    
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
    	// 新建初始大小为16的数组
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        // 设置扩容的阈值
        setThreshold(INITIAL_CAPACITY);
    }
    

    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);
    }
    
    // 如果有碰撞则向后查找
    private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
    
        while (e != null) {
            ThreadLocal k = e.get();
            if (k == key)
                return e;
            if (k == null)
            	// 如果key为null则进行一次清除
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }
    

    set

    /**
     * Set the value associated with key.
     *
     * @param key the thread local object
     * @param value the value to be set
     */
    private void set(ThreadLocal key, Object value) {
    
        // We don't use a fast path as with get() because it is at
        // least as common to use set() to create new entries as
        // it is to replace existing ones, in which case, a fast
        // path would fail more often than not.
    
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
    
    	// 使用的是开放地址法解决冲突,如果发生碰撞则向后查找
        // 如果得到的i位置已经有值,那么就向后一个单位尝试填充
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal k = e.get();
    		// 如果是相同的key就替换,说明同一个对象中的
            // 同一个threadLocal变量
            if (k == key) {
                e.value = value;
                return;
            }
    		// 因为是弱引用,ThreadLocal已经被回收,所以key就是null,将value放在这个位置
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    
    	// 直到找到一个元素为空的位置(e == null),
        // 每新占用一个数组位置(上面都是在替换原来元素或者替换
        // 已经被移除的元素,size已经加过的)就要判断是否需要进行扩容
        tab[i] = new Entry(key, value);
        int sz = ++size;
        // 如果没有清除数组中的元素并且元素个数已经大于等于阈值threshold则进行扩容
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
    
    // 扫描数组清除陈旧的数据,但并不是全部扫描,而是log2(n)对数扫描
    // 在全部扫描和不扫描之间取一个折中
    private boolean cleanSomeSlots(int i, int n) {
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
    
        do {
            i = nextIndex(i, len);
            Entry e = tab[i];
            if (e != null && e.get() == null) {
                n = len;
                removed = true;
                i = expungeStaleEntry(i);
            }
        // 无符号右移一位,相当于每次除2取整
        } while ( (n >>>= 1) != 0);
        return removed;
    }
    
    // 在staleSlot到下一个数组元素为null之间,清空陈旧(key为null)的元素,
    // 重新散列非陈旧的元素
    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
    
        // expunge entry at staleSlot
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;
    
        // Rehash until we encounter null
        Entry e;
        int i;
        for (i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal k = e.get();
            if (k == null) {
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    tab[i] = null;
    
                    // Unlike Knuth 6.4 Algorithm R, we must scan until
                    // null because multiple entries could have been stale.
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }
    
    
    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) {
                e.clear();
                // 将若引用置为null之后,进行一次清除
                expungeStaleEntry(i);
                return;
            }
        }
    }
    

    Java 中的引用

    各种不同的引用的区别就是引用到的对象被垃圾回收的时机不同

    • 强引用:如果有强引用到一个对象,那么该对象不会被回收
    • 弱引用:只有弱引用链接的对象,在系统进行GC的时候就会被垃圾回收
    • 软引用:只有内存不够的时候,在会被GC回收
    • 虚引用:任何时候都可以被回收

    神奇的0x61c88647

    在ThreadLocalMap的hash算法中,很少发生碰撞,原因在于精巧的hash算法

    private final int threadLocalHashCode = nextHashCode();
    
    private static final int HASH_INCREMENT = 0x61c88647;
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
    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);
    }
    

    这里最不解的是为什么要用0x61c88647这个数?怎么来的?
    0x61c88647换算成十进制是1640531527,计算方法如下
    1640531527 = (long) ((1L << 31) * (Math.sqrt(5) - 1))
    (Math.sqrt(5) - 1) / 2 是黄金分割数
    这种hash方法是Donald Knuth在 The Art of Computer Programming 中提出,不明觉厉

    问题

    清除stale数组元素的标准就是 key == null,什么时候ThreadLocal.key为null?
    key为null:因为key是ThreadLocal的弱引用,当ThreadLocal没有强引用的时候,ThreadLocal变量可能会被回收,这个时候出现了key为null的情况

  • 相关阅读:
    查看每个核的资源情况
    什么时候使用NO_UNNEST
    走FILTER效率高的2种情况
    PL/SQL 包头和包体
    产品研发要配合好
    ElasticSearch 文档并发处理以及文档路由
    ES(ElasticSearch) 索引创建
    BaikalDB技术实现内幕(三)--代价模型实现
    腾讯位置服务地图SDK自定义地图和路况
    mysql数据库优化
  • 原文地址:https://www.cnblogs.com/sunshine-2015/p/6072184.html
Copyright © 2020-2023  润新知