1、ThreadLocal是什么?
从名字我们就可以看到 ThreadLocal 叫做本地线程变量,意思是说,ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本(本地拷贝),这样每个线程都可以访问自己内部的副本变量,避免了线程安全问题。
ThreadLocal是整个线程的全局变量,不是整个程序的全局变量。
2、ThreadLocal怎么实现?(原理)
- ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
- ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
实现方式观察ThreadLocal的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);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocal.ThreadLocalMap threadLocals = null;
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
3、ThreadLocal内存泄漏是怎么回事
如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏。
解决办法是在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象。
//ThreadLocal内存泄漏
package communication;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class BaiLiThreadLocalMemoryLeakDemo {
private static final ThreadLocal<byte[]> threadLocal = new ThreadLocal <byte[]>();
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
byte[] data = new byte[50240 * 10240];
threadLocal.set(data);
// 不调⽤ remove ⽅法,会导致内存泄漏
//threadLocal.remove();
});
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.MINUTES);
}
}
4、ThreadLocalMap的结构
ThreadLocalMap虽然被称为Map,但是其实它是没有实现Map接口的,不过结构还是和HashMap比较类似的,主要关注的是两个要素:元素数组和散列方法。
- 元素数组一个table数组,存储Entry类型的元素,Entry是ThreadLocal弱引用作为key,Object作为value的结构。
private Entry[] table; - 散列方法就是怎么把对应的key映射到table数组的相应下标,ThreadLocalMap用的是哈希取余法,取出key的threadLocalHashCode,然后和table数组长度减一&运算(相当于取余)
int i = key.threadLocalHashCode & (table.length – 1);
补充一点每创建一个ThreadLocal对象,它就会新增0x61c88647,这个值很特殊,它是斐波那契数也叫黄金分割数。这样带来的好处就是hash分布非常均匀。
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
5、ThreadLocalMap怎么解决Hash冲突?
我们可能都知道HashMap使用了链表来解决冲突,也就是所谓的链地址法。
ThreadLocalMap内部使用的是开放地址法来解决Hash冲突的问题。具体来说,当发生Hash冲突时,ThreadLocalMap会将当前插入的元素从冲突位置开始依次往后遍历,直到找到一个空闲的位置,然后把该元素放在这个空闲位置。这样即使出现了Hash冲突,不会影响到已经插入的元素,而只是会影响到新的插入操作。
查找的时候,先根据ThreadLocal对象的hash值找到对应的位置,然后比较该槽位Entry对象中的key是否和get的key一致,如果不一致就依次往后查找。
6、ThreadLocalMap扩容机制
ThreadLocalMap的扩容机制和HashMap 类似,也是在元素数量达到阈值(默认为数组长度的 2/3)时进行扩容。具体来说,在set()方法中,如果当前元素数量已经达到了阈值,就会调用rehash()方法,rehash()会先去清理过期的Entry,然后还要根据条件判断size>=threshold-threshold/4也就是size >=threshold * 3/4来决定是否需要扩容:
private void rehash() {
//清理过期Entry
expungeStaleEntries();
//扩容
if (size >= threshold - threshold / 4)
resize();
}
//清理过期Entry
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
发现需要扩容时调用resize()方法,resize()方法首先将数组长度翻倍,然后创建一个新的数组newTab。 接着遍历旧数组oldTab中的所有元素,散列方法重新计算位置,开放地址解决冲突,然后放到新的newTab,遍历完成之后,oldTab中所有的entry数据都已经放入到newTab中了,然后table引用指向newTab。
需要注意的是,新数组的长度始终是2的整数次幂,并且扩容后新数组的长度始终大于旧数组的长度。这是为了保证哈希函数计算出的位置在新数组中仍然有效。
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
7、ThreadLocalMap怎么进行父子线程通信
在Java多线程编程中,父子线程之间的数据传递和共享问题一直是一个非常重要的议题。如果不处理好数据的传递和共享,会导致多线程程序的性能下降或者出现线程安全问题。ThreadLocal是Java提供的一种解决方案,可以非常好地解决父子线程数据共享和传递的问题。
那么它是如何实现通信的了?在Thread类中存在InheritableThreadLocal变量,简单的说就是使用 InheritableThreadLocal来进行传递,当父线程的lnheritableThreadLocal不为空时,就会将这个值传到当前子线程的InheritableThreadLocal。
public class BaiLiInheritableThreadLocalDemo {
public static void main(String[] args) throws Exception {
ThreadLocal threadLocal = new ThreadLocal<>();
threadLocal.set("threadLocal");
ThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
inheritableThreadLocal.set("分享 + inheritableThreadLocal");
Thread t = new Thread(() -> {
System.out.println("⼀键三连 + " + threadLocal.get());
System.out.println("⼀键三连 + " + inheritableThreadLocal.get());
});
t.start();
}
}
8、ThreadLocal的应用场景有哪些?
- 1、在进行对象跨层传递的时候,使用 ThreadLocal 可以避免多次传递,打破层次间的约束
- 2、线程间数据隔离
- 3、进行事务操作,用于存储线程事务信息
- 4、数据库连接,Session会话管理