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

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对象;
实战java高并发程序设计之ThreadLocal源码分析_第1张图片
threadLocal内部示意图

参考文献

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

你可能感兴趣的:(多线程/并发)