Java ThreadLocal 源代码分析

Java ThreadLocal

之前在写SSM项目的时候使用过一个叫PageHelper的插件

可以自动完成分页而不用手动写SQL limit

用起来大概是这样的

最开始的时候觉得很困惑,因为直接使用静态成员函数,那么就意味着如果有别的线程同时执行,可能会导致一些并发错误

答案是不会,因为PageHelper内部实现是使用到了ThreadLocal这个对象的,每个线程单独使用一个Page对象

百度了一下,发现ThreadLocal是一个提供类似线程内部的局部变量

我们来看一下ThreadLocal的源代码:初始化的时候涉及到这几个变量

private static AtomicInteger nextHashCode =
	new AtomicInteger();
private final int threadLocalHashCode = nextHashCode();

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
	return nextHashCode.getAndAdd(HASH_INCREMENT);
}

每次创建一个ThreadLocal的时候就会给这个?ThreadLocal分配一个hashcode

为什么不是每次increateAndGet 注释里面有解释:

连续生成的散列码的区别

隐式顺序线程局部ID进入近最优分布

两个大小表的幂的乘法哈希值。

首先看一下get方法

//ThreadLocal.java
	public T get() {
        //首先获取当前的Thread
        Thread t = Thread.currentThread();
        //通过当前Thread尝试获取 ThreadLocalMap
        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;
            }
        }
        //当前Thread并没有初始化map或者Thread值,进行初始化操作
        return setInitialValue();
    }

通过名字可以知道ThreadLocalMap似乎是个Map

我们先查看一下getMap

发现这个map是存储在Thread 里面的,包作用域,用户不可见

//Tread.java class Thread
ThreadLocal.ThreadLocalMap threadLocals = null;

我们现在看一下这个ThreadLocalMap的定义

static class ThreadLocalMap {
    //定义键值对,是一个WeakReference
        static class Entry extends WeakReference<ThreadLocal<?>> {
            //ThreadLocal里面保存变量的值
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    //默认初始容量
        private static final int INITIAL_CAPACITY = 16;
    //桶
        private Entry[] table;
        private int size = 0;

初始化:

我们可以看到第一次初始化的时候是使用firstKey的threadLocalHashCode(firstKey指的是外部的this)刚才提到的初始化的时候分配的一个hashcode,具体桶的位置跟hashmap类似都是(桶-1)&hashcode

//ThreadLocal.java 
//static class ThreadLocalMap    
	ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

ThreadLocalMap里面的 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);
        }

ThreadLocalMap解决hash冲突的方法并不是用链表,而是使用线性探测法

这也就解释了为什么分配的hashcode不应该是连续的原因,否则一旦出现hash冲突,线性探测找到一个可用的空间或者key对应的值非常艰难

我们来看一下是如何实现线性探测的:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
//当这个位置不是空的时候继续探测
    while (e != null) {
        ThreadLocal<?> k = e.get();
        //判断key是不是相等,如果相等说明找到了
        if (k == key)
            return e;
        //这里不太理解,查了一下别人的分析
        //k==null说明这个key已经被释放掉,需要清理掉
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);//((i + 1 < len) ? i + 1 : 0);就是下一个空间,如果到末尾就从头开始
        e = tab[i];//下一个空间里面的值
    }
    return null;
}
//这个函数不太理解
//我猜大概意思就是需要别的地方有一个ThreadLocal引用否则ThreadLocal可能被清理掉
//弱引用会被GC标记存活
//这个做的应该是标记为null之后,把后面的值放到前面,否则再次get的时候碰到null就找不到了
/**
 * 这个函数是ThreadLocal中核心清理函数,它做的事情很简单:
 * 就是从staleSlot开始遍历,将无效(弱引用指向对象被回收)清理,即对应entry中的value置为null,将指向这个entry的table[i]置为null,直到扫到空entry。
 * 另外,在过程中还会对非空的entry作rehash。
 * 可以说这个函数的作用就是从staleSlot开始清理连续段中的slot(断开强引用,rehash slot等)
 */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            //直接置null
            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)) {
                //每次线性探测一个格子直到找到null
                ThreadLocal<?> k = e.get();
                //key为null说明需要清理掉
                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;
        }

并发安全问题:

代码中没有使用到任何锁和同步,为什么还是安全的

因为每个线程操作的ThreadLocalMap都是每个线程自带的,当然不用同步啦

具体使用:比如说解决SimpleDateFormat的问题

这样每个线程只会创建一个

具体的线性探测hash可以看着里面的图

https://www.cnblogs.com/micrari/p/6790229.html

原文地址:https://www.cnblogs.com/stdpain/p/10661886.html