ThreadLocal源码:存取数据流程

1、回顾ThreadLocal

之前写过一篇ThreadLocal简单使用的日志,里面展示了一个例子,以及get()和set()方法的源码。ThreadLocal是不同于锁的方式解决了多线程访问临界资源的安全性问题,且某些场景下效率更高。最近也在读ThreadLocal的源码,发现很多知识点上一篇日志没有兼顾到(例如令我印象深刻的是线程中的ThreadLocalMap中维护的Entry[]数组是环形结构,数组的扩容阈值和用到了哈希函数和线性探测方式,都是当初学习数据结构与算法时满满的回忆哈哈),所以现在决定再写一篇自己对ThreadLocal学习总结的日志。

ThreadLocal的作用是把数据副本放入到该ThreadLocal实例所在的线程的Map中,Map中的key对应ThreadLocal的实例对象,value则对应我们放进去的数据。因为Map只属于当前线程,它是Thread类内部的一个实例变量,所以,只有当前线程可以访问到这个Map,对这个Map里面的数据做修改,所以可以保证多线程修改同一数据的安全性问题,通过隔离的方法,即把数据副本放入到各自线程的Map中去,各个线程修改自己的副本数据。

使用ThreadLocal还有一个很大的好处是,提高了效率。为了保证数据的安全性,我们很自然地会想到加锁,在对临界资源数据做修改前先加锁,修改操作完成后再释放锁,这种做法的确保护了数据的安全,但效率不高,因为一旦一个线程在修改临界资源时加锁了,那么其他想要访问这个临界资源的线程便不得不停下来,等待锁的释放,试想假如有大量线程因为等待锁而被阻塞,肯定导致整体效率不高。使用ThreadLocal则可以解决效率问题,正如上面所说,把数据副本放入到自己线程的Map中修改,线程之间互不干扰,也就不会有互相等待的问题了。

 

2、存储数据结构

2.1 ThreadLocalMap中的成员变量

    // ThreadLocalMap的初始大小capacity
	private static final int INITIAL_CAPACITY = 16;
	// Entry数组
	private Entry[] table;
	// Entry数组的大小
	private int size = 0;
	// Entry数组下一次扩容的阈值
	private int threshold;

可以看到,在线程的ThreadLocalMap中,有一个Entry[]数组,用来存放数据副本,这个Entry[]数组就是我们说的线程里的Map。前面说到,每一个Thread都有各自的Map,ThreadLocal类只是把数据放入到所在线程的Map中,然后Map中的key对应ThreadLocal类的实例,value则对应存储数据。但是注意,这个Map并不是并不是我们java.util.map包中的map,它只是形式上像Map,因为有key和value。那么这个key和value是怎么对应的呢?来看看这个存储在线程中的ThreadLocalMap,它的存储结点Entry是怎样定义的:

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

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

从Entry源码可以看到,它的构造方法中,需要一个ThreadLocal对象来对应,value存储数据,这是ThreadLocalMap中的结点结构。

2.2弱引用和强引用 – 会不会内存泄露?

可能你会关注到一个问题,Entry结点的存储结构里,为什么ThreadLocal对象要使用WeakReference弱引用呢?也就是说下一次GC垃圾回收时,这个ThreadLocal就会被回收,但是value不会。这种做法一开始我也不理解,看了些文章后,都说到是因为方便GC的问题,弱引用在下一次GC时机会被回收,这个可以理解。对于像HashMap这样的数据结构,key和value都不是弱引用,它们可以说是生命周期与线程绑定,只要线程没有被销毁,这些key和value绑定的结点就一直处于可达状态,不会被GC回收。

可是这样就有一个问题了,在ThreadLocalMap里的结点Entry,只有ThreadLocal是弱引用,value不是,也就是说value的生命周期是跟着线程走的,如果使用线程池的话,线程使用完后回到线程池“休眠”,而不是销毁,里面的value便跟着一起没有被GC回收,如果出现大量的value,最终可能会出现内存泄露。

这个问题解决方法在ThreadLocalMap的set(ThreadLocal<>, Object value)方法中得到了解决:

private void set(ThreadLocal<?> key, Object value) {
		
		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();
		}
	}

看到第15到18行,ThreadLocalMap中的set()方法会遍历整个Entry数组,当遇到键值key为null时,就会把当前要设置的key和value替换上去,这样就可以将key为null但value还存在的结点替换掉。

2.3 Entry[ ]数组环形结构与散列查找

怎么发现ThreadLocalMap中的Entry[]数组是个环形结构的?看源码,ThreadLocalMap是如何获得数组的下一个位置下标和上一个位置下标的:

// 获得下一个位置的下标
	private static int nextIndex(int i, int len) {
	    return ((i + 1 < len) ? i + 1 : 0);
	}

	// 获得前一个位置的下标
	private static int prevIndex(int i, int len) {
	    return ((i - 1 >= 0) ? i - 1 : len - 1);
	}

看获得下一个位置下标的方法,假设i+1 < len,则返回下标为 i+1,否则返回0,也就是回到数组起始位置。获得前一个位置下标的方法也是,若i-1 >= 0,下标没有越界,返回i-1,否则返回len-1,取到了数组末尾。由此可见,ThreadLocalMap中的Entry[]数组是一个环形结构。

在ThreadLocalMap的构造方法中,还发现了一个复杂的东西,原来它是一个哈希函数,往Entry[]数组中放入结点时,都会通过哈希函数获得哈希值,作为在Entry[]数组中的存放下标,然后初始化这个结点放入数组:

public ThreadLocalMap(java.lang.ThreadLocal<?> firstKey, Object firstValue) {
	    // 初始化table数组
	    table = new Entry[INITIAL_CAPACITY];
	    // 用firstKey的threadLocalHashCode与初始大小16取模得到哈希值
	    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
	    // 初始化该节点
	    table[i] = new Entry(firstKey, firstValue);
	    // 设置节点表大小为1
	    size = 1;
	    // 扩容阈值
	    setThreshold(INITIAL_CAPACITY);
	}

看到构造方法的第5行,获得结点在的Entry[]数组中存放下标的方式是通过一个哈希函数获得,也就是使用了散列查找。其中的threadLocalHashCode是一个在ThreadLocal被初始化时生成的int值,可以说是ThreadLocal的一个id,这个id并不是随机产生的,它的生成是在上一个ThreadLocal的id基础上,加上一个“魔数”得到(由于我没有理解其中的奥秘,所以对于这个魔数,大家可以自行查阅),从其他文章中我看到的是,通过这个魔数得到的threadLocalHashCode,可以让每一个结点在ThreadLocalMap中均匀分布,这个大家肯定知道,在数组中均匀分布,是为了在大量数据中,每次散列查找遇到冲突时,能更快地找到目标数据(ThreadLocalMap使用的是线性探测解决冲突问题)。

 

3、ThreadLocal – 存取数据过程

3.1 ThreadLocal.set()

public void set(T value)方法用来保存当前线程的副本变量值,它在ThreadLocal.java类中:

public T get() {
	    Thread t = Thread.currentThread();
	    ThreadLocalMap map = getMap(t);
	    if (map != null) {
	        ThreadLocalMap.Entry e = map.getEntry(this);
	        if (e != null)
	            return (T)e.value;
	    }
	    // 如果map为null,就初始化map.
	    return setInitialValue();
	}

set()方法首先取得当前线程的ThreadLocalMap,如果map不为空,则把ThreadLocal对象和value值保存到map中。因为此方法存在于ThreadLocal类中,所以this指的是当前的ThreadLocal。假如调用getMap()方法获取当前线程的map得到的是null,那就要先为当前线程创建一个ThreadLocalMap。

3.2 创建ThreadLocalMap

看完set()方法,你可能会有疑惑,为什么getMap()方法获取当前线程的map,第一次会返回null?不是说每一个线程都有自己的ThreadLocalMap吗?只不过向map中存取数据是由ThreadLocal来完成。没错,每一个线程都维护着自己的一个ThreadLocalMap,至于它为什么一开始为null?先来看看这个getMap()方法的源码:

ThreadLocalMap getMap(Thread t) {
	    return t.threadLocals;
	}
	public class Thread implements Runnable {
		// ThreadLocalMap的实例对象默认值是null
		ThreadLocal.ThreadLocalMap theadLocals = null; 
	}

可以看到,getMap()方法返回线程的实例变量threadLocals,类型是ThreadLocalMap,它的默认值是null。线程中的ThreadLocalMap是惰性构造的,只有当要往map里放入至少一个结点时,才会去创建这个map,所以才会出现第一次获取线程的ThreadLocalMap会返回null的情况。

接下来到创建ThreadLocalMap,方法同样在ThreadLocal.java类中:

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

createMap()方法就是创建一个新的ThreadLocalMap,并把当前的ThreadLocal实例作为key,数据副本firstValue作为value保存到map中。

3.3ThreadLocal.get()

最后到get()方法从ThreadLocalMap中获取value,来看源码:

public T get() {
	    Thread t = Thread.currentThread();
	    ThreadLocalMap map = getMap(t);
	    if (map != null) {
	        ThreadLocalMap.Entry e = map.getEntry(this);
	        if (e != null)
	            return (T)e.value;
	    }
	    // 如果map为null,就初始化map.
	    return setInitialValue();
	}

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

	private T setInitialValue() {
		// initialValue()方法默认返回null
	    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;
	}

第一步一样先获取当前线程的ThreadLocalMap,也就是那个对象threadLocals。如果map不为空,那么就从map中获取Entry结点,最后一步判断这个Entry结点是否为空,不为空就把结点中的value返回出去。get()方法的最后,如果一开始获取的map为空,那么就执行setInitialValue()方法,初始化一个map。注意我们前面说到,Thread中的map是惰性构造的,只有当要往map里放入至少一个结点时,才会去创建这个map,所以如果不往ThreadLocalMap中放入结点便直接调用get()方法,是会得到一个空的map。

从第17行看setInitialValue方法,它会先获取当前线程的ThreadLocalMap,如果此时map不为空,就把初始value(这里为null)和ThreadLocal对象设置进map中,并返回value。假如map为空,则创建一个map,并把初始value设置进去,最后返回value。

 

简单ThreadLocal示例代码:

https://github.com/justinzengtm/Java-Multithreading/tree/master/ThreadLoca

发布了97 篇原创文章 · 获赞 71 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/justinzengTM/article/details/100079186