Java線程安全面試題,你真的瞭解嗎?

多個線程無論以何種方式訪問某個類,而且在主調代碼中不須要進行同步,都能表現正確的行爲。java

線程安全有如下幾種實現方式:安全

不可變
不可變(Immutable)的對象必定是線程安全的,不須要再採起任何的線程安全保障措施。只要一個不可變的對象被正確地構建出來,永遠也不會看到它在多個線程之中處於不一致的狀態。多線程環境下,應當儘可能使對象成爲不可變,來知足線程安全。服務器

不可變的類型:數據結構

final 關鍵字修飾的基本數據類型
String
枚舉類型
Number 部分子類,如 Long 和 Double 等數值包裝類型,BigInteger 和 BigDecimal 等大數據類型。但同爲 Number 的原子類 AtomicInteger 和 AtomicLong 則是可變的。
對於集合類型,可使用 Collections.unmodifiableXXX() 方法來獲取一個不可變的集合。多線程

public class ImmutableExample {架構

public static void main(String[] args) {
    Map<String, Integer> map = new HashMap<>();
    Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
    unmodifiableMap.put("a", 1);
}

}
Exception in thread "main" java.lang.UnsupportedOperationException併發

at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
at ImmutableExample.main(ImmutableExample.java:9)

Collections.unmodifiableXXX() 先對原始的集合進行拷貝,須要對集合進行修改的方法都直接拋出異常。分佈式

public V put(K key, V value) {高併發

throw new UnsupportedOperationException();

}
互斥同步
synchronized 和 ReentrantLock。性能

非阻塞同步
互斥同步最主要的問題就是線程阻塞和喚醒所帶來的性能問題,所以這種同步也稱爲阻塞同步。

互斥同步屬於一種悲觀的併發策略,老是認爲只要不去作正確的同步措施,那就確定會出現問題。不管共享數據是否真的會出現競爭,它都要進行加鎖(這裏討論的是概念模型,實際上虛擬機會優化掉很大一部分沒必要要的加鎖)、用戶態核心態轉換、維護鎖計數器和檢查是否有被阻塞的線程須要喚醒等操做。

  1. CAS

隨着硬件指令集的發展,咱們可使用基於衝突檢測的樂觀併發策略:先進行操做,若是沒有其它線程爭用共享數據,那操做就成功了,不然採起補償措施(不斷地重試,直到成功爲止)。這種樂觀的併發策略的許多實現都不須要將線程阻塞,所以這種同步操做稱爲非阻塞同步。

樂觀鎖須要操做和衝突檢測這兩個步驟具有原子性,這裏就不能再使用互斥同步來保證了,只能靠硬件來完成。硬件支持的原子性操做最典型的是:比較並交換(Compare-and-Swap,CAS)。CAS 指令須要有 3 個操做數,分別是內存地址 V、舊的預期值 A 和新值 B。當執行操做時,只有當 V 的值等於 A,纔將 V 的值更新爲 B。

  1. AtomicInteger

J.U.C 包裏面的整數原子類 AtomicInteger 的方法調用了 Unsafe 類的 CAS 操做。

如下代碼使用了 AtomicInteger 執行了自增的操做。

private AtomicInteger cnt = new AtomicInteger();

public void add() {

cnt.incrementAndGet();

}
如下代碼是 incrementAndGet() 的源碼,它調用了 Unsafe 的 getAndAddInt() 。

public final int incrementAndGet() {

return unsafe.getAndAddInt(this, valueOffset, 1) + 1;

}
如下代碼是 getAndAddInt() 源碼,var1 指示對象內存地址,var2 指示該字段相對對象內存地址的偏移,var4 指示操做須要加的數值,這裏爲 1。經過 getIntVolatile(var1, var2) 獲得舊的預期值,經過調用 compareAndSwapInt() 來進行 CAS 比較,若是該字段內存地址中的值等於 var5,那麼就更新內存地址爲 var1+var2 的變量爲 var5+var4。

能夠看到 getAndAddInt() 在一個循環中進行,發生衝突的作法是不斷的進行重試。

public final int getAndAddInt(Object var1, long var2, int var4) {

int var5;
do {
    var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;

}

  1. ABA

若是一個變量初次讀取的時候是 A 值,它的值被改爲了 B,後來又被改回爲 A,那 CAS 操做就會誤認爲它歷來沒有被改變過。

J.U.C 包提供了一個帶有標記的原子引用類 AtomicStampedReference 來解決這個問題,它能夠經過控制變量值的版原本保證 CAS 的正確性。大部分狀況下 ABA 問題不會影響程序併發的正確性,若是須要解決 ABA 問題,改用傳統的互斥同步可能會比原子類更高效。

無同步方案
要保證線程安全,並非必定就要進行同步。若是一個方法原本就不涉及共享數據,那它天然就無須任何同步措施去保證正確性。

  1. 棧封閉

多個線程訪問同一個方法的局部變量時,不會出現線程安全問題,由於局部變量存儲在虛擬機棧中,屬於線程私有的。

public class StackClosedExample {

public void add100() {
    int cnt = 0;
    for (int i = 0; i < 100; i++) {
        cnt++;
    }
    System.out.println(cnt);
}

}
public static void main(String[] args) {

StackClosedExample example = new StackClosedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> example.add100());
executorService.execute(() -> example.add100());
executorService.shutdown();

}
100
100

  1. 線程本地存儲(Thread Local Storage)

若是一段代碼中所須要的數據必須與其餘代碼共享,那就看看這些共享數據的代碼是否能保證在同一個線程中執行。若是能保證,咱們就能夠把共享數據的可見範圍限制在同一個線程以內,這樣,無須同步也能保證線程之間不出現數據爭用的問題。

符合這種特色的應用並很多見,大部分使用消費隊列的架構模式(如「生產者-消費者」模式)都會將產品的消費過程儘可能在一個線程中消費完。其中最重要的一個應用實例就是經典 Web 交互模型中的「一個請求對應一個服務器線程」(Thread-per-Request)的處理方式,這種處理方式的普遍應用使得不少 Web 服務端應用均可以使用線程本地存儲來解決線程安全問題。

可使用 java.lang.ThreadLocal 類來實現線程本地存儲功能。

對於如下代碼,thread1 中設置 threadLocal 爲 1,而 thread2 設置 threadLocal 爲 2。過了一段時間以後,thread1 讀取 threadLocal 依然是 1,不受 thread2 的影響。

public class ThreadLocalExample {

public static void main(String[] args) {
    ThreadLocal threadLocal = new ThreadLocal();
    Thread thread1 = new Thread(() -> {
        threadLocal.set(1);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(threadLocal.get());
        threadLocal.remove();
    });
    Thread thread2 = new Thread(() -> {
        threadLocal.set(2);
        threadLocal.remove();
    });
    thread1.start();
    thread2.start();
}

}
1
爲了理解 ThreadLocal,先看如下代碼:

public class ThreadLocalExample1 {

public static void main(String[] args) {
    ThreadLocal threadLocal1 = new ThreadLocal();
    ThreadLocal threadLocal2 = new ThreadLocal();
    Thread thread1 = new Thread(() -> {
        threadLocal1.set(1);
        threadLocal2.set(1);
    });
    Thread thread2 = new Thread(() -> {
        threadLocal1.set(2);
        threadLocal2.set(2);
    });
    thread1.start();
    thread2.start();
}

}
它所對應的底層結構圖爲:

clipboard.png

每一個 Thread 都有一個 ThreadLocal.ThreadLocalMap 對象。

/* ThreadLocal values pertaining to this thread. This map is maintained

  • by the ThreadLocal class. */

ThreadLocal.ThreadLocalMap threadLocals = null;
當調用一個 ThreadLocal 的 set(T value) 方法時,先獲得當前線程的 ThreadLocalMap 對象,而後將 ThreadLocal->value 鍵值對插入到該 Map 中。

public void set(T value) {

Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
    map.set(this, value);
else
    createMap(t, value);

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

}
ThreadLocal 從理論上講並非用來解決多線程併發問題的,由於根本不存在多線程競爭。

在一些場景 (尤爲是使用線程池) 下,因爲 ThreadLocal.ThreadLocalMap 的底層數據結構致使 ThreadLocal 有內存泄漏的狀況,應該儘量在每次使用 ThreadLocal 後手動調用 remove(),以免出現 ThreadLocal 經典的內存泄漏甚至是形成自身業務混亂的風險。

  1. 可重入代碼(Reentrant Code)

這種代碼也叫作純代碼(Pure Code),能夠在代碼執行的任什麼時候刻中斷它,轉而去執行另一段代碼(包括遞歸調用它自己),而在控制權返回後,原來的程序不會出現任何錯誤。

可重入代碼有一些共同的特徵,例如不依賴存儲在堆上的數據和公用的系統資源、用到的狀態量都由參數中傳入、不調用非可重入的方法等。

免費Java高級資料須要本身領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分佈式等教程,一共30G。
傳送門:https://mp.weixin.qq.com/s/Jz...

相關文章
相關標籤/搜索