ThreadLocal原理及源码解析(一步一步点进去,不要背了,学思想)

Java
310
0
0
2022-12-22
文章内容引用自 咕泡科技 咕泡出品,必属精品

文章目录

  • 1ThreadLocal使用
  • 2ThreadLocal原理源码分析
  • 2.1get方法
  • 2.1.1入口
  • 2.1.1.1ThreadLocal中的ThreadLocalMap对象
  • 2.1.1.2强引用
  • 2.1.1.3软引用
  • 2.1.1.4弱引用
  • 2.1.1.5虚引用
  • 2.1.2 初始化方法
  • 初始化创建Map
  • 2.2Set方法
  • 2.2.1.set入口
  • set方法
  • 2.3remove方法
  • 2.4 扩容逻辑
  • 2.5其他非正常情况
  • 2.5.1当ThreadLocal与ThreadLocal1的hash值冲突
  • 2.5.2当Key被GC回收处理
  • 3为什么Key要弱引用?
  • 4Value为什么不用弱引用
  • 怎么解决hash冲突

1ThreadLocal使用

我们知道,一个共享变量或者资源,在多个线程操作的时候,肯定是会相互影响,不能隔离的

img

public class AtomicTest {

	int i=0;
	public void incr(){
		i+=10;
		System.out.println(i);
	}
	
	public static void main(String[] args) throws InterruptedException {
		AtomicTest test=new AtomicTest();
		Thread[] threads=new Thread[5];
		for (int j = 0; j < 5; j++) {
			threads[j] =new Thread(() -> {
				test.incr();
			});
			threads[j].start();
		}
		for (int j = 0; j < 5; j++) {
		threads[j].join();
		}
	}
}

比如,这样的代码,那么得到的结果,肯定是每次加10不定,还有因为原子性问题,肯定会出现相同的10.20…。

那么假如我有场景,我要做到线程之前的数据相互不影响!!相互隔离,也就是我们讲的线程安全。比如mybatis里面,sqlsession就是存在ThreadLocal里面的,sqlSession这个对象就是线程安全的!!那么实现方式就是我们今天讲的重点:threadLocal

上面的栗子怎么 变得香甜 实现线程安全?如下:

public class ThreadLocalTest {
	ThreadLocal<Integer> integerThreadLocal=new ThreadLocal<Integer>() {
		public Integer initialValue() {
			return 10;
		}
	};
	ThreadLocal<Integer> integerThreadLocal1=new ThreadLocal<Integer>() {
		public Integer initialValue() {
			return 20;
		}
	};
	public void incr() {
		int value = integerThreadLocal.get().intValue();
		integerThreadLocal.set(value += 10);
		int value1= integerThreadLocal1.get().intValue();
		integerThreadLocal1.set(value1 += 10);
		System.out.println(integerThreadLocal.get());
		System.out.println(integerThreadLocal1.get());
	}
	public static void main(String[] args) {
		ThreadLocalTest test=new ThreadLocalTest();
		Thread[] threads=new Thread[5];
		for (int j = 0; j < 2; j++) {
			threads[j] =new Thread(() -> {
			test.incr();
		});
	}

这样我们integerThreadLocal得到的结果都是20,integerThreadLocal1得到的结果都是30。

2ThreadLocal原理源码分析

我们从2个角度去分析源码,一个是get 一个是set

2.1get方法

2.1.1入口

    public T get() {
    	//获取当前线程 
        Thread t = Thread.currentThread();
        //去根据Thread拿ThreadLocalMap,我们发现Thread类下会有个数据对象叫做ThreadLocalMap 
        //1.1 线程第一次进来,map肯定是null 
        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;
            }
        }
        //1.2进入初始化方法 
        return setInitialValue();
    }

2.1.1.1ThreadLocal中的ThreadLocalMap对象

ThreadLocalMap对象,里面有个Entry的 key 、value的结构

static class ThreadLocalMap {
	/**
	* The entries in this hash map extend WeakReference,using
	* its main ref field as the key (which is always a
	* ThreadLocal object). Note that null keys (i.e.entry.get()==null) mean that the key is no longer referenced,
	* so the entry can be expunged from table. 
	* Such entries are referred to
	* as "stale entries" in the code that follows.
	*/
	static class Entry extends WeakReference<ThreadLocal<?>> {
	/** The value associated with this ThreadLocal. */
		Object value;
		Entry(ThreadLocal<?> k, Object v) {
			super(k);
			value = v;
		}
	}
//..........其他信息省略..................
}

我们发现,Entry的key是一个对于ThreadLocal这个对象的弱引用。

讲下四大引用 除了基础的数据类型外都是引用类型,那么java根据其生命周期的长短又将引用类型分为强引用、软引用、弱引用、虚引用。

2.1.1.2强引用

也是我们平时用得最多的,new一个对象就是强引用,例如 Object obj = new Object(); 当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象!记住是存活着,不可能是你new一个对象就永远不会被GC回收。 如果将引用赋值为null时,你的对象就表明不是存活着,这样就会可以被GC回收了

当内存不足的时候,jvm开始垃圾回收,对于强引用的对象,就算出现OOM也不会回收该对象的。 因此,强引用是造成java内存泄露的主要原因之一。
public static void main(String[] args) {
	Object obj=new Object();//这样定义就是一个强引用
	Object obj2=obj;//也是一个强引用
	obj=null;
	System.gc();
	//会不会被垃圾回收?
	System.out.println(obj2);
}

2.1.1.3软引用

软引用的生命周期比强引用短一些。软引用是通过SoftReference类实现的。当JVM认为内存空间不足时,就会去试图回收软引用指向的对象对于只有软引用的对象来说, 当系统内存充足时,不会被回收; 当系统内存不足时,会被回收;

Object obj=new Object();
SoftReference wrf=new SoftReference(obj);
obj=null;
System.out.println("未发生GC之前"+wrf.get());
System.gc();
System.out.println("内存充足,发生GC之后"+wrf.get());

2.1.1.4弱引用

弱引用是通过WeakReference类实现的,它的生命周期比软引用还要短,也是通过get()方法获取对象。在GC的时候,不管内存空间足不足都会回收这个对象,同样也可以配合ReferenceQueue使用,也同样适用于内存敏感的缓存。ThreadLocal中的key就用到了弱引用。

Object obj=new Object();
WeakReference wrf=new WeakReference(obj);
obj=null;
System.out.println("未发生GC之前"+wrf.get());
System.gc();
System.out.println("内存充足,发生GC之后"+wrf.get());

2.1.1.5虚引用

也称虚引用,是通过PhantomReference类实现的。任何时候可能被GC回收,就像没有引用一样。无法通过虚引用访问对象的任何属性或者函数。 那就要问了要它有什么用? 虚引用仅仅只是提供了一种确保对象被finalize以后来做某些事情的机制。比如说这个对象被回收之后发一个系统通知啊啥的。

虚引用是必须配合ReferenceQueue 使用的,具体使用方法和上面提到软引用的一样。主要用来跟踪对象被垃圾回收的活动。

我们知道了Entry的key是弱引用,弱引用的作用是什么我们知道了,那么至于为什么要用弱引用,我们等下再回来看,先把整个流程搞清楚!

2.1.2 初始化方法

private T setInitialValue() {
	//调用initialValue方法,默认为null,可以重写。重写设置初始值
	T value = initialValue();
	//获取当前线程
	Thread t = Thread.currentThread();
	//根据线程获取线程的ThreadLocalMap
	ThreadLocalMap map = getMap(t);
	//第一次初始化,为null
	if (map != null)
		map.set(this, value);
	else
		//走到创建Map逻辑 1.2.1
		createMap(t, value);
	return value;
}

初始化创建Map

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

进入ThreadLocalMap初始化构造函数

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue){
	//初始化Entry数组
	table = new Entry[INITIAL_CAPACITY];
	//根据线程的hashcode取模得到我应该放入数据的哪个下标位置
	int i = firstKey.threadLocalHashCode &(INITIAL_CAPACITY - 1);
	//在计算出的下标位置,放入entry,key为ThreadLocal对象,value为初始化的值
	table[i] = new Entry(firstKey, firstValue);
	size = 1;
	//设置ThreadLocalMap的threshold值,16*2/3=10
	setThreshold(INITIAL_CAPACITY);
}

自此,初始化流程完成!!

代码注释自认为给的很详细了

img

2.2Set方法

2.2.1.set入口

public void set(T value) {
	//获取当前线程
	Thread t = Thread.currentThread();
	//获取线程的Map,这个时候,我们上面get的时候已经初始化,已经有值
	ThreadLocalMap map = getMap(t);
	//get如果在map之前执行,肯定不为null
	if (map != null)
	//进入set方法 
		map.set(this, value);
	else
		createMap(t, value);
}

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.
	//将原有的table赋值给tab
	Entry[] tab = table;
	//得到tabl的大小
	int len = tab.length;
	//根据key的hashCode 取模数组,得到数据的下标,同一个key的时候,hashcode一样,所以
	//根据key找到的下标已经有entry对象并且已经赋值了初始化的值
	int i = key.threadLocalHashCode & (len-1);
	//在同一个key的get.set之后,e不为null,进入for循环
	for (Entry e = tab[i];
		e != null;
		e = tab[i = nextIndex(i, len)]) {
			//得到entry的key
			ThreadLocal<?> k = e.get();
			//因为get set传入的threadlocal对象是一个,满足条件
			if (k == key) {
				//将entry对象的value更改为新的value,返回
				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();
	}

这个是最基础的流程,我们大概可以整理流程图如下:

假如2个线程,操作integerThreadLocal这个ThreadLocal对象,并且ThreadLocal对象的hash值计算后在Entry中的数组下标为5,integerThreadLocal下标为3 栗子中的代码呦

正常流程如下: get方法后

img

Set方法后

img

多个线程,就是多个外面的Thread,做到线程之间数据隔离

看完了get、set,看一下remove吧

2.3remove方法

public void remove() {
	//根据当前线程获取ThreadLocalMap
	ThreadLocalMap m = getMap(Thread.currentThread());
	//如果ThreadLocalMap!=null
	if (m != null)
		//移除
		m.remove(this);
}
private void remove(ThreadLocal<?> key) {
	Entry[] tab = table;
	int len = tab.length;
	//根据key得到在数组中的下标位置
	int i = key.threadLocalHashCode & (len-1);
	for (Entry e = tab[i];
		e != null;
		e = tab[i = nextIndex(i, len)]) {
		//如果下标位置Entry的key为当前对象,进行整理清楚
		if (e.get() == key) {
			//解除Entry对key的弱引用
			e.clear();
			//对下标进行清除,并且对table进行整理
			expungeStaleEntry(i);
			return;
		}
	}
}

顺便看下扩容逻辑吧

2.4 扩容逻辑

入口还是在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;
	//没有进行清理并且size大于等于我的扩容界限,调用rehash
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash();
}

Rehash方法

private void rehash() {
	expungeStaleEntries();
	// Use lower threshold for doubling to avoid hysteresis
	//当容量大于等于四分之三时,进入resize方法
	if (size >= threshold - threshold / 4)
		resize();
}

resize方法

private void resize() {
	Entry[] oldTab = table;
	int oldLen = oldTab.length;
	//扩容容量为原容量的2倍
	int newLen = oldLen * 2;
	//初始化数组长度
	Entry[] newTab = new Entry[newLen];
	int count = 0;
	//循环遍历老的容量大小
	for (int j = 0; j < oldLen; ++j) {
		//遍历Enrty
		Entry e = oldTab[j];
		//如果Entry不为null
		if (e != null) {
			//获取Entry的key
			ThreadLocal<?> k = e.get();
			//如果key为null,无效数据,把value设置为空,让value能gc回收
			if (k == null) {
				e.value = null; // Help the GC
			} else {
				//不为空,得到k的新的下标地址
				int h = k.threadLocalHashCode & (newLen - 1);
				//如果!=null.代表发生hash冲突
				while (newTab[h] != null)
					//线性探测下一个
					h = nextIndex(h, newLen);
				//赋值给为空的entry位置
				newTab[h] = e;
				count++;
			}
		}
	}
	//设置下一次的扩容值
	setThreshold(newLen);
	size = count;
	table = newTab;
}

2.5其他非正常情况

2.5.1当ThreadLocal与ThreadLocal1的hash值冲突

我们来看set方法中多线程中多个ThreadLocal的hashCode冲突时,怎么解决,我们回到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;
	//这里可能发送hash冲突,假如threadLocal1跟threadLocal 2个对象的hash值相同,下标都是5
	int i = key.threadLocalHashCode & (len-1);
	//通过i去拿数据的Entry,我们拿到的是ThreadLocal的,因为
	ThreadLocal占据了5这个位置
	for (Entry e = tab[i];
		e != null;
		e = tab[i = nextIndex(i, len)]) {
		//得到的是ThreadLocal对象
		ThreadLocal<?> k = e.get();
		//ThreadLocal !=ThreadLocal1
		if (k == key) {
			e.value = value;
			return;
		}
		//第一个循环 k也不等于null
		//第二轮循环,
		if (k == null) {
			replaceStaleEntry(key, value, i);
			return;
		}
		//3个条件都不满足,进入下一个循环
		//i= nextIndex(i, len) 去找下一个小标的位置,直到找到下一个key为空的为止,这个场景我们等下过
		//或者遍历完到一个null的位置,就不在循序
	}
	//找到一个为null的位置(肯定有,因为有扩容机制)
	tab[i] = new Entry(key, value);
	int sz = ++size;
	//清理后,如果超过我的扩容界限 扩容界限为三分之二,进行扩容
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash();
}

冲突取值 我们知道它是用的线性探测去解决hash的,那么会出现一个问题?我根据hash去拿到的对象,可能不再是我自己想要的对象!

public T get() {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null) {
	//根据threadLocal对象 去map中获取Entry,如果冲突了我们看下怎么拿
		ThreadLocalMap.Entry e = map.getEntry(this);
		if (e != null) {
			T result = (T)e.value;
			return result;
		}
	}
	return setInitialValue();
}

getEntry方法:

private Entry getEntry(ThreadLocal<?> key) {
	//根据key的hash下标值去取值
	int i = key.threadLocalHashCode & (table.length - 1);
	Entry e = table[i];
	//如果取到的enrty不为null 并且对象也是我需要的对象,直接返回
	if (e != null && e.get() == key)
		return e;
	else
		//如果不是我想要的对象,进入getEntryAfterMiss
		return getEntryAfterMiss(key, i, e);
}

getEntryAfterMiss方法:

//key:我需要get的对象 i 根据key计算出来的下标 e 下标中的当前值
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i,Entry e) {
	Entry[] tab = table;
	int len = tab.length;
	//如果e!=null。进入逻辑,如果e为null,说明ThreadLocal没有设置value,直接返回空
	while (e != null) {
		//得到当前位置的Entry对象
		ThreadLocal<?> k = e.get();
		//如果当前位置的Entry跟传进来的一致,直接返回
		if (k == key)
			return e;
		if (k == null)
			//如果对象的key被GC回收,进入整理逻辑,把当前位置设置为null 并且进行整理,rehash
			expungeStaleEntry(i);
		else
			//去下一个线性找
			i = nextIndex(i, len);
			//把e设置为下一个Enrty对象
		e = tab[i];
	}
	return null;
}

2.5.2当Key被GC回收处理

我们刚才讲过我们的key是弱引用,何为弱引用,就是我这个key就算外面有引用,只要发生GC也会被回收,就会出现我Entry的数据有可能是key为null ,但是value不为null的场景。 我们继续来看ThreadLocal怎么解决,继续回到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;
			}
		//因为Key被回收,所以key为null,会进入replaceStaleEntry方法
		if (k == null) {
			replaceStaleEntry(key, value, i);//这里这里
			return;
		}
	}
	//找到key为null的,不会走下面逻辑
	tab[i] = new Entry(key, value);
	int sz = ++size;
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash();
}

replaceStaleEntry方法: 看这个方法,我们举个例子:在Thread1线程执行threadLocal1.set10);同时threadLocal1通过hash算法得到的下标为5;然后5的下标的key被GC回收,key=null。

//key为我需要获取值的ThreadLocal对象,value为需要set的值 i为key被回收的数组下标
//根据举例的场景:key为ThreadLocal1对象 value=10 i=5
private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {
	Entry[] tab = table;
	int len = tab.length;
	Entry e;
	// Back up to check for prior stale entry in current run.
	// We clean out whole runs at a time to avoid continual
	// incremental rehashing due to garbage collector freeing
	// up refs in bunches (i.e., whenever the collector runs).
	//slotToExpunge为5
	int slotToExpunge = staleSlot;
	//向数组前面轮询找 找到一个null的entry为止 假如下标为4的entry为null,跳出循环
	for (int i = prevIndex(staleSlot, len);
	(e = tab[i]) != null;
	i = prevIndex(i, len))
	//假如下标为4的不是null,并且是被GC回收的,那么slotToExpunge赋值为向前找,找到最靠近null的被GC回收的Entry
	if (e.get() == null)slotToExpunge = i;
	// Find either the key or trailing null slot of run,whichever
	// occurs first
	//向后循环,找到entry为null为止
	for (int i = nextIndex(staleSlot, len);
		(e = tab[i]) != null;
		i = nextIndex(i, len)) {
		ThreadLocal<?> k = e.get();
		// If we find key, then we need to swap it
		// with the stale entry to maintain hash table order.
		// The newly stale slot, or any other stale slot
		// encountered above it, can then be sent to expungeStaleEntry
		// to remove or rehash all of the other entries in run.
		//假如向后找到了key跟我传入的一样的entry
		if (k == key) {
			//如果一样,替换value
			e.value = value;
			//假如下标为7的跟我传入的key是一样的
			tab[i] = tab[staleSlot];
			//在下标为5的位置放入7下的entry
			tab[staleSlot] = e;
			// Start expunge at preceding stale entry if it exists
			//如果slotToExpunge=slotToExpunge,则向前遍历没有找到key被回收的Entry
		if (slotToExpunge == staleSlot)
			//将slotToExpunge改成7
			slotToExpunge = i;
			//执行cleanSomeSlots方法
			cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);//expungeStaleEntry方法
			//返回
			return;
		}
		// If we didn't find stale entry on backward scan, the
		// first stale entry seen while scanning for key is the
		// first still present in the run.
		//如果循环到后面的也是被GC回收的,并且向前遍历没有找到key被回收的Entry
		if (k == null && slotToExpunge == staleSlot)
		//slotToExpunge设置为 key被GC回收的Entry的下标位置
			slotToExpunge = i;
	}
	// If key not found, put new entry in stale slot
	//回收的entry的value设置为null (利于value对象回收)
	tab[staleSlot].value = null;
	//在回收的下标位置,新建对象赋值
	tab[staleSlot] = new Entry(key, value);

	// If there are any other stale entries in run, expunge them
	//slotToExpunge!=staleSlot,需要向前或者向后有找到需要清理的Entry,执行cleanSomeSlots
	if (slotToExpunge != staleSlot)
		cleanSomeSlots(expungeStaleEntry(slotToExpunge),len);//expungeStaleEntry方法
}

expungeStaleEntry方法:

private int expungeStaleEntry(int staleSlot) {
	Entry[] tab = table;
	int len = tab.length;
	// expunge entry at staleSlot
	//将传进来的下标位置的Entry value设置为null value就可以被GC回收了
	//将传进来的下标位置的Entry设置为null 清理空间
	tab[staleSlot].value = null;
	tab[staleSlot] = null;
	size--; //数组里的size-1
	// Rehash until we encounter null
	Entry e;
	int i;
	//根据传进来的位置向后遍历,遍历到null为止
	for (i = nextIndex(staleSlot, len);
		(e = tab[i]) != null;
		i = nextIndex(i, len)) {
		ThreadLocal<?> k = e.get();
		//如果entry对象的key被GC回收,清空entry
		if (k == null) {
			e.value = null;
			tab[i] = null;
			size--;
		} else {
			//如果entry的对象没有被GC回收
			//重新去计算下这个位置下的key的hash
			int h = k.threadLocalHashCode & (len - 1);
			//如果占的位置不是它hash的位置
			if (h != i) {
				//把现在的位置设置为null
				tab[i] = null;
				// Unlike Knuth 6.4 Algorithm R, we must scan until
				// null because multiple entries could have been stale.
				//看该有的位置是不是空的,如果不是,去找寻下一个null的(开放寻址解决hash冲突)
				while (tab[h] != null)
					h = nextIndex(h, len);
					//放到该有的位置去
					tab[h] = e;
			}
		}
	}
	//返回i的值 传进来的下标的 后面的最接近null的entry
	return i;
}

cleanSomeSlots方法:

//i 传入下标 n为传进来的数组的长度
private boolean cleanSomeSlots(int i, int n) {
	boolean removed = false;
	Entry[] tab = table;
	int len = tab.length;
	do {
		//根据传进来的下标去找下标后一个
		i = nextIndex(i, len);
		//得到该下标的enrty
		Entry e = tab[i];
		//如果下标的enrty 的key被GC回收了
		if (e != null && e.get() == null) {
			//n改为table的长度
			n = len;
			removed = true;
			//拿到i去清除与重新rehash后面的,直到找到null为止
			i = expungeStaleEntry(i);
		}
	} while ( (n >>>= 1) != 0); //不用遍历n次,只遍历n/2次,达到时间与空间的平衡
		return removed; //如果有清除,设置为true
}

前面分析了set方法第一次初始化ThreadLocalMap的过程,也对ThreadLocalMap的结构有了一个全面的了解。那么接下来看一下map不为空时的执行逻辑

  • 根据key的散列哈希计算Entry的数组下标
  • 通过线性探索探测从i开始往后一直遍历到数组的最后一个Entry
  • 如果map中的key和传入的key相等,表示该数据已经存在,直接覆盖
  • 如果map中的key为空,则用新的key、value覆盖,并清理key=null的数据
  • rehash扩容

3为什么Key要弱引用?

假如每个key都强引用指向ThreadLocal的对象,也就是上图虚线那里是个强引用,那么这个ThreadLocal对象就会因为和Entry对象存在强引用关联而无法被GC回收,造成内存泄漏,除非线程结束后,线程被回收了,map也跟着回收。

如果key是强引用,那么当我们执行threadLocal=null时,这个对象还被key关联,无法进行回收,只有当线程结束后,才会取消关联

但是用弱引用,我们就能在GC的时候,回收!

但是如果用的是线程池,那么的话线程就不会结束,只会放在线程池中等待下一个任务,但是这个线程的 map 还是没有被回收,它里面存在value的强引用,所以会导致内存溢出。

所以一般用threadLocal.remove()来清除内存 在ThreadLocal的生命周期中,都存在这些引用。看下图:实线代表强引用,虚线代表弱引用。

img

4Value为什么不用弱引用

是因为不清楚这个Value 除了map 的引用还是否还存在其他引用,如果不存在其他引用,当GC 的时候就会直接将这个Value干掉了,而此时我们的ThreadLocal还处于使用期间,就会造成Value为null的错误,所以将其设置为强引用

怎么解决hash冲突

1.首先,那个魔数就能保证重复性会低,但是基数必须是2的N次方(举例) 2.用开放寻址法,如果真的查到的下标已经存在数据,就去找下一个,找到一个null的为止,并且是环形查找,因为肯定会有空的,会进行提前扩容