併發編程 - 死鎖 - ThreadLocal

死鎖

死鎖是指兩個或多個進程在等待對方釋放資源的情況下無限期地阻塞的現象,如下代碼所示:

public static void main(String[] args) {
  final Object resource1 = "resource1";
  final Object resource2 = "resource2";

  Thread t1 = new Thread(() -> {
      synchronized (resource1) {
          System.out.println("Thread 1: locked resource 1");

          try {
              Thread.sleep(100);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }

          synchronized (resource2) {
              System.out.println("Thread 1: locked resource 2");
          }
      }
  });

  Thread t2 = new Thread(() -> {
      synchronized (resource2) {
          System.out.println("Thread 2: locked resource 2");

          try {
              Thread.sleep(100);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }

          synchronized (resource1) {
              System.out.println("Thread 2: locked resource 1");
          }
      }
  });

  t1.start();
  t2.start();
}
複製代碼

線程 t1 和 t2,它們都試圖同時訪問兩個資源 resource1 和 resource2。線程 t1 首先獲取了資源 resource1,然後等待 100 毫秒。在此期間,線程 t2 獲取了資源 resource2。接下來,線程 t1 嘗試獲取資源 resource2,但是由於它已經被線程 t2 佔用了,所以 t1 等待 t2 釋放資源 resource2。同時,線程 t2 嘗試獲取資源 resource1,但是由於它已經被線程 t1 佔用了,所以 t2 等待 t1 釋放資源 resource1。這就導致了死鎖的情況。

死鎖發生的條件

死鎖的解決方案

上述的四個條件中只要我們破壞其中一個就能避免死鎖的發生,首先第一個我們是沒有辦法修改的,因爲這個是業務需要,修改的話可能會對我們自己的業務造成影響出現 bug。

ThreadLocal

線程隔離機制。 ThreadLocal 實際上一種線程隔離機制,也是爲了保證在多線程環境下對於共享變量的訪問的安全性
底層實現其實就是將共享變量拷貝一份存存到線程的工作內存,然後使用。

具體使用方式如下:

public class UserContextHolder {
    private static final ThreadLocal<UserContext> userContextThreadLocal = new ThreadLocal<>();

    public static UserContext getUserContext() {
        UserContext userContext = userContextThreadLocal.get();
        if (userContext == null) {
            userContext = new UserContext();
            userContextThreadLocal.set(userContext);
        }
        return userContext;
    }

    public static void clear() {
        userContextThreadLocal.remove();
    }
}
複製代碼

上面的代碼定義了一個 UserContextHolder 類,其中包含了一個 ThreadLocal 對象 userContextThreadLocal,用於存儲 UserContext 類型的變量副本。

當調用 getUserContext() 方法時,首先嚐試從當前線程的變量副本中獲取 UserContext 對象,如果不存在則創建一個新的 UserContext 對象,並將其存儲到變量副本中。這樣每個線程都有自己獨立的 UserContext 對象,不會發生線程間數據共享的問題。

當需要清除線程變量時,可以調用 clear() 方法來刪除當前線程的變量副本,避免內存泄漏。

通過 ThreadLocal 實現線程上下文的存儲,可以避免將上下文對象作爲參數傳遞給每個方法的麻煩,從而提高代碼的可讀性和可維護性。

ThreadLocal 原理分析

首先看一下 set 源碼如下

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 這個 ThreadLocalMap 就是當前線程的 threadLocals

再看一下 ThreadLocalMap 結構

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;
        }
    }

    /**
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    private Entry[] table;

    /**
     * The number of entries in the table.
     */
    private int size = 0;

    /**
     * The next size value at which to resize.
     */
    private int threshold; // Default to 0
    .....
  }
複製代碼

發現其實最主要的就是一個 table 數組,數組中存着 Entry 對象其實就是一個鍵值對。
類似於 HashMap,它內部維護了一個 Entry 數組,每個 Entry 對象包含了一個 key 和一個 value,用於存儲 ThreadLocal 對象和其對應的變量副本。
繼續往下 set 看源碼 ThreadLocalMap 存在則直接插入值不存在則創建

數組的初始大小是 16

然後 threadLocalHashCode 與運算獲取到數組的下標然後存儲

如果已經存在則直接 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)]) {
        if (e.refersTo(key)) {
            e.value = value;
            return;
        }

        if (e.refersTo(null)) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
複製代碼

如果對應的 i 中的 key, 就是我們想要則則直接賦值就可以了。如果 Entry 不是 null 但是 key 是 null,這裏需要注意的是這個 key 就是 ThreadLocal<?> 實例,所以這裏有人可能會奇怪這裏不是一直持有的嗎爲什麼會爲 null

可以看到是這裏是弱引用所以如果外面沒有變量指向這個實例的話那麼就會被釋放,所以 key 就會變成 null 最後再看 replaceStaleEntry 方法

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)]) {
        if (e.refersTo(key)) {
            e.value = value;
            return;
        }

        if (e.refersTo(null)) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
複製代碼

主要作用是清除被 GC 回收的 key 對象對應的 Entry 對象。當調用 set 方法時,如果發現當前線程對應的 ThreadLocalMap 對象中存在已經被 GC 回收的 key 對象對應的 Entry 對象,那麼就會調用 replaceStaleEntry 方法來清除這些 “髒” 的 Entry 對象,以確保 ThreadLocalMap 中只保留有效的 Entry 對象。

具體來說,replaceStaleEntry 方法會遍歷引用隊列中的所有 Entry 對象,對於每個 Entry 對象,如果其對應的 key 對象已經被 GC 回收,那麼就將該 Entry 對象從 ThreadLocalMap 中移除。如果發現有多個 Entry 對象對應同一個 key 對象,那麼只保留最新的那個 Entry 對象,將其餘的 Entry 對象都移除

get 源碼

所以此時就循環遍歷查找

Thread.join

上文中再說 Happens-Before 可見性模型說到了 Thread.join,爲什麼使用 Thread.join 可見呢。
底層其實就是用的是 wait/notify,她會是當前線程阻塞一直等到上面的代碼執行完之後才被喚醒:

來源:https://www.toutiao.com/article/7231111886082392636/?log_from=0ed030a5c6eb4_1684976790621

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/3i6ZLF1fMttqUc5baaYs5A