Java並發包——線程同步和鎖


Java並發包——線程同步和鎖

摘要:本文主要學習了Java並發包里有關線程同步的類和鎖的一些相關概念。

部分內容來自以下博客:

https://www.cnblogs.com/dolphin0520/p/3923167.html

https://blog.csdn.net/tyyj90/article/details/78236053

線程同步方式

對於線程安全我們前面使用了synchronized關鍵字,對於線程的協作我們使用Object.wait()和Object.notify()。在JDK1.5中java為我們提供了Lock來實現與它們相同的功能,並且性能優於它們,在JDK1.6時,JDK對synchronized做了優化,在性能上兩種方式差距不大了。

synchronized的缺陷

synchronized修飾的代碼塊,當一個線程獲取了對應的鎖,並執行該代碼塊時,其他線程便只能一直等待,等待獲取鎖的線程釋放鎖,如果沒有釋放則需要無限的等待下去。

獲取鎖的線程釋放鎖只會有兩種情況:

1)獲取鎖的線程執行完了該代碼塊,然后線程釋放對鎖的占有。

2)線程執行發生異常,此時JVM會讓線程自動釋放鎖。

總結一下,也就是說Lock提供了比synchronized更多的功能。但是要注意以下幾點:

1)Lock不是Java語言內置的,synchronized是Java語言的關鍵字,因此是內置特性。Lock是一個類,通過這個類可以實現同步訪問。

2)synchronized不需要手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完之后,系統會自動讓線程釋放對鎖的占用。而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。

Lock

Lock接口位於java.util.concurrent.locks包中。

 1 public interface Lock {
 2     // 用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。
 3     void lock();
 4 
 5     // 用來獲取鎖。允許在等待時由其它線程調用interrupt方法來中斷等待而直接返回,這時不用獲取鎖,而會拋出一個InterruptedException。
 6     void lockInterruptibly() throws InterruptedException;
 7 
 8     // 用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false。
 9     boolean tryLock();
10 
11     // 用來嘗試獲取鎖,如果拿到鎖或者在等待期間內拿到了鎖,則返回true。如果在某段時間之內獲取失敗,就返回false。
12     boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
13 
14     // 釋放鎖。
15     void unlock();
16 
17     // 獲取Condition對象。
18     Condition newCondition();
19 }

lock方法

首先lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。

由於在前面講到如果采用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。

通常使用Lock來進行同步的話,是以下面這種形式去使用的:

1 Lock lock = ... ;
2 lock.lock();
3 try {
4     // 處理任務
5 } catch(Exception e) {
6 
7 } finally {
8     lock.unlock();// 釋放鎖
9 }

tryLock方法

tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。

一般情況下通過tryLock來獲取鎖時是這樣使用的:

 1 Lock lock = ... ;
 2 if (lock.tryLock()) {
 3     try {
 4         // 處理任務
 5     } catch (Exception e) {
 6 
 7     } finally {
 8         lock.unlock();// 釋放鎖
 9     }
10 } else {
11     // 獲取失敗處理其他事情
12 }

lockInterruptibly方法

lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。也就使說,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那么對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。

由於lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly()的方法外聲明拋出InterruptedException。

一般的使用形式如下:

1 public void method() throws InterruptedException {
2     Lock lock = ... ;
3     lock.lockInterruptibly();
4     try {
5         // 處理任務
6     } finally {
7         lock.unlock();
8     }
9 }

注意,當一個線程獲取了鎖之后,是不會被interrupt()方法中斷的。因為本身在前面的文章中講過單獨調用interrupt()方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。

因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。

而用synchronized修飾的話,當一個線程處於等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。

ReentrantLock

ReentrantLock類實現了Lock接口,並且ReentrantLock提供了更多的方法。

 1 public class Demo {
 2     public static void main(String[] args) {
 3         DemoThread dt = new DemoThread();
 4         Thread t1 = new Thread(dt, "窗口1");
 5         Thread t2 = new Thread(dt, "窗口2");
 6         t1.start();
 7         t2.start();
 8     }
 9 }
10 
11 class DemoThread implements Runnable {
12     private int ticket = 3;
13     Lock lock = new ReentrantLock();
14 
15     @Override
16     public void run() {
17         while (ticket > 0) {
18             try {
19                 Thread.sleep(1);
20             } catch (InterruptedException e) {
21                 e.printStackTrace();
22             }
23             
24             lock.lock();
25             try {
26                 if (ticket > 0) {
27                     System.out.println(Thread.currentThread().getName() + " 進入賣票環節 ");
28                     System.out.println(Thread.currentThread().getName() + " 售賣的車票編號為: " + ticket--);
29                 }
30             } catch (Exception e) {
31                 e.printStackTrace();
32             } finally {
33                 lock.unlock();
34             }
35         }
36     }
37 }

注意在聲明Lock的時候,要注意不要聲明為局部變量。

ReadWriteLock

ReadWriteLock也是一個接口,用來定義讀寫鎖。

1 public interface ReadWriteLock {
2     Lock readLock();
3 
4     Lock writeLock();
5 }

一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操作分開,分成兩個鎖來分配給線程,從而使得多個線程可以同時進行讀操作。

ReentrantReadWriteLock

ReentrantReadWriteLock實現了ReadWriteLock接口,支持多個線程同時進行讀操作。

 1 public class Demo {
 2     public static void main(String[] args) {
 3         DemoThread dt = new DemoThread();
 4         new Thread(() -> dt.showTicket(), "窗口1").start();
 5         new Thread(() -> dt.showTicket(), "窗口2").start();
 6         new Thread(() -> dt.showTicket(), "窗口3").start();
 7         new Thread(() -> dt.saleTicket(), "窗口4").start();
 8     }
 9 }
10 
11 class DemoThread {
12     private int ticket = 3;
13     ReadWriteLock lock = new ReentrantReadWriteLock();
14 
15     public void showTicket() {
16         while (ticket > 0) {
17             try {
18                 Thread.sleep(1);
19             } catch (InterruptedException e) {
20                 e.printStackTrace();
21             }
22             lock.readLock().lock();
23             try {
24                 if (ticket > 0) {
25                     System.out.println(Thread.currentThread().getName() + " 進入預售環節");
26                     System.out.println(Thread.currentThread().getName() + " 預售的車票編號為: " + ticket);
27                 }
28             } catch (Exception e) {
29                 e.printStackTrace();
30             } finally {
31                 lock.readLock().unlock();
32             }
33         }
34         System.out.println(Thread.currentThread().getName() + " 進入結束環節");
35     }
36 
37     public void saleTicket() {
38         while (ticket > 0) {
39             try {
40                 Thread.sleep(1);
41             } catch (InterruptedException e) {
42                 e.printStackTrace();
43             }
44             lock.writeLock().lock();
45             try {
46                 if (ticket > 0) {
47                     System.out.println(Thread.currentThread().getName() + " 進入售票環節");
48                     System.out.println(Thread.currentThread().getName() + " 售賣的車票編號為: " + ticket--);
49                 }
50             } catch (Exception e) {
51                 e.printStackTrace();
52             } finally {
53                 lock.writeLock().unlock();
54             }
55         }
56         System.out.println(Thread.currentThread().getName() + " 進入結束環節");
57     }
58 }

運行結果如下:

 1 窗口2 進入預售環節
 2 窗口1 進入預售環節
 3 窗口1 預售的車票編號為: 3
 4 窗口2 預售的車票編號為: 3
 5 窗口3 進入預售環節
 6 窗口3 預售的車票編號為: 3
 7 窗口4 進入售票環節
 8 窗口4 售賣的車票編號為: 3
 9 窗口4 進入售票環節
10 窗口4 售賣的車票編號為: 2
11 窗口2 進入預售環節
12 窗口3 進入預售環節
13 窗口1 進入預售環節
14 窗口1 預售的車票編號為: 1
15 窗口2 預售的車票編號為: 1
16 窗口3 預售的車票編號為: 1
17 窗口4 進入售票環節
18 窗口4 售賣的車票編號為: 1
19 窗口4 進入結束環節
20 窗口3 進入結束環節
21 窗口2 進入結束環節
22 窗口1 進入結束環節

從運行的結果來看,最多有三個線程在同時讀,提高了讀操作的效率。

如果有一個線程已經占用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。

如果有一個線程已經占用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。

關於synchronized和Lock的比較

1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現。

2)synchronized在發生異常時,會自動釋放線程占有的鎖,因此不會導致死鎖現象發生。而Lock在發生異常時,如果沒有主動釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖。

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

4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。

5)Lock可以提高多個線程進行讀操作的效率。

6)synchronized的底層是一個基於CAS操作的等待隊列,synchronized還實現了自旋鎖,並針對不同的系統和硬件體系進行了優化,而Lock則完全依靠系統阻塞掛起等待線程。

7)在資源競爭不是很激烈的情況下,synchronized的性能要優於ReetrantLock,但是在資源競爭很激烈的情況下,synchronized的性能會下降幾十倍,但是ReetrantLock的性能能維持常態

鎖的分類

在讀很多並發文章中,會提及各種各樣鎖如公平鎖,樂觀鎖等等,這篇文章介紹各種鎖的分類。介紹的內容如下:

1 可重入鎖
2 獨享鎖/共享鎖
3 互斥鎖/讀寫鎖
4 公平鎖/非公平鎖
5 樂觀鎖/悲觀鎖
6 分段鎖
7 偏向鎖/輕量級鎖/重量級鎖
8 自旋鎖

上面是很多鎖的名詞,這些分類並不是全是指鎖的狀態,有的指鎖的特性,有的指鎖的設計,下面總結的內容是對每個鎖的名詞進行一定的解釋。

可重入鎖

可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。

對於synchronized和ReentrantLock而言,都是可重入鎖。

可重入鎖的一個好處是可一定程度避免死鎖,如果不是可重入鎖的話,可能造成死鎖。

獨享鎖/共享鎖

獨享鎖是指該鎖一次只能被一個線程所持有。共享鎖是指該鎖可被多個線程所持有。

對於synchronized和ReentrantLock而言,都是獨享鎖。

但是對於ReadWriteLock而言,其讀鎖是共享鎖,其寫鎖是獨享鎖。讀鎖的共享鎖可保證並發讀是非常高效的,讀寫,寫讀,寫寫的過程是互斥的。

獨享鎖與共享鎖也是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享。

互斥鎖/讀寫鎖

上面講的獨享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現。

互斥鎖在Java中的具體實現就是ReentrantLock。讀寫鎖在Java中的具體實現就是ReadWriteLock。

公平鎖/非公平鎖

公平鎖是指多個線程按照申請鎖的順序來獲取鎖,非公平鎖是指多個線程獲取鎖的順序並不是按照申請鎖的順序。

對於synchronized而言,是一種非公平鎖。

對於ReentrantLock而言,通過構造函數指定該鎖是否是公平鎖,默認是非公平鎖。非公平鎖的優點在於吞吐量比公平鎖大。

樂觀鎖/悲觀鎖

樂觀鎖與悲觀鎖不是指具體的什么類型的鎖,而是指看待並發同步的角度。

悲觀鎖認為對於同一個數據的並發操作,一定是會發生修改的,哪怕沒有修改,也會認為修改。因此對於同一個數據的並發操作,悲觀鎖采取加鎖的形式。悲觀的認為,不加鎖的並發操作一定會出問題

樂觀鎖則認為對於同一個數據的並發操作,是不會發生修改的。在更新數據的時候,會采用嘗試更新,不斷重新的方式更新數據。樂觀的認為,不加鎖的並發操作是沒有事情的

悲觀鎖在Java中的使用,就是利用各種鎖。

樂觀鎖在Java中的使用,是無鎖編程,常常采用的是CAS算法,典型的例子就是原子類,通過CAS自旋實現原子操作的更新。

分段鎖

分段鎖其實是一種鎖的設計,並不是具體的一種鎖,對於ConcurrentHashMap而言,其並發的實現就是通過分段鎖的形式來實現高效的並發操作。

分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。

偏向鎖/輕量級鎖/重量級鎖

這三種鎖是指鎖的狀態,並且是針對Synchronized。在JDK5通過引入鎖升級的機制來實現高效Synchronized。

這三種鎖的狀態是通過對象監視器在對象頭中的字段來表明的。

偏向鎖是指一段同步代碼一直被一個線程所訪問,那么該線程會自動獲取鎖。降低獲取鎖的代價。

輕量級鎖是指當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。

重量級鎖是指當鎖為輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹為重量級鎖。重量級鎖會讓其他申請的線程進入阻塞,性能降低。

自旋鎖

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

了解AQS

什么是AQS

AQS是英文單詞AbstractQueuedSynchronizer的縮寫,翻譯過來就是抽象的隊列式的同步器,AQS定義了一套多線程訪問共享資源的同步器框架,許多同步類實現都依賴於它,如常用的ReentrantLock、Semaphore、CountDownLatch等等。

實現方式

AQS的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態。

原理

AQS維護了一個state用來代表資源共享狀態 private volatile int state; ,AQS提供了三種操作state的方法: int getState(); 、 void setState(int newState); 、 boolean compareAndSetState(int expect, int update); 。

AQS通過內置的FIFO同步隊列 static final class Node 來完成資源獲取線程的排隊工作,如果當前線程獲取同步狀態失敗(鎖)時,AQS則會將當前線程以及等待狀態等信息構造成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,則會把節點中的線程喚醒,使其再次嘗試獲取同步狀態。

資源共享方式

AQS定義兩種資源共享方式:Exclusive(獨占,只有一個線程能執行,如ReentrantLock)和Share(共享,多個線程可同時執行,如Semaphore/CountDownLatch)。

使用分析

不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實現共享資源state的獲取與釋放方式即可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。自定義同步器實現時主要實現以下幾種方法:

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

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

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

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

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

以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()函數返回,繼續后余動作。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM