天天看點

java cas 非線程安全_Java 中的各種鎖和 CAS + 面試題

Java 中的各種鎖和 CAS + 面試題

如果說快速了解多線程有什麼捷徑的話,那本文介紹的各種鎖無疑是其中之一,它不但為我們開發多線程程式提供理論支援,還是面試中經常被問到的核心面試題之一。是以下面就讓我們一起深入地學習一下這些鎖吧。

樂觀鎖和悲觀鎖

悲觀鎖和樂觀鎖并不是某個具體的“鎖”而是一種并發程式設計的基本概念。樂觀鎖和悲觀鎖最早出現在資料庫的設計當中,後來逐漸被 Java 的并發包所引入。

悲觀鎖

悲觀鎖認為對于同一個資料的并發操作,一定是會發生修改的,哪怕沒有修改,也會認為修改。是以對于同一個資料的并發操作,悲觀鎖采取加鎖的形式。悲觀地認為,不加鎖的并發操作一定會出問題。

樂觀鎖

樂觀鎖正好和悲觀鎖相反,它擷取資料的時候,并不擔心資料被修改,每次擷取資料的時候也不會加鎖,隻是在更新資料的時候,通過判斷現有的資料是否和原資料一緻來判斷資料是否被其他線程操作,如果沒被其他線程修改則進行資料更新,如果被其他線程修改則不進行資料更新。

公平鎖和非公平鎖

根據線程擷取鎖的搶占機制,鎖又可以分為公平鎖和非公平鎖。

公平鎖

公平鎖是指多個線程按照申請鎖的順序來擷取鎖。

非公平鎖

非公平鎖是指多個線程擷取鎖的順序并不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先擷取鎖。

ReentrantLock 提供了公平鎖和非公平鎖的實作。

公平鎖:new ReentrantLock(true)

非公平鎖:new ReentrantLock(false)

如果構造函數不傳任何參數的時候,預設提供的是非公平鎖。

獨占鎖和共享鎖

根據鎖能否被多個線程持有,可以把鎖分為獨占鎖和共享鎖。

獨占鎖

獨占鎖是指任何時候都隻有一個線程能執行資源操作。

共享鎖

共享鎖指定是可以同時被多個線程讀取,但隻能被一個線程修改。比如 Java 中的 ReentrantReadWriteLock 就是共享鎖的實作方式,它允許一個線程進行寫操作,允許多個線程讀操作。

ReentrantReadWriteLock 共享鎖示範代碼如下:

public class ReadWriteLockTest {

public static void main(String[] args) throws InterruptedException {

final MyReadWriteLock rwLock = new MyReadWriteLock();

// 建立讀鎖 r1 和 r2

Thread r1 = new Thread(new Runnable() {

@Override

public void run() {

rwLock.read();

}

}, "r1");

Thread r2 = new Thread(new Runnable() {

@Override

public void run() {

rwLock.read();

}

}, "r2");

r1.start();

r2.start();

// 等待同時讀取線程執行完成

r1.join();

r2.join();

// 開啟寫鎖的操作

new Thread(new Runnable() {

@Override

public void run() {

rwLock.write();

}

}, "w1").start();

new Thread(new Runnable() {

@Override

public void run() {

rwLock.write();

}

}, "w2").start();

}

static class MyReadWriteLock {

ReadWriteLock lock = new ReentrantReadWriteLock();

public void read() {

try {

lock.readLock().lock();

System.out.println("讀操作,進入 | 線程:" + Thread.currentThread().getName());

Thread.sleep(3000);

System.out.println("讀操作,退出 | 線程:" + Thread.currentThread().getName());

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

lock.readLock().unlock();

}

}

public void write() {

try {

lock.writeLock().lock();

System.out.println("寫操作,進入 | 線程:" + Thread.currentThread().getName());

Thread.sleep(3000);

System.out.println("寫操作,退出 | 線程:" + Thread.currentThread().getName());

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

lock.writeLock().unlock();

}

}

}

}

以上程式執行結果如下:

讀操作,進入 | 線程:r1

讀操作,進入 | 線程:r2

讀操作,退出 | 線程:r1

讀操作,退出 | 線程:r2

寫操作,進入 | 線程:w1

寫操作,退出 | 線程:w1

寫操作,進入 | 線程:w2

寫操作,退出 | 線程:w2

可重入鎖

可重入鎖指的是該線程擷取了該鎖之後,可以無限次的進入該鎖鎖住的代碼。

自旋鎖

自旋鎖是指嘗試擷取鎖的線程不會立即阻塞,而是采用循環的方式去嘗試擷取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗 CPU。

CAS 與 ABA

CAS(Compare and Swap)比較并交換,是一種樂觀鎖的實作,是用非阻塞算法來代替鎖定,其中 java.util.concurrent 包下的 AtomicInteger 就是借助 CAS 來實作的。

但 CAS 也不是沒有任何副作用,比如著名的 ABA 問題就是 CAS 引起的。

ABA 問題描述

老王去銀行取錢,餘額有 200 元,老王取 100 元,但因為程式的問題,啟動了兩個線程,線程一和線程二進行比對扣款,線程一擷取原本有 200 元,扣除 100 元,餘額等于 100 元,此時阿裡給老王轉賬 100 元,于是啟動了線程三搶先線上程二之前執行了轉賬操作,把 100 元又變成了 200 元,而此時線程二對比自己事先拿到的 200 元和此時經過改動的 200 元值一樣,就進行了減法操作,把餘額又變成了 100 元。這顯然不是我們要的正确結果,我們想要的結果是餘額減少了 100 元,又增加了 100 元,餘額還是 200 元,而此時餘額變成了 100 元,顯然有悖常理,這就是著名的 ABA 的問題。

執行流程如下。

線程一:取款,擷取原值 200 元,與 200 元比對成功,減去 100 元,修改結果為 100 元。

線程二:取款,擷取原值 200 元,阻塞等待修改。

線程三:轉賬,擷取原值 100 元,與 100 元比對成功,加上 100 元,修改結果為 200 元。

線程二:取款,恢複執行,原值為 200 元,與 200 元對比成功,減去 100 元,修改結果為 100 元。

最終的結果是 100 元。

ABA 問題的解決

常見解決 ABA 問題的方案加版本号,來區分值是否有變動。以老王取錢的例子為例,如果加上版本号,執行流程如下。

線程一:取款,擷取原值 200_V1,與 200_V1 比對成功,減去 100 元,修改結果為 100_V2。

線程二:取款,擷取原值 200_V1 阻塞等待修改。

線程三:轉賬,擷取原值 100_V2,與 100_V2 對比成功,加 100 元,修改結果為 200_V3。

線程二:取款,恢複執行,原值 200_V1 與現值 200_V3 對比不相等,退出修改。

最終的結果為 200 元,這顯然是我們需要的結果。

在程式中,要怎麼解決 ABA 的問題呢?

在 JDK 1.5 的時候,Java 提供了一個 AtomicStampedReference 原子引用變量,通過添加版本号來解決 ABA 的問題,具體使用示例如下:

String name = "洲洋";

String newName = "Java";

AtomicStampedReference as = new AtomicStampedReference(name, 1);

System.out.println("值:" + as.getReference() + " | Stamp:" + as.getStamp());

as.compareAndSet(name, newName, as.getStamp(), as.getStamp() + 1);

System.out.println("值:" + as.getReference() + " | Stamp:" + as.getStamp());

以上程式執行結果如下:

值:老王 | Stamp:1

值:Java | Stamp:2

相關面試題

1.synchronized 是哪種鎖的實作?為什麼?

答:synchronized 是悲觀鎖的實作,因為 synchronized 修飾的代碼,每次執行時會進行加鎖操作,同時隻允許一個線程進行操作,是以它是悲觀鎖的實作。

2.new ReentrantLock() 建立的是公平鎖還是非公平鎖?

答:非公平鎖,檢視 ReentrantLock 的實作源碼可知。

/\*\* \* Creates an instance of {@code ReentrantLock}. \* This is equivalent to using {@code ReentrantLock(false)}. \*/

public ReentrantLock() {

sync = new NonfairSync();

}

3.synchronized 使用的是公平鎖還是非公平鎖?

答:synchronized 使用的是非公平鎖,并且是不可設定的。這是因為非公平鎖的吞吐量大于公平鎖,并且是主流作業系統線程排程的基本選擇,是以這也是 synchronized 使用非公平鎖原由。

4.為什麼非公平鎖吞吐量大于公平鎖?

答:比如 A 占用鎖的時候,B 請求擷取鎖,發現被 A 占用之後,堵塞等待被喚醒,這個時候 C 同時來擷取 A 占用的鎖,如果是公平鎖 C 後來者發現不可用之後一定排在 B 之後等待被喚醒,而非公平鎖則可以讓 C 先用,在 B 被喚醒之前 C 已經使用完成,進而節省了 C 等待和喚醒之間的性能消耗,這就是非公平鎖比公平鎖吞吐量大的原因。

5.volatile 的作用是什麼?

答:volatile 是 Java 虛拟機提供的最輕量級的同步機制。

當變量被定義成 volatile 之後,具備兩種特性:

保證此變量對所有線程的可見性,當一條線程修改了這個變量的值,修改的新值對于其他線程是可見的(可以立即得知的);

禁止指令重排序優化,普通變量僅僅能保證在該方法執行過程中,得到正确結果,但是不保證程式代碼的執行順序。

6.volatile 對比 synchronized 有什麼差別?

答:synchronized 既能保證可見性,又能保證原子性,而 volatile 隻能保證可見性,無法保證原子性。比如,i++ 如果使用 synchronized 修飾是線程安全的,而 volatile 會有線程安全的問題。

7.CAS 是如何實作的?

答: CAS(Compare and Swap)比較并交換,CAS 是通過調用 JNI(Java Native Interface)的代碼實作的,比如,在 Windows 系統 CAS 就是借助 C 語言來調用 CPU 底層指令實作的。

8.CAS 會産生什麼問題?應該怎麼解決?

答:CAS 是标準的樂觀鎖的實作,會産生 ABA 的問題(詳見正文)。

ABA 通常的解決辦法是添加版本号,每次修改操作時版本号加一,這樣資料對比的時候就不會出現 ABA 的問題了。

9.以下說法錯誤的是?

A:獨占鎖是指任何時候都隻有一個線程能執行資源操作

B:共享鎖指定是可以同時被多個線程讀取和修改

C:公平鎖是指多個線程按照申請鎖的順序來擷取鎖

D:非公平鎖是指多個線程擷取鎖的順序并不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先擷取鎖

答:B

題目解析:共享鎖指定是可以同時被多個線程讀取,但隻能被一個線程修改。

總結

本文介紹了 Java 中各種鎖,明白了 Java 程式中比較常用的為非公平鎖而非公平鎖,原因在于非公平鎖的吞吐量要更大,并且發生線程“饑餓”的情況很少,是風險遠小于收益的事是以可以廣而用之。又重點介紹了 CAS 和著名的 ABA 的問題,以及解決 ABA 的常見手段:添加版本号,可以通過 Java 自身提供的 AtomicStampedReference(原子引用變量)來解決 ABA 的問題,至此我們對 Java 多線程的了解又向前邁了一大步。

歡迎關注我的公衆号,回複關鍵字“Java” ,将會有大禮相送!!! 祝各位面試成功!!!

java cas 非線程安全_Java 中的各種鎖和 CAS + 面試題

%97%E5%8F%B7%E4%BA%8C%E7%BB%B4%E7%A0%81.png)