线程安全的map_ThreadLocal | 线程本地存储
并發(fā)場(chǎng)景下,多個(gè)線程同時(shí)讀寫共享變量就有可能產(chǎn)生并發(fā)安全問題。反過來也可以說,不存在共享變量,就不會(huì)出現(xiàn)線程安全問題。Java中有兩種常用的避免共享變量的方法,使用局部變量,以及使用 ThreadLocal。
局部變量存在于每個(gè)線程內(nèi)部的調(diào)用棧中,多個(gè)線程之間互相訪問不到對(duì)方的局部變量,這就叫做線程封閉。如下圖所示,局部變量存在于線程各自的調(diào)用棧中,線程之間互不打擾。
采用局部變量的方案,的確避免了變量被多個(gè)線程共享,同時(shí)它也禁止同一個(gè)線程中不同方法共享這個(gè)變量。然而,單線程中不同的方法之間共享變量是不會(huì)導(dǎo)致線程安全問題的。
如果想讓同一個(gè)線程,不同的方法共享變量就可以使用 ThreadLocal,Java 提供的線程本地存儲(chǔ)方案。ThreadLocal 可以保證同一個(gè)變量,該線程中的方法看到的值是一樣,不同線程之間卻是隔離。
ThreadLocal 的使用方法
常規(guī)使用 ThreadLocal 的方式很簡(jiǎn)單,創(chuàng)建一個(gè) ThreadLocal 對(duì)象,然后調(diào)用它的 set(value)方法設(shè)置值,再調(diào)用 get() 方法獲取這個(gè) ThreadLocal 對(duì)象對(duì)應(yīng)的value。
// 創(chuàng)建一個(gè) ThreadLocal ThreadLocal<String> tl = new ThreadLocal<>(); // set方法 tl.set("深頁"); // get方法 tl.get();ThreadLocal 類的注釋中還帶有為每個(gè)線程分配自增 id 的示例代碼。withInitial()方法會(huì)調(diào)用initialValue()方法,為 ThreadLocal 設(shè)置 get() 的初始值。執(zhí)行下面的代碼,可以看到每個(gè)類都有自己的id,并且id的自增的。
public class ThreadId {// Integer類型的原子類,用來分配Id,保證其本身是線程安全的private static final AtomicInteger nextId = new AtomicInteger();// 創(chuàng)建一個(gè) ThreadLocal 變零,并且為其賦值private static ThreadLocal <Integer> threadId = ThreadLocal.withInitial(() -> nextId.getAndIncrement());// 獲取id,即從ThreadLocal中獲取對(duì)應(yīng)的值public static int getId() {return threadId.get();}public static void main(String[] args) {for (int i = 0; i < 100; i++) {new Thread(() ->System.out.println(Thread.currentThread().getName()+ ": " + ThreadId.getId())).start();}} }ThreadLocal還有一個(gè)經(jīng)典的使用案例,就是將線程不安全的 SimpleDateFormat 類封裝成線程安全的,原理其實(shí)和上面的例子是一樣:
static class SafeDateFormat {static final ThreadLocal<DateFormat> tl = ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));static DateFormat get() {return tl.get();} }ThreadLocal的底層原理
先來看 set() 方法:
再看 get() 方法:
public T get() {Thread t = Thread.currentThread();// getMap()返回當(dāng)前線程的threadLocalsThreadLocalMap map = getMap(t);if (map != null) {// map存在返回當(dāng)前ThreadLocal對(duì)應(yīng)的value值ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}// map不存在就初始化return setInitialValue(); }看過上面兩個(gè)方法,可以看到它們除了涉及到 Thread 類,還涉及到了一個(gè)類 ThreadLocalMap。那么 Thread、ThreadLocal、ThreadLocalMap 之間是什么關(guān)系呢?
ThreadLocalMap 是 ThreadLocal 的靜態(tài)內(nèi)部類,ThreadLocalMap 的底層是一個(gè) Entry[] table 數(shù)組,Entry 是 ThreadLocalMap 的靜態(tài)內(nèi)部類,以 ThreadLocal 作為 key,以設(shè)置的值作為 value,如下所示:
static class Entry extends WeakReference <ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;} }Thread 持有一個(gè) ThreadLocalMap 的引用 threadLocals:
ThreadLocal.ThreadLocalMap threadLocals = null;所以說,我們通過當(dāng)前線程 Thread t 可以到 t 持有的 ThreadLocalMap,并且通過 ThreadLocal 對(duì)象返回其對(duì)應(yīng)的 value。
ThreadLocal 內(nèi)存泄露問題
使用 Thread.start() 方法是不會(huì)產(chǎn)生內(nèi)存泄露的問題的,只有當(dāng)我們?cè)诰€程池中使用 ThreadLocal 才有可能產(chǎn)生內(nèi)存泄露問題。
內(nèi)存泄露的本質(zhì)是長(zhǎng)生命周期的對(duì)象,持有短生命周期對(duì)象。當(dāng)短生命周期的對(duì)象使用結(jié)束之后,理應(yīng)被垃圾回收器回收,但是它卻被一個(gè)更長(zhǎng)生命周期的對(duì)象引用。通過可達(dá)性分析算法,該短生命周期的對(duì)象被一個(gè)GC Root引用,理應(yīng)被回收的它就無法被回收。
**那為什么在線程池中使用ThreadLocal就可能發(fā)生內(nèi)存泄露的問題呢?**我們就從長(zhǎng)生命周期的對(duì)象,持有短生命周期對(duì)象這個(gè)角度進(jìn)行分析。
線程池作為一種池化資源技術(shù),目的是避免線程的頻繁創(chuàng)建和銷毀。一般來說,線程池中的線程生命周期都很長(zhǎng),是和應(yīng)用程序同生共死的。這就意味著,被 Thread 持有的 ThreadLocalMap 一直都不會(huì)被回收。
ThreadLocalMap 底層是一個(gè) Entry 數(shù)組,Entry是<ThreadLocal,value>對(duì)結(jié)構(gòu)。Entry 對(duì) ThreadLocal 是弱引用(WeakReference),所以ThreadLocal 生命周期之后,是結(jié)束是可以被回收掉的。但是 Entry 對(duì) value 強(qiáng)引用的,所以即便 Value 的生命周期結(jié)束了,Value 也是無法被回收的,從而導(dǎo)致內(nèi)存泄露。
InheritableThreadLocal 與繼承性
使用 ThreadLocal 還有這樣一種需求,ThreadLocal 創(chuàng)建了線程變量 V,然后希望該線程創(chuàng)建的子線程也能訪問到父線程的線程變量 V。
為此 Java 提供了 InheritableThreadLocal 來支持這種特性,InheritableThreadLocal 繼承自 ThreadLocal,用法其實(shí)和 ThreadLocal 一樣。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {最后做一個(gè)小結(jié),多線程同時(shí)讀寫共享變量就有可能產(chǎn)生并發(fā)問題。一種解決并發(fā)問題的思路就是避免變量被共享。與之對(duì)應(yīng)的技術(shù)有線程隔離(局部變量),以及線程本地存儲(chǔ)ThreadLocal。
相比于使用局部變量,ThreadLocal 存儲(chǔ)的變量可以供線程中的方法共享,單線程對(duì)共享變量的讀寫必定是線程安全的。
總結(jié)
以上是生活随笔為你收集整理的线程安全的map_ThreadLocal | 线程本地存储的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 向量距离计算 java_Milvus 向
- 下一篇: 泰坦尼克号数据集_泰坦尼克号项目可视化