一篇没人看的ThreadLocal源码详解的枯燥文章

ThreadLocal详解

ThreadLocal有什么用

Synchronized的作用是同步线程使它们能安全地对共享变量进行操作,而ThreadLocal它的作用就是进行线程间的数据隔离,即每个线程都有自己的一个变量副本,彼此不会影响对方的数据。先来实践感受一下:

public class ThreadLoaclTest {
    static ThreadLocal<Integer> tl = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {

        Runnable r = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    tl.set(i);
                }
                System.out.println(Thread.currentThread().getName() + " " + tl.get());
            }
        };

        Thread t1 = new Thread(r, "t1");
        Thread t2 = new Thread(r, "t2");
        Thread t3 = new Thread(r, "t3");

        t1.start();
        t2.start();
        t3.start();
    }
}
t1 99
t2 99
t3 99

从结果中可见,虽然三个线程都是对同一个ThreadLocal对象进行操作,但是每个线程的值都没有被其他线程所影响。

首先解释一个误区

大家都知道ThreadLocal有个内部类ThreadLocalMap,其有个Entry节点类,ThreadLocalMap中是使用Entry节点来实现K、V映射的,但是在JDK1.8中,Entry节点的K并不是线程,而是ThreadLocal对象实例,V是这个ThreadLocal对象实例的值。并且这个ThreadLocalMap并不是ThreadLocal在维护,而是Thread在维护,即每个线程都有一个ThreadLocalMap

JDK1.8改变了ThreadLocal的设计,有什么好处呢?

之前是ThreadLocal维护ThreadLocalMap,Thread作为K,那么有多少个ThreadLocal就有多少个ThreadLocalMap,而有多少个线程,ThreadLocal中就有多少个Entry节点。这样就造成了一个问题,一般情况下ThreadLocal的个数很少,而线程数很多。就会造成单个ThreadLocalMap很长,在获取元素时效率很低。

而JDK1.8之后决定ThreadLocalMap中元素个数的的ThreadLocal,所以不会很长,查询效率高,也没有因此而增大内存占用。

ThreadLocal的结构

public class ThreadLocal<T> {
 	private final int threadLocalHashCode = nextHashCode();
    private static AtomicInteger nextHashCode = new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647;
	static int nextHashCode() {
	        return nextHashCode.getAndAdd(HASH_INCREMENT);
	    }
	static class ThreadLocalMap {
	        static class Entry extends WeakReference<ThreadLocal<?>> {//可以看到这里Entry节点弱引用了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;
	        private int threshold; // 扩容阈值
}

上面为简化之后的代码,可以看见ThreadLocal中有个内部类ThreadLocalMap,ThreadLocalMap中也有个内部类Entry,Entry弱引用了ThreadLocal,这样防止发生OOM。

赋值操作

set(T value)

public void set(T value) {
        Thread t = Thread.currentThread();//获取当前线程
        ThreadLocalMap map = getMap(t);//获取当前线程维护的ThreadLocalMap
        if (map != null)//如果当前线程维护的ThreadLocalMap已经初始化
            map.set(this, value);//构造Entry节点加入ThreadLocalMap
        else
            createMap(t, value);//初始化并构造Entry节点加入ThreadLocalMap
    }
getMap(Thread t) 和Thread中的threadLocals属性
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到getMap只是将当前线程的threadLocals属性返回,也印证了ThreadLocalMap是由Thread来维护的。

map.set(ThreadLocal<?> key, Object value)
private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);//计算下标

    		//从下标位置开始遍历ThreadLocalMap
            for (Entry e = tab[i];
                 e != null;
                 //这里更新了i,把i更新为i+1
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
				
                //找到ThreadLocal对应的Entry节点,做更新操作
                if (k == key) {
                    e.value = value;//更新值
                    return;
                }
				//Entry不为空但K为空(即ThreadLocal为空,因为它是弱引用,所以可能被回收了)
                if (k == null) {
                    replaceStaleEntry(key, value, i);//将节点移除
                    return;
                }
            }
    		//使用开放寻址法解决哈希冲突
			//没有找到节点,添加节点,这里i已经被更新过了
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
createMap(Thread t, T firstValue)
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

调用这个方法初始化线程的ThreadLocalMap,并添加第一个节点。

查询操作

get()

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);//同样获取当前线程的ThreadLocalMap
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);//根据ThreadLocal获取节点
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;//获取这个节点的V
                return result;
            }
        }
        return setInitialValue();
    }
getEntry(ThreadLocal<?> key)
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);
        }

如何解决哈希冲突

计算下标的代码如下:

int i = key.threadLocalHashCode & (len-1);

&(len-1)这一部分大家想必十分熟悉,同HashMap一样是为了提高计算效率。

最主要的是:key.threadLocalHashCode

	private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode = new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

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

可以看到threadLocalHashCode的值是通过AtomicInteger类的方法getAndAdd去在原始值上增加一个十六进制数来得到的。

第一个关键点:threadLocalHashCode是被fianl修饰的

所以每个ThreadLocal对象的threadLocalHashCode都是固定的

第二个关键点:nextHashCode是静态的

说明每个ThreadLocal对象都共享同一个nextHashCode,nextHashCode在nextHashCode()方法中被修改,不断再原值上增加HASH_INCREMENT,而nextHashCode()方法只会被调用一次,就是当对象实例化的时候。这样就实现了不同的ThreadLocal对象的threadLocalHashCode基本不会相同

我们通过一个例子来说明一下:

import java.util.concurrent.atomic.AtomicInteger;

public class Main {

    private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode = new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

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

    public static void main(String[] args) {
        Main a = new Main();
        Main b = new Main();
        Main c = new Main();
        for (int i = 0;i<2;i++){
            System.out.println("第"+(i+1)+"次打印");
            System.out.println(a.threadLocalHashCode);
            System.out.println(b.threadLocalHashCode);
            System.out.println(c.threadLocalHashCode);
        }
    }
}
1次打印
0
1640531527
-10139042422次打印
0
1640531527
-1013904242

可以看到,我们根据ThreadLocal的样子构建了一个Main类(下面我们把它当作ThreadLocal类),在main方法中新建三个ThreadLocal对象,打印这三个ThreadLocal对象的threadLocalHashCode,每个都不一样,但是每次打印三个的值都不会变。

因为我们在new第一个ThreadLocal对象实例a的时候,就通过nextHashCode()方法给它的threadLocalHashCode赋了初始值,因为这个时候nextHashCode第一次使用,所以为0;创建第二个ThreadLocal实例b时,同样的流程,但这时候因为a曾经调用过nextHashCode()方法把nextHashCode更新了(因为nextHashCode.getAndAdd(HASH_INCREMENT)),所以b的threadLocalHashCode也就变了;创建c的时候同理。

至于为什么要nextHashCode.getAndAdd(HASH_INCREMENT)给原值加上HASH_INCREMENT这个值,那是因为不断加这个值形成的队列类似斐波那契数列,十分契合2^n数组。可以看一下下面的演示:

for (int i = 0;i<16;i++){
            System.out.print((nextHashCode()&15)+" ");
        }
5 12 3 10 1 8 15 6 13 4 11 2 9 0 7 14 

可以看到假设ThreadLocalMap的容量是16,那么拿nextHashCode()&(16-1)计算出来的16个下标竟然没有重复可见对于2^n容量的数组,这个算法计算出来的下标出现哈希冲突的概率很小很小。

总结

ThreadLocal依靠在Thread中维护的ThreadLocalMap(初始容量n=16,扩容阈值为n*2/3)来实现数据隔离,以ThreadLocal对象为K,以要存的值为V构建Entry节点,然后通过ThreadLocal的哈希值来计算在ThreadLocalMap中的存储下标,将Entry节点存进去。这样每个线程都有自己的一份ThreadLocal和值的副本,实现数据隔离。要获取值时,就通过ThreadLocal去ThreadLocalMap里面找,也是获取它的哈希值然后计算下标并找到Entry,假如Entry的K为空,说明弱引用的ThreadLocal被回收了,就从ThreadLocalMap中删除,否则返回这个Entry的V。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值