天天看點

Java系列9-線程知識詳解計數器,程序,線程排程

作者:飛雪波波

CyclicBarrier、CountDownLatch、Semaphore 的用法

1. CountDownLatch(線程計數器 )

CountDownLatch 類似于 java.util.concurrent 包下,利用它可以實作類似計數器的功能。比如有一個任務 A,它要等待其他 4 個任務執行完畢之後才能執行,此時就可以利用 CountDownLatch來實作這種功能了。

final CountDownLatch latch = new CountDownLatch(2);

new Thread(){public void run() {

System.out.println("子線程"+Thread.currentThread().getName()+"正在執行");

Thread.sleep(3000);

System.out.println("子線程"+Thread.currentThread().getName()+"執行完畢");

latch.countDown();

};}.start();

new Thread(){

public void run() {

System.out.println("子線程"+Thread.currentThread().getName()+"正在執行");

Thread.sleep(3000);

System.out.println("子線程"+Thread.currentThread().getName()+"執行完畢");

latch.countDown();

};}.start();

System.out.println("等待 2 個子線程執行完畢...");

latch.await();

System.out.println("2 個子線程已經執行完畢");

System.out.println("繼續執行主線程");

}

2. CyclicBarrier(回環栅欄-等待至 barrier 狀态再全部同時執行)

字面意思回環栅欄,通過它可以實作讓一組線程等待至某個狀态之後再全部同時執行。叫做回環是因為當所有等待線程都被釋放以後,CyclicBarrier 可以被重用。我們暫且把這個狀态就叫做barrier,當調用 await()方法之後,線程就處于 barrier 了。CyclicBarrier 中最重要的方法就是 await 方法,它有 2 個重載版本:

1. public int await():用來挂起目前線程,直至所有線程都到達 barrier 狀态再同時執行後續任務;

2. public int await(long timeout, TimeUnit unit):讓這些線程等待至一定的時間,如果還有線程沒有到達 barrier 狀态就直接讓到達 barrier 的線程執行後續任務。具體使用如下,另外 CyclicBarrier 是可以重用的。

public static void main(String[] args) {

int N = 4;

CyclicBarrier barrier = new CyclicBarrier(N);

for(int i=0;i<N;i++)

new Writer(barrier).start();

}

static class Writer extends Thread{

private CyclicBarrier cyclicBarrier;

public Writer(CyclicBarrier cyclicBarrier) {

this.cyclicBarrier = cyclicBarrier;

}

@Override

public void run() {

try {

Thread.sleep(5000);

//以睡眠來模拟線程需要預定寫入資料操作System.out.println(" 線程 "+Thread.currentThread().getName()+" 寫 入數 據完畢,等待其他線程寫入完畢");

cyclicBarrier.await();

} catch (InterruptedException e) {

e.printStackTrace();

}catch(BrokenBarrierException e){

e.printStackTrace();

}

System.out.println("所有線程寫入完畢,繼續處理其他任務,比如資料操作");

}

}

3. Semaphore(信号量-控制同時通路的線程個數)

Semaphore 翻譯成字面意思為 信号量,Semaphore 可以控制同時通路的線程個數,通過acquire() 擷取一個許可,如果沒有就等待,而 release() 釋放一個許可。Semaphore 類中比較重要的幾個方法:

1. public void acquire(): 用來擷取一個許可,若無許可能夠獲得,則會一直等待,直到獲得許可。

2. public void acquire(int permits):擷取 permits 個許可

3. public void release() { } :釋放許可。注意,在釋放許可之前,必須先獲獲得許可。

4. public void release(int permits) { }:釋放 permits 個許可上面 4 個方法都會被阻塞,如果想立即得到執行結果,可以使用下面幾個方法

1. public boolean tryAcquire():嘗試擷取一個許可,若擷取成功,則立即傳回 true,若擷取失敗,則立即傳回 false

2. public boolean tryAcquire(long timeout, TimeUnit unit):嘗試擷取一個許可,若在指定的時間内擷取成功,則立即傳回 true,否則則立即傳回 false

3. public boolean tryAcquire(int permits):嘗試擷取 permits 個許可,若擷取成功,則立即傳回 true,若擷取失敗,則立即傳回 false

4. public boolean tryAcquire(int permits, long timeout, TimeUnit unit): 嘗試擷取 permits個許可,若在指定的時間内擷取成功,則立即傳回 true,否則則立即傳回 false

5. 還可以通過 availablePermits()方法得到可用的許可數目。

例子:若一個工廠有 5 台機器,但是有 8 個勞工,一台機器同時隻能被一個勞工使用,隻有使用完了,其他勞工才能繼續使用。那麼我們就可以通過 Semaphore 來實作:int N = 8; //勞工數

Semaphore semaphore = new Semaphore(5); //機器數目

for(int i=0;i<N;i++)

new Worker(i,semaphore).start();

}

static class Worker extends Thread{

private int num;

private Semaphore semaphore;

public Worker(int num,Semaphore semaphore){

this.num = num;

this.semaphore = semaphore;

}

@Override

public void run() {

try {

semaphore.acquire();

System.out.println("勞工"+this.num+"占用一個機器在生産...");

Thread.sleep(2000);

System.out.println("勞工"+this.num+"釋放出機器");

semaphore.release();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

CountDownLatch 和 CyclicBarrier 都能夠實作線程之間的等待,隻不過它們側重點不同;CountDownLatch 一般用于某個線程 A 等待若幹個其他線程執行完任務之後,它才執行;而 CyclicBarrier 一般用于一組線程互相等待至某個狀态,然後這一組線程再同時執行;另外,CountDownLatch 是不能夠重用的,而 CyclicBarrier 是可以重用的。

Semaphore 其實和鎖有點類似,它一般用于控制對某組資源的通路權限。

volatile 關鍵字的作用(變量可見性、禁止重排序)

Java 語言提供了一種稍弱的同步機制,即 volatile 變量,用來確定将變量的更新操作通知到其他線程。volatile 變量具備兩種特性,volatile 變量不會被緩存在寄存器或者對其他處理器不可見的地方,是以在讀取 volatile 類型的變量時總會傳回最新寫入的值。

變量可見性其一是保證該變量對所有線程可見,這裡的可見性指的是當一個線程修改了變量的值,那麼新的值對于其他線程是可以立即擷取的。

禁止重排序volatile 禁止了指令重排。比 sychronized 更輕量級的同步鎖在通路 volatile 變量時不會執行加鎖操作,是以也就不會使執行線程阻塞,是以 volatile 變量是一種比 sychronized 關鍵字更輕量級的同步機制。volatile 适合這種場景:一個變量被多個線程共享,線程直接給這個變量指派。

Java系列9-線程知識詳解計數器,程式,線程排程

當對非 volatile 變量進行讀寫的時候,每個線程先從記憶體拷貝變量到 CPU 緩存中。如果計算機有多個 CPU,每個線程可能在不同的 CPU 上被處理,這意味着每個線程可以拷貝到不同的 CPUcache 中。而聲明變量是 volatile 的,JVM 保證了每次讀變量都從記憶體中讀,跳過 CPU cache這一步。适用場景值得說明的是對 volatile 變量的單次讀/寫操作可以保證原子性的,如 long 和 double 類型變量,但是并不能保證 i++這種操作的原子性,因為本質上 i++是讀、寫兩次操作。在某些場景下可以代替 Synchronized。但是,volatile 的不能完全取代 Synchronized 的位置,隻有在一些特殊的場景下,才能适用 volatile。總的來說,必須同時滿足下面兩個條件才能保證在并發環境的線程安全:

(1)對變量的寫操作不依賴于目前值(比如 i++),或者說是單純的變量指派(booleanflag = true)。

(2)該變量沒有包含在具有其他變量的不變式中,也就是說,不同的 volatile 變量之間,不能互相依賴。隻有在狀态真正獨立于程式内其他内容時才能使用 volatile。

如何在兩個線程之間共享資料

Java 裡面進行多線程通信的主要方式就是共享記憶體的方式,共享記憶體主要的關注點有兩個:可見性和有序性原子性。Java 記憶體模型(JMM)解決了可見性和有序性的問題,而鎖解決了原子性的問題,理想情況下我們希望做到“同步”和“互斥”。有以下正常實作方法:将資料抽象成一個類,并将資料的操作作為這個類的方法

1. 将資料抽象成一個類,并将對這個資料的操作作為這個類的方法,這麼設計可以和容易做到同步,隻要在方法上加”synchronized“

public class MyData {

private int j=0;

public synchronized void add(){

j++;

System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j);

}

public synchronized void dec(){

j--;

System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j);

}

public int getData(){

return j;

}

}

public class AddRunnable implements Runnable{

MyData data;

public AddRunnable(MyData data){

this.data= data;

}

public void run() {

data.add();

}

}

public class DecRunnable implements Runnable {

MyData data;

public DecRunnable(MyData data){

this.data = data;

}

public void run() {

data.dec();

}

}

public static void main(String[] args) {

MyData data = new MyData();

Runnable add = new AddRunnable(data);

Runnable dec = new DecRunnable(data);

for(int i=0;i<2;i++){

new Thread(add).start();

new Thread(dec).start();

}

Runnable 對象作為一個類的内部類

2. 将 Runnable 對象作為一個類的内部類,共享資料作為這個類的成員變量,每個線程對共享資料的操作方法也封裝在外部類,以便實作對資料的各個操作的同步和互斥,作為内部類的各個 Runnable 對象調用外部類的這些方法。

public class MyData {

private int j=0;

public synchronized void add(){

j++;

System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j);

}

public synchronized void dec(){

j--;

System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j);

}

public int getData(){

return j;

}

}

public class TestThread {

public static void main(String[] args) {

final MyData data = new MyData();

for(int i=0;i<2;i++){

new Thread(new Runnable(){

public void run() {

data.add();

}

}).start();

new Thread(new Runnable(){

public void run() {

data.dec();

}

}).start();

}

}

}

ThreadLocal 作用(線程本地存儲)

ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,ThreadLocal 的作用是提供線程内的局部變量,這種變量線上程的生命周期内起作用,減少同一個線程内多個函數或者元件之間一些公共變量的傳遞的複雜度。ThreadLocalMap(線程的一個屬性)

1. 每個線程中都有一個自己的 ThreadLocalMap 類對象,可以将線程自己的對象保持到其中,各管各的,線程可以正确的通路到自己的對象。

2. 将一個共用的 ThreadLocal 靜态執行個體作為 key,将不同對象的引用儲存到不同線程的ThreadLocalMap 中,然後線上程執行的各處通過這個靜态 ThreadLocal 執行個體的 get()方法取得自己線程儲存的那個對象,避免了将這個對象作為參數傳遞的麻煩。

3. ThreadLocalMap 其實就是線程裡面的一個屬性,它在 Thread 類中定義ThreadLocal.ThreadLocalMap threadLocals = null;

Java系列9-線程知識詳解計數器,程式,線程排程

使用場景最常見的 ThreadLocal 使用場景為 用來解決 資料庫連接配接、Session 管理等。

private static final ThreadLocal threadSession = new ThreadLocal();

public static Session getSession() throws InfrastructureException {

Session s = (Session) threadSession.get();

try {

if (s == null) {

s = getSessionFactory().openSession();

threadSession.set(s);

}

} catch (HibernateException ex) {

throw new InfrastructureException(ex);

}

return s;

}

synchronized 和 ReentrantLock 的差別

1. 兩者的共同點:

1. 都是用來協調多線程對共享對象、變量的通路

2. 都是可重入鎖,同一線程可以多次獲得同一個鎖

3. 都保證了可見性和互斥性

2. 兩者的不同點:

1. ReentrantLock 顯示的獲得、釋放鎖,synchronized 隐式獲得釋放鎖

2. ReentrantLock 可響應中斷、可輪回,synchronized 是不可以響應中斷的,為處理鎖的不可用性提供了更高的靈活性

3. ReentrantLock 是 API 級别的,synchronized 是 JVM 級别的

4. ReentrantLock 可以實作公平鎖

5. ReentrantLock 通過 Condition 可以綁定多個條件

6. 底層實作不一樣, synchronized 是同步阻塞,使用的是悲觀并發政策,lock 是同步非阻塞,采用的是樂觀并發政策

7. Lock 是一個接口,而 synchronized 是 Java 中的關鍵字,synchronized 是内置的語言實作。

8. synchronized 在發生異常時,會自動釋放線程占有的鎖,是以不會導緻死鎖現象發生;而 Lock 在發生異常時,如果沒有主動通過 unLock()去釋放鎖,則很可能造成死鎖現象,是以使用 Lock 時需要在 finally 塊中釋放鎖。

9. Lock 可以讓等待鎖的線程響應中斷,而 synchronized 卻不行,使用 synchronized 時,等待的線程會一直等待下去,不能夠響應中斷。

10. 通過 Lock 可以知道有沒有成功擷取鎖,而 synchronized 卻無法辦到。

11. Lock 可以提高多個線程進行讀操作的效率,既就是實作讀寫鎖等。

ConcurrentHashMap 并發

1. 減小鎖粒度

減小鎖粒度是指縮小鎖定對象的範圍,進而減小鎖沖突的可能性,進而提高系統的并發能力。減小鎖粒度是一種削弱多線程鎖競争的有效手段,這種技術典型的應用是 ConcurrentHashMap(高性能的 HashMap)類的實作。對于 HashMap 而言,最重要的兩個方法是 get 與 set 方法,如果我們對整個 HashMap 加鎖,可以得到線程安全的對象,但是加鎖粒度太大。Segment 的大小也被稱為 ConcurrentHashMap 的并發度。

2. ConcurrentHashMap 分段鎖

ConcurrentHashMap,它内部細分了若幹個小的 HashMap,稱之為段(Segment)。預設情況下一個 ConcurrentHashMap 被進一步細分為 16 個段,既就是鎖的并發度。

如果需要在 ConcurrentHashMap 中添加一個新的表項,并不是将整個 HashMap 加鎖,而是首先根據 hashcode 得到該表項應該存放在哪個段中,然後對該段加鎖,并完成 put 操作。在多線程環境中,如果多個線程同時進行 put 操作,隻要被加入的表項不存放在同一個段中,則線程間可以做到真正的并行。

ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成。Segment 是一種可重入鎖 ReentrantLock,在 ConcurrentHashMap 裡扮演鎖的角色,HashEntry 則用于存儲鍵值對資料。一個 ConcurrentHashMap 裡包含一個 Segment 數組,Segment 的結構和 HashMap類似,是一種數組和連結清單結構, 一個 Segment 裡包含一個 HashEntry 數組,每個 HashEntry 是一個連結清單結構的元素, 每個 Segment 守護一個 HashEntry 數組裡的元素,當對 HashEntry 數組的資料進行修改時,必須首先獲得它對應的 Segment 鎖。

Java系列9-線程知識詳解計數器,程式,線程排程

Java 中用到的線程排程

1. 搶占式排程:

搶占式排程指的是每條線程執行的時間、線程的切換都由系統控制,系統控制指的是在系統某種運作機制下,可能每條線程都分同樣的執行時間片,也可能是某些線程執行的時間片較長,甚至某些線程得不到執行的時間片。在這種機制下,一個線程的堵塞不會導緻整個程序堵塞。

2. 協同式排程:

協同式排程指某一線程執行完後主動通知系統切換到另一線程上執行,這種模式就像接力賽一樣,一個人跑完自己的路程就把接力棒交接給下一個人,下個人繼續往下跑。線程的執行時間由線程本身控制,線程切換可以預知,不存在多線程同步問題,但它有一個緻命弱點:如果一個線程編寫有問題,運作到一半就一直堵塞,那麼可能導緻整個系統崩潰。

Java系列9-線程知識詳解計數器,程式,線程排程

3. JVM 的線程排程實作(搶占式排程)

java 使用的線程調使用搶占式排程,Java 中線程會按優先級配置設定 CPU 時間片運作,且優先級越高越優先執行,但優先級高并不代表能獨自占用執行時間片,可能是優先級高得到越多的執行時間片,反之,優先級低的分到的執行時間少但不會配置設定不到執行時間。

4. 線程讓出 cpu 的情況:

1. 目前運作線程主動放棄 CPU,JVM 暫時放棄 CPU 操作(基于時間片輪轉排程的 JVM 作業系統不會讓線程永久放棄 CPU,或者說放棄本次時間片的執行權),例如調用 yield()方法。

2. 目前運作線程因為某些原因進入阻塞狀态,例如阻塞在 I/O 上。

3. 目前運作線程結束,即運作完 run()方法裡面的任務。

程序排程算法

1. 優先排程算法

1. 先來先服務排程算法(FCFS)

當在作業排程中采用該算法時,每次排程都是從後備作業隊列中選擇一個或多個最先進入該隊列的作業,将它們調入記憶體,為它們配置設定資源、建立程序,然後放入就緒隊列。在程序排程中采用 FCFS 算法時,則每次排程是從就緒隊列中選擇一個最先進入該隊列的程序,為之配置設定處理機,使之投入運作。該程序一直運作到完成或發生某事件而阻塞後才放棄處理機,特點是:算法比較簡單,可以實作基本上的公平。

2. 短作業(程序)優先排程算法

短作業優先(SJF)的排程算法是從後備隊列中選擇一個或若幹個估計運作時間最短的作業,将它們調入記憶體運作。而短程序優先(SPF)排程算法則是從就緒隊列中選出一個估計運作時間最短的程序,将處理機配置設定給它,使它立即執行并一直執行到完成,或發生某事件而被阻塞放棄處理機時再重新排程。該算法未照顧緊迫型作業。

2. 高優先權優先排程算法

為了照顧緊迫型作業,使之在進入系統後便獲得優先處理,引入了最高優先權優先(FPF)排程算法。當把該算法用于作業排程時,系統将從後備隊列中選擇若幹個優先權最高的作業裝入記憶體。當用于程序排程時,該算法是把處理機配置設定給就緒隊列中優先權最高的程序。

1. 非搶占式優先權算法

在這種方式下,系統一旦把處理機配置設定給就緒隊列中優先權最高的程序後,該程序便一直執行下去,直至完成;或因發生某事件使該程序放棄處理機時。這種排程算法主要用于批處理系統中;也可用于某些對實時性要求不嚴的實時系統中。

2. 搶占式優先權排程算法

在這種方式下,系統同樣是把處理機配置設定給優先權最高的程序,使之執行。但在其執行期間,隻要又出現了另一個其優先權更高的程序,程序排程程式就立即停止目前程序(原優先權最高的程序)的執行,重新将處理機配置設定給新到的優先權最高的程序。顯然,這種搶占式的優先權排程算法能更好地滿足緊迫作業的要求,故而常用于要求比較嚴格的實時系統中,以及對性能要求較高的批處理和分時系統中。

2.高響應比優先排程算法

在批處理系統中,短作業優先算法是一種比較好的算法,其主要的不足之處是長作業的運作得不到保證。如果我們能為每個作業引入前面所述的動态優先權,并使作業的優先級随着等待時間的增加而以速率 a 提高,則長作業在等待一定的時間後,必然有機會配置設定到處理機。該優先權的變化規律可描述為:

Java系列9-線程知識詳解計數器,程式,線程排程

(1) 如果作業的等待時間相同,則要求服務的時間愈短,其優先權愈高,因而該算法有利于短作業。

(2) 當要求服務的時間相同時,作業的優先權決定于其等待時間,等待時間愈長,其優先權愈高,因而它實作的是先來先服務。

(3) 對于長作業,作業的優先級可以随等待時間的增加而提高,當其等待時間足夠長時,其優先級便可升到很高,進而也可獲得處理機。簡言之,該算法既照顧了短作業,又考慮了作業到達的先後次序,不會使長作業長期得不到服務。是以,該算法實作了一種較好的折衷。當然,在利用該算法時,每要進行排程之前,都須先做響應比的計算,這會增加系統開銷。

3. 基于時間片的輪轉排程算法

1. 時間片輪轉法

在早期的時間片輪轉法中,系統将所有的就緒程序按先來先服務的原則排成一個隊列,每次排程時,把 CPU 配置設定給隊首程序,并令其執行一個時間片。時間片的大小從幾 ms 到幾百 ms。當執行的時間片用完時,由一個計時器發出時鐘中斷請求,排程程式便據此信号來停止該程序的執行,并将它送往就緒隊列的末尾;然後,再把處理機配置設定給就緒隊列中新的隊首程序,同時也讓它執行一個時間片。這樣就可以保證就緒隊列中的所有程序在一給定的時間内均能獲得一時間片的處理機執行時間。

2. 多級回報隊列排程算法

(1) 應設定多個就緒隊列,并為各個隊列賦予不同的優先級。第一個隊列的優先級最高,第二個隊列次之,其餘各隊列的優先權逐個降低。該算法賦予各個隊列中程序執行時間片的大小也各不相同,在優先權愈高的隊列中,為每個程序所規定的執行時間片就愈小。例如,第二個隊列的時間片要比第一個隊列的時間片長一倍,……,第 i+1 個隊列的時間片要比第 i 個隊列的時間片長一倍。

(2) 當一個新程序進入記憶體後,首先将它放入第一隊列的末尾,按 FCFS 原則排隊等待排程。當輪到該程序執行時,如它能在該時間片内完成,便可準備撤離系統;如果它在一個時間片結束時尚未完成,排程程式便将該程序轉入第二隊列的末尾,再同樣地按 FCFS 原則等待排程執行;如果它在第二隊列中運作一個時間片後仍未完成,再依次将它放入第三隊列,……,如此下去,當一個長作業(程序)從第一隊列依次降到第 n 隊列後,在第 n 隊列便采取按時間片輪轉的方式運作。

(3) 僅當第一隊列空閑時,排程程式才排程第二隊列中的程序運作;僅當第 1~(i-1)隊列均空時,才會排程第 i 隊列中的程序運作。如果處理機正在第 i 隊列中為某程序服務時,又有新程序進入優先權較高的隊列(第 1~(i-1)中的任何一個隊列),則此時新程序将搶占正在運作程序的處理機,即由排程程式把正在運作的程序放回到第 i 隊列的末尾,把處理機配置設定給新到的高優先權程序。

在多級回報隊列排程算法中,如果規定第一個隊列的時間片略大于多數人機互動所需之處理時間時,便能夠較好的滿足各種類型使用者的需要。

什麼是 CAS(比較并交換-樂觀鎖機制-鎖自旋)

1. 概念及特性

CAS(Compare And Swap/Set)比較并交換,CAS 算法的過程是這樣:它包含 3 個參數CAS(V,E,N)。V 表示要更新的變量(記憶體值),E 表示預期值(舊的),N 表示新值。當且僅當 V 值等于 E 值時,才會将 V 的值設為 N,如果 V 值和 E 值不同,則說明已經有其他線程做了更新,則目前線程什麼都不做。最後,CAS 傳回目前 V 的真實值。

CAS 操作是抱着樂觀的态度進行的(樂觀鎖),它總是認為自己可以成功完成操作。當多個線程同時使用 CAS 操作一個變量時,隻有一個會勝出,并成功更新,其餘均會失敗。失敗的線程不會被挂起,僅是被告知失敗,并且允許再次嘗試,當然也允許失敗的線程放棄操作。基于這樣的原理,CAS 操作即使沒有鎖,也可以發現其他線程對目前線程的幹擾,并進行恰當的處理。

2. 原子包 java.util.concurrent.atomic(鎖自旋)

JDK1.5 的原子包:java.util.concurrent.atomic 這個包裡面提供了一組原子類。其基本的特性就是在多線程環境下,當有多個線程同時執行這些類的執行個體包含的方法時,具有排他性,即當某個線程進入方法,執行其中的指令時,不會被其他線程打斷,而别的線程就像自旋鎖一樣,一直等到該方法執行完成,才由 JVM 從等待隊列中選擇一個另一個線程進入,這隻是一種邏輯上的了解。

相對于對于 synchronized 這種阻塞算法,CAS 是非阻塞算法的一種常見實作。由于一般 CPU 切換時間比 CPU 指令集操作更加長, 是以 J.U.C 在性能上有了很大的提升。如下代碼:

public class AtomicInteger extends Number implements java.io.Serializable {

private volatile int value;

public final int get() {

return value;

}

public final int getAndIncrement() {

for (;;) { //CAS 自旋,一直嘗試,直達成功

int current = get();

int next = current + 1;

if (compareAndSet(current, next))

return current;

}

}

public final boolean compareAndSet(int expect, int update) {

return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

}

}

getAndIncrement 采用了 CAS 操作,每次從記憶體中讀取資料然後将此資料和+1 後的結果進行CAS 操作,如果成功就傳回結果,否則重試直到成功為止。而 compareAndSet 利用 JNI 來完成CPU 指令的操作。

Java系列9-線程知識詳解計數器,程式,線程排程

3. ABA 問題

CAS 會導緻“ABA 問題”。CAS 算法實作一個重要前提需要取出記憶體中某時刻的資料,而在下時刻比較并替換,那麼在這個時間差類會導緻資料的變化。

比如說一個線程 one 從記憶體位置 V 中取出 A,這時候另一個線程 two 也從記憶體中取出 A,并且two 進行了一些操作變成了 B,然後 two 又将 V 位置的資料變成 A,這時候線程 one 進行 CAS 操作發現記憶體中仍然是 A,然後 one 操作成功。盡管線程 one 的 CAS 操作成功,但是不代表這個過程就是沒有問題的。

部分樂觀鎖的實作是通過版本号(version)的方式來解決 ABA 問題,樂觀鎖每次在執行資料的修改操作時,都會帶上一個版本号,一旦版本号和資料的版本号一緻就可以執行修改操作并對版本号執行+1 操作,否則就執行失敗。因為每次操作的版本号都會随之增加,是以不會出現 ABA 問題,因為版本号隻會增加不會減少。

什麼是 AQS(抽象的隊列同步器)

AbstractQueuedSynchronizer 類如其名,抽象的隊列式的同步器,AQS 定義了一套多線程通路共享資源的同步器架構,許多同步類實作都依賴于它,如常用的ReentrantLock/Semaphore/CountDownLatch。

Java系列9-線程知識詳解計數器,程式,線程排程

它維護了一個 volatile int state(代表共享資源)和一個 FIFO 線程等待隊列(多線程争用資源被阻塞時會進入此隊列)。這裡 volatile 是核心關鍵詞,具體 volatile 的語義,在此不述。state 的通路方式有三種:

getState()

setState()

compareAndSetState()

AQS 定義兩種資源共享方式

Exclusive 獨占資源-ReentrantLock

Exclusive(獨占,隻有一個線程能執行,如 ReentrantLock)

Share 共享資源-Semaphore/CountDownLatch

Share(共享,多個線程可同時執行,如 Semaphore/CountDownLatch)。

AQS 隻是一個架構,具體資源的擷取/釋放方式交由自定義同步器去實作,AQS 這裡隻定義了一個接口,具體資源的擷取交由自定義同步器去實作了(通過 state 的 get/set/CAS)之是以沒有定義成abstract , 是 因 為 獨 占 模 式 下 隻 用 實 現 tryAcquire-tryRelease , 而 共 享 模 式 下 隻 用 實 現tryAcquireShared-tryReleaseShared。如果都定義成 abstract,那麼每個模式也要去實作另一模式下的接口。不同的自定義同步器争用共享資源的方式也不同。自定義同步器在實作時隻需要實作共享資源 state 的擷取與釋放方式即可,至于具體線程等待隊列的維護(如擷取資源失敗入隊/喚醒出隊等),AQS 已經在頂層實作好了。自定義同步器實作時主要實作以下幾種方法:

1. isHeldExclusively():該線程是否正在獨占資源。隻有用到 condition 才需要去實作它。

2. tryAcquire(int):獨占方式。嘗試擷取資源,成功則傳回 true,失敗則傳回 false。

3. tryRelease(int):獨占方式。嘗試釋放資源,成功則傳回 true,失敗則傳回 false。

4. tryAcquireShared(int):共享方式。嘗試擷取資源。負數表示失敗;0 表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。

5. tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放後允許喚醒後續等待結點傳回true,否則傳回 false。

同步器的實作是 ABS 核心(state 資源狀态計數)

同步器的實作是 ABS 核心,以 ReentrantLock 為例,state 初始化為 0,表示未鎖定狀态。A 線程lock()時,會調用 tryAcquire()獨占該鎖并将 state+1。此後,其他線程再 tryAcquire()時就會失敗,直到 A 線程 unlock()到 state=0(即釋放鎖)為止,其它線程才有機會擷取該鎖。當然,釋放鎖之前,A 線程自己是可以重複擷取此鎖的(state 會累加),這就是可重入的概念。但要注意,擷取多少次就要釋放多麼次,這樣才能保證 state 是能回到零态的。

以 CountDownLatch 以例,任務分為 N 個子線程去執行,state 也初始化為 N(注意 N 要與線程個數一緻)。這 N 個子線程是并行執行的,每個子線程執行完後 countDown()一次,state會 CAS 減 1。等到所有子線程都執行完後(即 state=0),會 unpark()主調用線程,然後主調用線程就會從 await()函數傳回,繼續後餘動作。

ReentrantReadWriteLock 實作獨占和共享兩種方式

一般來說,自定義同步器要麼是獨占方法,要麼是共享方式,他們也隻需實作 tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared 中的一種即可。但 AQS 也支援自定義同步器同時實作獨占和共享兩種方式,如 ReentrantReadWriteLock。

繼續閱讀