ThreadLocal分析

目录

1.简介

2.使用

3.源码分析

3.1 ThreadLocal源码分析

3.1.1 get()方法

3.1.2 set()方法

3.1.3 remove()方法

3.2 ThreadLocalMap源码分析

3.2.1 expungeStaleEntry(int staleSlot)

3.2.2 getEntry(ThreadLocal key)

3.2.3 getEntryAfterMiss()

3.3.4 void set(ThreadLocal key, Object value) 保存元素的流程

3.3.5 void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot)

3.3.6 boolean cleanSomeSlots(int i, int n)

3.3.7 void remove(ThreadLocal key)

3.3.8 其他方法

4.总结

4.1 ThreadLocal内存泄漏问题


1.简介

ThreadLocal作为线程并发安全的一种工具,是为缓存线程局部数据而设计的存储结构入口,作为java程序员或多或少都了解jvm内存分配,TLB(Thread Local Buffer)线程局部缓存,jvm给对象分配内存时,优先在TLB中分配,TLB中不够了再去全局加锁请求分配,这就是一种线程局部缓存思想,本文的ThreadLocal的设计思想个人认为道理相通,可以类比理解。

ThreadLocal只是存储的入口类,而真正实现局部存储的数据结构是ThreadLocal.ThreadLocalMap,对于jdk1.8版本,其实现为数组结构,具体的实现方式在源码分析中慢慢道来。

2.使用

 

3.源码分析

3.1 ThreadLocal源码分析

ThreadLocal关键方法

public public T get();

public void set(T value);

public void remove();

3.1.1 get()方法

//ThreadLocal三个public方法之一(set,get,remove)
public T get() {
	//获取当前执行线程
	Thread t = Thread.currentThread();
	//获取线程的成员变量ThreadLocalMap
	ThreadLocalMap map = getMap(t);

	//map!=null,则map已经被其他ThreadLocal实例初始化了
	if (map != null) {

		//利用当前ThreadLocal实例为key获取entry
		ThreadLocalMap.Entry e = map.getEntry(this);

		//获取到entry,返回value值
		if (e != null) {
			@SuppressWarnings("unchecked")
			T result = (T)e.value;
			return result;
		}
	}

	//若当前线程的ThreadLocal.ThreadLocalMap threadLocals == null, 那么执行初始化方法
	return setInitialValue();
}

get()方法主要的目的是根据当前ThreadLocal实例在当前线程的ThreadLocalMap(entry列表)中获取entry,并最终获得value,但是当前线程的ThreadLocalMap尚未初始化的情况下,getMap(t)==null,那么就会对当前线程的ThreadLocalMap成员变量进行初始化。

map.getEntry(this) 为ThreadLocalMap的方法,我们在ThreadLocalMap中再来分析。

这里我们先来看当前线程未初试化的情况(getMap(t)==null):接着跟setInitialValue()方法

private T setInitialValue() {

	//首先执行可能被重写的初试化方法
	T value = initialValue();

	//
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);


	if (map != null) {
		map.set(this, value);
	} else {
		//初始化线程的map对象,以当前的ThreadLocal-->value 作为第一个键值对,
		// 初始化时,未重写初始化方法value=null也会存入
		createMap(t, value);
	}

	return value;
}

其中 initialValue() 方法需要子类重写,若不重写,则默认返回null。

尚未初试化时,getMap(t)==null,那么就会执行else中的createMap(t,value)方法,而其实现也异常简单,就是给当前线程的成员变量创建ThreadLocalMap结构对象,并且以当前[ThreadLocal-->value]组成的键值对为容器的第一个元素。

//替当前线程初始化Map变量值
void createMap(Thread t, T firstValue) {
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}

我们再继续跟进ThreadLocalMap(ThreadLocal, T)这个带参构造方法。

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
	//初始数组,长度INITIAL_CAPACITY=16
	table = new Entry[INITIAL_CAPACITY];
	//计算当前第一个元素应该所处的位置,根据ThreadLocal的不可变实例变量 threadLocalHashCode (作为hash值)
	int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

	//直接放置当前键值对
	table[i] = new Entry(firstKey, firstValue);
	//元素个数
	size = 1;
	//初始化扩容阈值
	setThreshold(INITIAL_CAPACITY);
}

setThreshold(n)则是设置对table数组进行扩容的阈值。

private void setThreshold(int len) {
	threshold = len * 2 / 3;
}

总结:get方法在ThreadLocal层的实现相对简单,无非就是根据当前ThreadLocal作为key来获取value值

3.1.2 set()方法

public void set(T value) {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);

	//map存在,保存当前键值对
	if (map != null)
		map.set(this, value);
	else
		createMap(t, value);
}

createMap(t, value)在get一节已经分析过,不在赘述,而map.set(this, value)是ThreadLocalMap的方法,我们在ThreadLocalMap中重点分析。

3.1.3 remove()方法

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

同样真正执行remove的是在ThreadLocalMap的方法中实现。可见整个ThreadLocal体系的核心在ThreadLocalMap这个类的实现。

 

3.2 ThreadLocalMap源码分析

重点方法

private Entry getEntry(ThreadLocal<?> key);
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e);
private int expungeStaleEntry(int staleSlot);

private void set(ThreadLocal<?> key, Object value);
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot);
private boolean cleanSomeSlots(int i, int n);


private void remove(ThreadLocal<?> key);
private void rehash();
private void expungeStaleEntries();
private void resize()


private static int nextIndex(int i, int len);

private static int prevIndex(int i, int len);

3.2.1 expungeStaleEntry(int staleSlot)

首先来看一个被调用最多的方法,也是ThreadLocalMap实现的关键, private int expungeStaleEntry(int staleSlot)

//参数:staleSlot 该位置的entry已经过期
//核心方法,探测式整理(清理加归位处理)
//扫描从staleSlot位置开始的entry,直到遇到第一个null为止,[及staleSlot与null之间的元素]
private int expungeStaleEntry(int staleSlot) {
	Entry[] tab = table;
	int len = tab.length;

	// expunge entry at staleSlot
	//清理这个过期的元素
	tab[staleSlot].value = null;
	tab[staleSlot] = null;

	//元素个数维护
	size--;

	// Rehash until we encounter null
	//清理过期的元素,及移动元素到正确的位置上去
	Entry e;
	int i;
	for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {

		ThreadLocal<?> k = e.get();

		//过期的entry直接清除
		if (k == null) {
			e.value = null;
			tab[i] = null;
			size--;

		} else {

			//没过期的entry进行rehash,归位
			int h = k.threadLocalHashCode & (len - 1);

			//元素不在正确的位置上(有hash冲突)
			if (h != i) {
				//置空当前位置i
				tab[i] = null;

				//从正确位置开始查找(新位置可能hash冲突),找到空位
				while (tab[h] != null)
					h = nextIndex(h, len);

				//新的位置
				tab[h] = e;
			}
		}
	}
	return i;
}

解释:该方法从一个过期的元素位置开始,并向后移动扫描,遇到过期元素就清除,遇到还处于正常状态的元素对其进行rehash操作,将正常元素移动到正确的位置上去,直到遇到null位置停止扫描,并返回null位置的索引下标。

我们下面画图理解该方法流程

初始情况如图

假设调用expungeStaleEntry时staleSlot=3

清除位置i=3处的元素:

继续向后扫描

发现i=4处的元素正常,并且 int h = k.threadLocalHashCode & (len - 1);得出h=3,说明加入该元素时有hash冲突,需要对该元素归位到位置3处。

继续向后扫描到i=5,发现该元素过期,直接清除

继续向后扫描i=6,发现该元素E6正常,并且 int h = k.threadLocalHashCode & (len - 1); 得出h=3,需要移动该元素,但是i=3处已经有元素,所以需要往3之后放置到i=4处。

继续向后扫描i=7,发现该元素正常,并且 int h = k.threadLocalHashCode & (len - 1); 得出h=7,不需要移动

继续向后扫描 i=8,发现该位置为null,退出循环,扫描结束,返回该位置的位置索引

3.2.2 getEntry(ThreadLocal<?> key)

//ThreadLocalMap的方法,根据key(threadlocal实例)获取entry
private Entry getEntry(ThreadLocal<?> key) {
	//根据hash值确定元素所处位置
	int i = key.threadLocalHashCode & (table.length - 1);
	Entry e = table[i];

	//没有hash冲突,并且当前entry没有过期,一次就找到
	if (e != null && e.get() == key)
		return e;
	else

		//hash冲突或则该位置[e.get()==null(表示entry已过期)]
		return getEntryAfterMiss(key, i, e);
}

getEntry实现很简单,只判断了hash出来的位置是不是当前找的元素,若为找到直接调用 getEntryAfterMiss()方法

3.2.3 getEntryAfterMiss()

//参数一:当前ThreadLocal实例
//参数二:当前ThreadLocal实例根据hash值计算出来的位置i
//参数三:位置i上的entry
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
	Entry[] tab = table;
	int len = tab.length;

	//循环进入条件是e!=null,为什么不考虑hash冲突,e==null,且当前查找的元素在后面?
	//答:因为每次清楚过期entry的时候,或则remove的时候都进行了数据修正归位置,将后面的数据移到正确的位置上去
	while (e != null) {

		ThreadLocal<?> k = e.get();

		//是当前要找的entry
		if (k == key)
			return e;

		//遍历到的entry已经过期,则对该位置进行探测式整理
		if (k == null)
			expungeStaleEntry(i);
		else
			i = nextIndex(i, len);

		//继续迭代
		e = tab[i];
	}
	return null;
}

getEntry方法的主流程就是getEntry(ThreadLocal t) --> getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) --> expungeStaleEntry(i);至此,获取元素的调用就结束了,其中包含了元素的遍历查找,以及对过期元素的清理,对仍需要保留的元素进行rehash判断是否需要移位,让其处于正确的位置上,让整个数据结构处于正确。

3.3.4 void 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);

	//e!=null, 遍历(可能hash冲突)
	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) {
			//替换过期的entry为新的键值对entry
			replaceStaleEntry(key, value, i);
			return;
		}
	}

	//没有hash冲突,直接放在位置i
	tab[i] = new Entry(key, value);
	int sz = ++size;

	//清理过期元素,检查容量
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash();
}

1. 这里面若直接找到该元素,就直接替换value

2.若存在hash冲突,并且扫描到了一个过期的元素,则调用replaceStaleEntry(key, value, i);

3.若遍历未找到当前元素entry,并且也没有遍历到过期的entry,则在i(i已经根据hash冲突,遍历时被更新)位置处new entry并保存。

4.最后若进行到第三步,那么需要对容器中的元素进行一次部分清理(cleanSomeSlots),确定元素个数是否超过阈值,并进行响应的扩容操作。

现在继续分析 replaceStaleEntry(key, value, i);

3.3.5 void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot)

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
	Entry[] tab = table;
	int len = tab.length;
	Entry e;

	//从已经确定过期位置向前查找过期元素,以便于扩大清理范围
	int slotToExpunge = staleSlot;
	for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)){
		if (e.get() == null){
			slotToExpunge = i;
		}
	}

	//向后遍历(staleSlot+1开始),查找该元素是否存在
	for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {

		ThreadLocal<?> k = e.get();

		//该键值对已经存在,直接替换value
		if (k == key) {
			e.value = value;

			//将过期元素往后放(i)
			tab[i] = tab[staleSlot];

			//将当前entry往前放(归位)
			tab[staleSlot] = e;

			//除了位置staleSlot位置,未找到其他位置过期,则将开始清理位置改为i(缩小清理范围)
			if (slotToExpunge == staleSlot)
				slotToExpunge = i;

			//该方法会返回清理到的null的位置
			int nullIndex = expungeStaleEntry(slotToExpunge);

			//从nullIndex的下一个位置,简单清理几次(len=2^x,即清理x+1此)
			cleanSomeSlots(nullIndex, len);
			return;
		}

		//位置entry过期,若之前未扫描到其他过期的entry,重置过期扫描起始位置为i
		if (k == null && slotToExpunge == staleSlot)
			slotToExpunge = i;
	}

	//走到这里:遍历没有找到当前键值对entry,则将元素放在当前过期的位置(替换)
	tab[staleSlot].value = null;
	tab[staleSlot] = new Entry(key, value);

	//进行到这里:先确定遍历有没有扫描到其他过期的entry
	if (slotToExpunge != staleSlot){
		//清理
		cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
	}
}

该方法主要有两件事

1.找到当前元素的位置,并设置value值

2.对需要清理的元素范围进行确定,并进行一次清理过期元素操作。

接下来我们继续看看这个设计比较新颖的cleanSomeSlots方法

3.3.6 boolean cleanSomeSlots(int i, int n)

private boolean cleanSomeSlots(int i, int n) {
	boolean removed = false;
	Entry[] tab = table;
	int len = tab.length;
	do {
		i = nextIndex(i, len);
		Entry e = tab[i];

		//清理过期entry
		if (e != null && e.get() == null) {
			n = len;
			removed = true;

			//探测式清理
			i = expungeStaleEntry(i);
		}
	} while ( (n >>>= 1) != 0);

	//确定是否有清楚成功
	return removed;
}

其中n是用来确定while循环执行几次的参数,n=2^x表达式确定当前while循环需要被执行x+1次。

3.3.7 void remove(ThreadLocal<?> key)

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)]) {

		//找到键值对entry
		if (e.get() == key) {

			//清除弱引用(this.referent = null; 置为过期),及entry.get()会等于null
			e.clear();

			//从当前位置i(当前位置的e.get()==null),开始整理元素(清理过期的,移动未过期的到正确位置)
			expungeStaleEntry(i);
			return;
		}
	}
}

3.3.8 其他方法

诸如 resize(),expungeStaleEntries()等方法均比较简单,这里不在赘述。

4.总结

从第三节的源码分析我们可以看出,ThreadLocal的原理相对比较简单,这个数据结构节变量关系类图如下:

threadLocal引用示意图

threadLocal引用示意图

而我们使用的ThreadLocal只是这个数据结构的入口API。

4.1 ThreadLocal内存泄漏问题

在上图上我看可以看出,ThreadLocal实例的包含一个强引用(ThreadLocalRef)和一个弱引用(key),若强引用断开,那么该ThreadLocal在下次GC时就会被回收,那么entry.get()==null,导致该value永远无法被访问,并且是强引用,无法被GC回收,这就导致了内存泄漏。

在ThreadLocal的方法中已经尽量避免产生内存泄漏了,在ThreadLocal的get,set,remove中都有对过期entry的探测,发现过期的key就及时移除。但是若线程很长一段时间都没使用这几个方法,那么就会导致过期的entry一直没有被回收,这也是风险。

注意:在平时的使用过程中,我们使用完后尽量使用remove操作及时删除不用的entry,也是避免内存泄漏。特别的现在更多的使用的是线程池,若不及时remove还可能导致业务逻辑问题。

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值