ThreadLocal分析详解,这一篇就够了

在这里插入图片描述

先简单总结一下,等看完代码分析后,可以在回来看一下上面的图。每个线程中都持有一个ThreadLocalMap对象,ThreadLocalMap中又保存了ThreadLocal - value键值对。

代码分析

先看一个demo,后面会以此来分析

private ThreadLocal<String> mThreadLocal = new ThreadLocal<>();

@Override
protected void onCreate(Bundle savedInstanceState) {
    ......
    //主线程
    mThreadLocal.set("I'm main");
    Log.d("main ThreadLocal", mThreadLocal.get() + "");

    //子线程
    new Thread(new Runnable() {
        @Override
        public void run() {
            Log.d("child ThreadLocal", mThreadLocal.get() + "");
            mThreadLocal.set("I'm child");
            Log.d("child ThreadLocal", mThreadLocal.get() + "");
            mThreadLocal.set("I'm child2");
            Log.d("child ThreadLocal", mThreadLocal.get() + "");
        }
    }, "child").start();
}

打印信息:
 D/main ThreadLocal: I'm main
 D/child ThreadLocal: null 
 D/child ThreadLocal: I'm child
 D/child ThreadLocal: I'm child2

ThreadLocal的核心方法是setget,下面就跟随这俩个方法去作分析。

1、set方法

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //查询当前线程是否已创建ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //已创建,则更新
        map.set(this, value);
    else
        //未创建,则新建
        createMap(t, value);
}

上述代码逻辑很简单,首次map对象是为null(非ui线程),所以走createMap方法。

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

此处就可以看出如上图所描述的关系:

1、ThreadLocalMap中包含了ThreadLocal对象和value值。
2、通过threadLocals赋值,将线程和ThreadLocalMap绑定。

static class ThreadLocalMap {
    ......
	  //Entry是一个单key-value的类,用来保存ThreadLocal对象和其对应的value值
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    ......
    //数组容器的初始容量,必须为2的幂次方
    private static final int INITIAL_CAPACITY = 16;
    //保存所有 ThreadLocal-value 的数组容器
	  private Entry[] table;    
    
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //创建数组
        table = new Entry[INITIAL_CAPACITY];          
        //计算数组下标
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //保存ThreadLocal对象和firstValue值,并将其保存到数组中
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        //设置阀值,扩容需要
        setThreshold(INITIAL_CAPACITY);
    }
    ......          	  
}

构造方法中,Entry用来保存参数ThreadLocal对象和其对应的value值的,table数组保存了所有Entry对象。

所以,ThreadLocal - firstValue键值对最终以Entry的形式保存到数组table中了,其保存的位置是根据参数ThreadLocal计算出的一个下标值。

我的几个疑问

  • 每个线程通过ThreadLocal.set只能保存一个值,也就是一对ThreadLocal - Value键值类Entry,为什么会使用table数组保存?

    我们知道多个线程可以共用一个ThreadLocal对象来存取数据。当然也可以一个线程拥有多个ThreadLocal对象了。这样线程就包含了该线程所持有的所有ThreadLocal - Value。跟踪调试发现确实如此。

    示例:

    new Thread(new Runnable() {
       @Override
        public void run() {
            mThreadLocal.set("I'm child");
    //      Log.d("child ThreadLocal", mThreadLocal.get() + "");
            ThreadLocal<String> thre = new ThreadLocal<>();
            thre.set("I'm child2");
        }
    }, "child").start();
    

    在这里插入图片描述
    在这里插入图片描述

  • 数组下标是怎么计算的?为什么要这么计算

    firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    
    key.threadLocalHashCode & (len-1);
    
    k.threadLocalHashCode & (len - 1);
    
    ................
    
    private static AtomicInteger nextHashCode = new AtomicInteger();
    private final int threadLocalHashCode = nextHashCode();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        //自增, 但返回的是上一次值。类似 N++
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }	
    

    从代码可看出,下标跟threadLocalHashCodetable的长度有关:
    &上len-1保证了下标的范围为0~最大下标
    &上threadLocalHashCode的自增保证了不同ThreadLocal对应的threadLocalHashCode,防止碰撞
    该疑问可以结合下面的问题一块了解

  • 数组容器的初始容量,为什么必须为2的幂次方?貌好多容器也是如此,如HashMap

    主要是为了提高效率。这块可以看一下HashMap的原理分析。
    如果不了解,我这块举个例子,简单说明一下,比如容量分别为16和10:

    • 16-1=15,15的二进制为0000 1111,这样某个值与其按位&的话,其取值范围为0~15
    • 10-1=9, 9的二进制为0000 1001,这样某个值与其按位&的话,其取值为0、1、8、9

    很明显,容量不是2的幂次方的可用的值比较少,这样一是很容易碰撞(即有些数据算出来的下标非常容易相同)。二是造成资源浪费,因为本来有10个位置(桶),而只是使用了0、1、8、9四个位置,其他6个位置浪费掉了。

    那为什么不用求余来计算位置呢?
    两者的关联: 某值(如20:0001 0100) & 0000 1111 = 100(十进制正好为4 == 20 % 16)
    求余也是可以了,记得早期的HashMap中好像就是用的该方式(记得不是很清楚了),但是这样会有几个问题,一是负数处理比较麻烦。二是位算法比求余算法能快些,毕竟求余最终还是通过二进制去处理的。

2、get方法

159    public T get() {
 		   //获取当前线程	      
160        Thread t = Thread.currentThread();
		   //获取当前线程的ThreadLocalMap对象
161        ThreadLocalMap map = getMap(t);
162        if (map != null) {
				//根据ThreadLocal得到值value
163            ThreadLocalMap.Entry e = map.getEntry(this);
164            if (e != null) {
165                @SuppressWarnings("unchecked")
166                T result = (T)e.value;
167                return result;
168            }
169        }
		   //map为null(如线程中未执行过set方法),则去初始化
170        return setInitialValue();
171    }
	   
	   ......
	   
232    ThreadLocalMap getMap(Thread t) {
233        return t.threadLocals;
234    }

分析完set后,这块就很简单了,我们主要看一下setInitialValue的内容。

179    private T setInitialValue() {
180        T value = initialValue();
181        Thread t = Thread.currentThread();
182        ThreadLocalMap map = getMap(t);
183        if (map != null)
184            map.set(this, value);
185        else
186            createMap(t, value);
187        return value;
188    }
	   
	   ......
	   
126    protected T initialValue() {
127        return null;
128    }

setInitialValueset方法很像,只是value值为初始化的null,而不是传参进来的。


参考

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值