好久不见,不晓得大家新年过得怎么样?有没有痛痛快快得放松?是不是还能收到很多压岁钱?好了,话不多说,咱们开始明天的主题:ThreadLocal。
我收集了4个面试中呈现频率较高的对于ThreadLocal的问题:
什么是ThreadLocal?什么场景下应用ThreadLocal?
ThreadLocal的底层是如何实现的?
ThreadLocal在什么状况下会呈现内存透露?
应用ThreadLocal要留神哪些内容?
咱们先从一个“流言”开始,通过剖析ThreadLocal的源码,尝试纠正“流言”带来的误会,并解答下面的问题。
流传已久的“流言”
很多文章都在说“ThreadLocal通过拷贝共享变量的形式解决并发平安问题”,例如:
这种说法并不精确,很容易让人误会为ThreadLocal会拷贝共享变量。来看个例子:
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) { new Thread(() -> { try { System.out.println(DATE_FORMAT.parse("2023-01-29")); } catch (ParseException e) { e.printStackTrace(); } }).start();}
}
复制代码
咱们晓得,多线程并发拜访同一个DateFormat实例对象会产生重大的并发平安问题,那么退出ThreadLocal是不是能解决并发平安问题呢?批改下代码:
/**
- 第一种写法
*/
private static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new ThreadLocal<>() {
@Overrideprotected DateFormat initialValue() { return DATE_FORMAT;}
};
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) { new Thread(() -> { try { System.out.println(DATE_FORMAT_THREAD_LOCAL.get().parse("2023-01-29")); } catch (ParseException e) { e.printStackTrace(); } }).start();}
}
复制代码
预计会有很多小伙伴会说:“你这么写不对!《阿里巴巴Java开发手册》中不是这么用的!”。把书中的用法搬过去:
/**
- 第二种写法
*/
private static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new ThreadLocal<>() {
@Overrideprotected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd");}
};
复制代码
Tips:代码小改了一下~~
咱们来看两种写法的差异:
第一种写法,ThreadLocal#initialValue时应用共享变量DATE_FORMAT;
第二种写法,ThreadLocal#initialValue时创立SimpleDateFormat对象。
依照“流言”的形容,第一种写法会拷贝DATE_FORMAT的正本提供给不同的线程应用,但从后果上来看ThreadLocal并没有这么做。
有的小伙伴可能会狐疑是因为DATE_FORMAT_THREAD_LOCAL线程共享导致的,但别忘了第二种写法也是线程共享的。
到这里咱们应该可能猜到,第二种写法中每个线程会拜访不同的SimpleDateFormat实例对象,接下来咱们通过源码一探到底。
ThreadLocal的实现
除了应用ThreadLocal#initialValue外,还能够通过ThreadLocal#set增加变量后再应用:
ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();
threadLocal.set(new SimpleDateFormat("yyyy-MM-dd"));
System.out.println(threadLocal.get().parse("2023-01-29"));
复制代码
Tips:这么写仅仅是为了展现用法~~
应用ThreadLocal非常简单,3步就能够实现:
创建对象
增加变量
取出变量
无参结构器没什么好说的(空实现),咱们从ThreadLocal#set开始。
ThreadLocal#set的实现
ThreadLocal#set的源码:
public void set(T value) {,
Thread t = Thread.currentThread();// 获取以后线程的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) { // 增加变量 map.set(this, value);} else { // 初始化ThreadLocalMap createMap(t, value);}
}
复制代码
ThreadLocal#set的源码非常简单,但却走漏出了不少重要的信息:
变量存储在ThreadLocalMap中,且与以后线程无关;
ThreadLocalMap应该相似于Map的实现。
接着来看源码:
public class ThreadLocal<T> {
ThreadLocalMap getMap(Thread t) { return t.threadLocals;}void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue);}
}
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
复制代码
很清晰的展现出ThreadLocalMap与Thread的关系:ThreadLocalMap是Thread的成员变量,每个Thread实例对象都领有本人的ThreadLocalMap。
另外,还记得在对于线程你必须晓得的8个问题(上)提到Thread实例对象与执行线程的关系吗?
如果从Java的层面来看,能够认为创立Thread类的实例对象就实现了线程的创立,而调用Thread.start0能够认为是操作系统层面的线程创立和启动。
能够近似的看作是:Thread实例对象≈执行线程Thread实例对象\approx执行线程Thread实例对象≈执行线程。也就是说,属于Thread实例对象的ThreadLocalMap也属于每个执行线程。
基于以上内容,咱们如同失去了一个非凡的变量作用域:属于线程。
Tips:
实际上属于线程也即是属于Thread实例对象,因为Thread是线程在Java中的形象;
ThreadLocalMap属于线程,但不代表存储到ThreadLocalMap的变量属于线程。
ThreadLocalMap的实现
ThreadLocalMap是ThreadLocal的外部类,代码也不简单:
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private Entry[] table; private int size = 0; private int threshold; private void setThreshold(int len) { threshold = len * 2 / 3; } 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的特点:
ThreadLocalMap底层存储构造是Entry数组;
通过ThreadLocal的哈希值取模定位数组下标;
构造方法增加变量时,存储的是原始变量。
很显著,ThreadLocalMap是哈希表的一种实现,ThreadLocal作为Key,咱们能够将ThreadLocalMap看做是“简版”的HashMap。
Tips:
本文不探讨哈希表实现中解决哈希抵触,数组扩容等问题的形式;
也不须要关注ThreadLocalMap#set和ThreadLocalMap#getgetEntry的实现;
与构造方法一样,ThreadLocalMap#set中存储的是原始变量。
到目前为止,无论是ThreadLocalMap#set还是ThreadLocalMap的构造方法,都是存储原始变量,没有任何拷贝正本的操作。也就是说,想要通过ThreadLocal实现变量在线程间的隔离,就须要手动为每个线程创立本人的变量。
ThreadLocal#get的实现
ThreadLocal#get的源码也非常简单:
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();
}
复制代码
后面的局部很容易了解,咱们看map == null时调用的ThreadLocal#setInitialValue办法:
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);}if (this instanceof TerminatingThreadLocal) { TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);}return value;
}
复制代码
ThreadLocal#setInitialValue办法简直和ThreadLocal#set一样,但变量是通过ThreadLocal#initialValue取得的。如果是通过ThreadLocal#initialValue增加变量,在第一次调用ThreadLocal#get时将变量存储到ThreadLocalMap中。
ThreadLocal的原理
好了,到这里咱们曾经能够构建出对ThreadLocal比拟残缺的认知了。咱们先来看ThreadLocal,ThreadLocalMap和Thread三者之间的关系:
能够看到,ThreadLocal是作为ThreadLocalMap中的Key的,而ThreadLocalMap又是Thread中的成员变量,属于每一个Thread实例对象。遗记ThreadLocalMap是ThreadLocal的外部类这层关系,整体构造就会十分清晰。
创立ThreadLocal对象并存储数据时,会为每个Thread对象创立ThreadLocalMap对象并存储数据,ThreadLocal对象作为Key。在每个Thread对象的生命周期内,都能够通过ThreadLocal对象拜访到存储的数据。