实战java高并发程序设计之ThreadLocal源码分析

xiaoxiao2022-06-12  40

ThreadLocal类在面试中经常问到,它的作用,使用场景,如何实现等等问题。所以对它的学习也是十分有必要的。

使用场景

很多书中经常举多线程中数据库连接的例子来说明ThreadLocal的使用场景。具体的可以看这篇博客,在这里我总结下什么情况下应该使用ThreadLocal,使用ThreadLocal应该满足以下几个条件:①前提是多线程环境下的使用;②各线程都需要使用到这个变量;③这个变量被各线程频繁的使用;④各线程间变量是隔离不会影响的。看起来这些场景很奇怪,如果满足④为什么不使用线程私有变量?②也表明私有变量能够解决这个问题,为什么不用呢?这些问题都留到最后来解答吧,先看下面一个例子。

代码实例

举例说明:一个公司包括很多部门,有技术部门,财务部门以及业务部门等;每个部门都有一定的杂事需要处理,比如素质拓展、团队建设、业绩考核等工作,这里我们把处理杂事的人或者组织称为HRBP(人事经理)。HRBP有名字和工作时间:

public class HRBP { private String name; // 名字 private String date; // 工作时间 为了方便用字符串代替 public void setName(String name){ this.name = name; } public String getName(){ return name; } public void setDate(String date){ this.date = date; } public String getDate(){ return date; } @Override public String toString() { return "HRBP [name=" + name + ", date=" + date + "]"; } }

把各部门看做不同的线程;假如各部门在同一时间都需要开展素拓活动,很明显,这个时候HRBP忙不过来,无法顾及三个部门,为了保证部门工作的顺利开展,需要错开时间,也就是异步进行;还有一种解决方法:为每个部门配备一个HRBP,单独负责各部门的工作,但考虑每个部门并不是很忙,比如技术部门活动和工作都安排在月初、而财务则在月底较忙、业务部门则在下半年比较忙,所以为每个部门配备一个HRBP显得有点资源浪费,所以可以通过错开时间而开展各部门的工作。

public class ThreadLocalTest { private static ThreadLocal<HRBP> hrpbTLocal = new ThreadLocal<HRBP>(); public static void main(String[] args) { final HRBP hrbp = new HRBP(); hrbp.setName("zhangsan"); hrbp.setDate("all years"); // 财务部门 new Thread(new Runnable() { @Override public void run() { hrbp.setDate("late month"); // 集中在月底时间处理财务部门的管理事情 hrpbTLocal.set(hrbp); System.out.println(Thread.currentThread().getName() + ": " + hrpbTLocal.get()); } }, "treasurersDept").start(); // 技术部门 new Thread(new Runnable() { @Override public void run() { hrbp.setDate("first month"); // 集中在月初处理技术部门的管理事情 hrpbTLocal.set(hrbp); System.out.println(Thread.currentThread().getName() + ": " + hrpbTLocal.get()); } }, "technologyDept").start(); // 业务部门 new Thread(new Runnable() { @Override public void run() { hrbp.setDate("last half year"); // 集中在下半年处理业务部门的管理事情 hrpbTLocal.set(hrbp); System.out.println(Thread.currentThread().getName() + ": " + hrpbTLocal.get()); } }, "operatingDept").start(); } }

代码运行结果:

treasurersDept: HRBP [name=zhangsan, date=late month] technologyDept: HRBP [name=zhangsan, date=first month] operatingDept: HRBP [name=zhangsan, date=last half year]

代码里把各部门作为各线程,各线程占有hrbp的时间分别不同,但都是”zhangsan”这个hrbp。 例子和代码结合可以概括为: 起初,一个hrbp被三个线程共享,但当三个线程在某时刻都需要hrbp时,只能同步访问,导致访问效率比较低下;为了提高各部门的工作效率,可以为三个部门分别提供一个hrbp,但考虑到每个部门的忙活时间段不同,配备三个hrbp显得有点资源浪费,所以为了效率和资源的充分利用,hrbp分别在不同的时间段侧重不同的部门,这就好比为每个线程配备一个私有的hrbp,各个线程之间的工作互不影响。此时回头看看使用场景,或许能豁然开朗。 hrbp在三个不同的线程里是如何做到时间分配的?即某个对象的属性在多个线程里是如何做到互不干扰的?此时,ThreadLocal就出来了。

源码解读

ThreadLocal类意思是线程本地变量,变量只在本线程内有用。

3.1 构造方法
public ThreadLocal() {} // 创建一个线程本地变量

在上面的例子中:

ThreadLocal<HRBP> hrpbTLocal = new ThreadLocal<HRBP>();

表示创建一个线程本地变量,变量中可放HRBP类型的对象;为方便使用用private和static修饰符修饰hrpbTLocal对象。

3.2 set()方法

代码中新建并启动三个线程,各线程中调用hrpbTLocal对象的set(),放入修改后的hrbp对象;从结果看出貌似生成了三个hrbp对象,那么底层是如何实现的呢?接下来看一下set():

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

逻辑貌似很简单,分为以下三个步骤: 1> 获取当前线程t; 2> 根据当前线程t调用getMap()返回一个ThreadLocalMap对象map; 3> map对象不为空则调用set()把value放进去,否则调用createMap()创建新的对象;

3.2.1 ThreadLocalMap

ThreadLocalMap何许类也?让我们通过getMap方法一探究竟!

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

t为当前线程,getMap方法返回的是当前线程的全局变量threadLocals;

ThreadLocal.ThreadLocalMap threadLocals = null;

threadLocals默认为空;同时,可见ThreadLocalMap类是ThreadLocal类的内部类;通过看ThreadLocalMap类源码发现以下几点: 1> ThreadLocalMap类并非实现Map接口;它是一个定制的哈希映射,用于维护线程本地值; 2> 哈希键值对中的键一般为ThreadLocal对象,并采用weakReferences引用,方便线程本地的垃圾回收;

3.2.2 createMap()

在本实例中,默认的threadLocals为空,因此调用createMap(t,value)方法新建ThreaLocalMap对象;

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

t为当前线程,this为threadLocal对象;

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对象要完成以下几件事: 1> 生成一个INITIAL_CAPACITY(16)大小的Entry类型的数组; 2> 根据threadLocal对象的hashCode值与15做与运算得到entry对象的存储位置;并设置entry在table数组中的个数为1; 3> 设置调整阈值,以在最坏的情况保持2/3的负载因子; 这里可以看出,每个线程的threadLocal对象其实都是同一个,只不过存储的方式是取threadLocal对象的hashCode值作为key,而能存储各线程的value值。

至此,set方法过程已经结束。从上面代码机制中可以看出:ThreadLocalMap是延迟创建,只有当有线程本地值添加以后才会新建对象;而且建立的map对象中,键固定为当前线程的threadLocal对象,且是弱引用类型的对象(线程可建立多个threadLocal对象)。

3.2.3 ThreadLocalMap.set()

在此,我觉得非常有必要看一下当新建第二个threadLocal对象时发生的事情,此时调用map.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); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

代码可以分为以下几步: 1> 当前threadLocal对象的hashCode值与数组长度(16)减1做位运算得到entry对象在table中的存储位置;(为什么要减1?) 2> 如果当前位置有值(entry对象),则判断entry对象的键与传进来的threadLocal是否是同一个threadLocal对象,若是则直接覆盖并结束,若不是则调用nextIndex()方法寻找下一个位置; 3> nextIndex()不采用重新hash而是直接加1寻找下一个存储entry对象的位置;这样做是因为set动作比较频繁,重新hash之后失败率更高。

private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }

4> 若当前entry不为空,但entry对象上的键为空,则在该位置插入键值对取代原来的键值对并结束。 5> 若当位置为空,则new一个entry放在该位置; 6> 若该位置后无空闲entry对象且entry对象数量大于等于阈值(数组长度与负载因子(2/3)的乘积),则rehash()数组(容量扩大为原来的2倍)。

3.2 get()

上面分析了如何往ThreadLocal对象中set值,下面看看如何取值

public T get() { Thread t = Thread.currentThread(); 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; } } return setInitialValue(); }

get方法相对比较简单,分为以下几步: 1> 通过当前线程t获得map对象; 2> 如果map对象不为空,则获取当前threadLocal键值对应的entry对象,并返回value; 3> 如果map对象为空,则调用setInitialValue()方法创建ThreadLocalMap对象,并把键值对放入map对象中,最后返回value值;

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; }

可知与set()方法非常类似,只是多了第一行初始化value的代码(value=null)。按理来说返回一个不存在的map,直接返回null不就好了,为什么还要新建map呢?

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

调用ThreadLocalMap的remove()方法

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(); expungeStaleEntry(i); return; } } }

清除threadlocal对象对象的entry对象。

总结

经过以上的讨论分析可知以下几点: 1.ThreadLocal类作为查找线程本地变量value的key,而每个线程的threadLocal对象是共有的对象,只不过通过取hash值产生映射关系; 2.通过ThreadLocalMap类组件Entry对象,维持threadLocal与value的映射关系;为了便于GC,把threadLocal对象声明为弱引用; 3.ThreadLocalMap类并非实现Map接口,而是特制的哈希映射类,所以其没有entry链,在内部采用数组的形式存放映射关系,如果发生hash冲突,则通过+1的方式存储entry对象; threadLocal内部示意图

参考文献

《实战java高并发程序设计》 http://www.cnblogs.com/dolphin0520/p/3920407.html https://www.jianshu.com/p/98b68c97df9b https://www.cnblogs.com/coshaho/p/5127135.html

转载请注明原文地址: https://www.6miu.com/read-4932574.html

最新回复(0)