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