Java ThreadLocal源码解析

本文是对Java ThreadLocal(Java8)的源码的解析,对ThreadLocal基本使用还不了解的朋友可先快速学习ThreadLocal后再来阅读本文。

set方法

set方法可以让多个线程保存同一变量的副本。基本使用代码如下:

threadLocal.set(data);

那么为什么ThreadLocal可以起到线程隔离的作用呢?这就要进入set方法源码一探究竟了。

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
 			//1
            map.set(this, value);
        else
        	//2
            createMap(t, value);
}

        代码很短,先获取当前的线程,也就是此时调用ThreadLocal的set方法的线程,然后获取ThreadLocalMap,ThreadLocalMap不空就设值,空就创建一个ThreadLocalMap。
        ThreadLocalMap是ThreadLocal的静态内部类,从名字也可以看出来它是用于存储ThreadLocal的。跳进getMap方法可以看看。

//ThreadLocal.java
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
}

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

        直接返回Thread的ThreadLocalMap,也就是说每个线程都自带一个ThreadLocalMap来存储不同的ThreadLocal。注意,在Java8以前ThreadLocalMap是ThreadLocal成员变量,Java8开始就变了。
        线程创建时,它的ThreadLocalMap是空的。也就是说线程第一次调用set方法设值时,会运行代码2创建LocalThreadMap,下次再调用set才会运行代码1。先看看createMap方法做了什么。

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
}

代码很简单就是让当前调用set方法的线程new一个ThreadLocalMap。再看看它的构造方法做了什么。

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

/**
 * The initial capacity -- MUST be a power of two.
 */
private static final int INITIAL_CAPACITY = 16;

        代码1的table是一个数组,用来保存Entry的,初始容量为16。初始容量的注释上解释道容量必须为2的n次方。接下来进行哈希运算决定新值在数组的插入位置,最后设置一个初始阈值。可以看的出来ThreadLocalMap并是直接用Java自带的HashMap,而是自己实现了Map的相关操作。
        先看看代码1的Entry类,它是ThreadLocalMap的静态内部类。其实就是ThreadLocalMap存储的实体,以ThreadLocal为Key,set方法设置的值就是作为Value保存在Entry里的。Entry对ThreadLocal是弱引用,也就是说这个ThreadLocal在下一次Java GC时可能会被回收,采用弱引用的原因会在后面解释。

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
}

        先分析一下稍微简单的代码3,它给map设置了一个初始阈值,当map元素个数超过阈值,map就需要扩容了,可以看到这里的阈值就是容量的2/3。

private void setThreshold(int len) {
            threshold = len * 2 / 3;
}

回到刚刚的代码2,代码二是用来决定Entry在table数组的插入位置的。

...
//2
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
...
//ThreadLocal.java
private final int threadLocalHashCode = nextHashCode();

        插入位置就是ThreadLocal的哈希值和15(INITIAL_CAPACITY 为16)做与运算。threadLocalHashCode是一个常量,由nextHashCode方法得到,看看它的实现。

/**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
private static final int HASH_INCREMENT = 0x61c88647;
private static AtomicInteger nextHashCode = new AtomicInteger();
...
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

        threadLocalHashCode是得到的值其实就是上一次nextHashCode的值加上0x61c88647的结果(因为用的是getAndAdd方法,先得值在自加0x61c88647),为了保证自加是原子操作,所以nextHashCode使用AtomicInteger而非int。HASH_INCREMENT为什么选用0x61c88647可参考这篇文章
        创建了ThreadLocalMap并把值存入,createMap方法就结束了。那么,下一次ThreadLocal再存值时,就运行文章最开始的代码1,回顾一下。

if (map != null)
 	//1
    map.set(this, value);

跳进去看看。

private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            //1
            int i = key.threadLocalHashCode & (len-1);
			
			//2
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                if (k == key) {
                	//4
                    e.value = value;
                    return;
                }

                if (k == null) {
                	//5
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

			//6
			tab[i] = new Entry(key, value);
            int sz = ++size;

			//7
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

        首先,先在代码1处求得待插入的位置,但这可能并不是最终位置,因为哈希表在插入时有可能出现碰撞的现象(就是说不同的值可能计算出来的插入地址相同),而代码2的for循环就是用来检测插入位置是否冲突的。这个for循环的工作流程是这样的。
        如果插入位置是空着的,那没问题直接运行代码6插入就行了。如果插入位置被占用了,那就看看插入位置是否符合要求(代码),插入位置的ThreadLocal和现在调用set方法的ThreadLocal是同一个对象实例,说明本次的调用set方法是在更新副本变量值。
        如果比较后发现插入位置的ThreadLocal和本ThreadLocal不是同一个对象实例,这个时候才是真正的碰撞。这里使用了线性检测法来处理冲突。简单的说就是发现插入位置i碰撞,就看看i+1位置是不是空着的,i+1也被占用了就再看看下一个位置行不行,终于找到空位了插入即可。for循环里的nextIndex方法就是在寻找下一个插入位置。如果i已经是数组的最后一个位置了,那就回到第0个位置继续检测。这么一来就总会找到一个空位了。
        那么代码5是什么意思,字面上看就是取代旧的Entry。上面说过Entry是ThreadLocal的弱引用,进行一次GC后,Entry所引用的ThreadLocal可能会为空,Entry引用为空又占着table数组的一个空位,这个Entry里保存的副本值也就成了脏数据,此时调用replaceStaleEntry就是在重新利用这个Entry。
        再重新读for循环的代码会发现,更新副本值(代码4)和重新利用已有的Entry(代码5)是不会消耗table数组空位的。可代码6新插入的Entry就不一样了,为了保证table数组的空位的到充分利用,每次新插入后都要清理一下脏的Entry及在必要时扩容,看看代码7的cleanSomeSlots方法。

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;
                    //1
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

        刚刚提到过Entry引用的ThreadLocal可能会被回收,导致Entry携带脏数据,这个方法就是用来查找并回收这类Entry的。通过do-while循环查找,发现了Entry携带脏数据就调用代码1的expungeStaleEntry方法回收它。

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;
            ...
}

把Entry的副本引用设为空,并将它从table数组里移除,接下来等垃圾回收器回收就行了。重新看看代码7的判断。

if (!cleanSomeSlots(i, sz) && sz >= threshold){
	refresh();
}

        cleanSomeSlots方法里如果找到了携带脏数据的Entry就会返回true,这么一来,只有在出现脏Entry且ThreadLocalMap容量只剩不到1/3时(threshold设定为总容量的2/3)才会调用refresh方法刷新,看看它做了什么。

private void rehash() {
			//1
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            //2
            if (size >= threshold - threshold / 4)
                resize();
}
...
private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
}

        现在代码1调用expungeStaleEntries方法做最后一次大回收,如果这次大回收后容量依然还是很低,那就只能调用resize方法扩容了。
        expungeStaleEntries之所以叫大回收,是因为它直接把这个table数组遍历了一遍,这样就能最大范围地回收没用的Entry了。

get方法

ThreadLocal的get方法基本使用如下:

value = threadLocal.get();

看看源码

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	//1
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //2
        return setInitialValue();
}

        先获取当前线程,在获取这个线程的ThreadLocalMap,代码1获取到存储变量副本的Entry,不空就直接返回变量副本,空就调用代码2的setInitialValue方法设置初值。先来分析代码1的getEntry方法。

private Entry getEntry(ThreadLocal<?> key) {
         int i = key.threadLocalHashCode & (table.length - 1);
         Entry e = table[i];
         //1
         if (e != null && e.get() == key)
             return e;
         else
         	//2
             return getEntryAfterMiss(key, i, e);
}

代码1先判断ThreadLocal是否匹配,上面讲过,可能出现ThreadLocal可能会被回收导致Entry返回空 又或者 碰撞监测导致插入位置后移,如果匹配就返回,不匹配就调用getEntryAfterMiss方法。

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)
                	//1
                    expungeStaleEntry(i);
                else
                	//2
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
}

代码1是在重新利用没用的Entry,代码2是在寻找下一个合适的插入位置,上面讲过了。回到刚才getEntry方法,如果连ThreadLocalMap都为空,那么还得对ThreadLocalMap初始化,就是在setInitialValue方法中进行的。

...
	//2
	return setInitialValue();
}
private T setInitialValue() {
		//1
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

protected T initialValue() {
        return null;
}

        代码1的获取副本的初始值,注意,如果不重写initialValue方法,返回值必然是空值。接下来对ThreadLocalMap初始化操作和刚刚讲解的set方法是一样的。如此一来ThreadLocal的get方法就解析完了。

remove方法

基本使用如下。

threadLocal.remove();

再看看源码。

//ThreadLocal.java
public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
}

//ThreadLocalMap.java
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();
                    //1
                    expungeStaleEntry(i);
                    return;
                }
            }
}

寻找Entry的方法还是一样的,只是这回是将Entry回收(代码1)。

内存泄漏问题

        上面讲到过Entry对ThreadLocal是弱引用,为什么要使用弱引用,而不使用强引用?可以先想想使用强引用会有怎样的后果。
        ThreadLocalMap使用了线性检测法来判断处理碰撞。检测到i位置碰撞就看看i+1位置是否能用。问题就出现在这里。如果使用强引用除非手动把table数组的元素置空,否则GC是不会回收这个Entry的,即使Entry所引用的ThreadLocal及对应的变量副本确实没用了。ThreadLocal没用了又不能回收,table数组的元素越来越多,碰撞也越来越严重,如此恶性循环,能用的内存就越来越小了。
        这回再想想用弱引用就能明白它的道理了。有一篇文章讨论过要不要使用ThreadLocal的remove方法。个人理解是这样的,ThreadLocalMap采用弱引用策略已经起到回收没用的Entry的作用,只是remove方法让程序更早地解除相关的引用关系,算是一种保障吧。

ThreadLocal 和 Thread私有成员变量 的区别

来看看下面这两个类。

class MyThread extends Thread{
	private int mCount = 0;

	public int getCount(){
		return mCount;
	}

	public void increase(){
		mCount++;
	}
}

class MyThread2 extends Thread{
	private ThreadLocal<Integer> mCount = new ThreadLocal();

	public int getCount(){
		return mCount.get();
	}

	public void increase(){
		mCount.set(getCount());
	}
}

        这两个类对mCount操作的效果是一样的,但使用ThreadLocal和成员私有变量的出发点是不一样的。ThreadLocal是作用在不同的线程空间上的,而私有成员变量是作用在不同的对象实例上的。在MyThread2里 ,想要改变mCount只能在存储它的线程里修改。而在MyThread里,可以在不同的线程里改变同一线程的mCount。

参考文章

1.为什么使用0x61c88647
2.处理哈希冲突的线性探测法
3.使用ThreadLocal到底需不需要remove?

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值