Java併發編程:Semaphore和Lock區別

Java提供了一個類Semaphore來實現信號量,概念上講,一個信號量相當於持有一些許可(permits),線程可以調用Semaphore對象的acquire()方法獲取一個許可,調用release()來歸還一個許可

1 構造方法: 
Semaphore有兩個構造方法 Semaphore(int)Semaphore(int,boolean),參數中的int表示該信號量擁有的許可數量,boolean表示獲取許可的時候是否是公平的,如果是公平的那麼,當有多個線程要獲取許可時,會按照線程來的先後順序分配許可,否則,線程獲得許可的順序是不定的。這裏在jdk中講到 “一般而言,非公平時候的吞吐量要高於公平鎖”,這是爲什麼呢?附上鍊接中的一段話:

非公平鎖性能高於公平鎖性能的原因:在恢復一個被掛起的線程與該線程真正運行之間存在着嚴重的延遲。假設線程A持有一個鎖,並且線程B請求這個鎖。由於鎖被A持有,因此B將被掛起。當A釋放鎖時,B將被喚醒,因此B會再次嘗試獲取這個鎖。與此同時,如果線程C也請求這個鎖,那麼C很可能會在B被完全喚醒之前獲得、使用以及釋放這個鎖。這樣就是一種雙贏的局面:B獲得鎖的時刻並沒有推遲,C更早的獲得了鎖,並且吞吐量也提高了。當持有鎖的時間相對較長或者請求鎖的平均時間間隔較長,應該使用公平鎖。在這些情況下,插隊帶來的吞吐量提升(當鎖處於可用狀態時,線程卻還處於被喚醒的過程中)可能不會出現。

2 獲取許可 
可以使用acquire()、acquire(int)、tryAcquire()等去獲取許可,其中int參數表示一次性要獲取幾個許可,默認爲1個,acquire方法在沒有許可的情況下,要獲取許可的線程會阻塞,而tryAcquire()方法在沒有許可的情況下會立即返回 false,要獲取許可的線程不會阻塞,這與Lock類的lock()與tryLock()類似

3 釋放許可 
線程可調用 release()、release(int)來釋放(歸還)許可,注意一個線程調用release()之前並不要求一定要調用了acquire (There is no requirement that a thread that releases a permit must have acquired that permit by calling {@link #acquire})

4 使用場景 
我們一般使用信號量來限制訪問資源的線程數量,比如有一個食堂,最多允許5個人同時喫飯,則如下:

 
  1.  
    class EatThread extends Thread{
  2.  
    private Semaphore semaphore;
  3.  
    public EatThread(Semaphore semaphore){
  4.  
    this.semaphore=semaphore;
  5.  
    }
  6.  
  7.  
    public void run(){
  8.  
    try {
  9.  
    semaphore.acquire();//獲取一個許可,當然也可以調用acquire(int),這樣一個線程就能拿到多個許可
  10.  
    long eatTime=(long) (Math.random()*10);
  11.  
    System.out.println(Thread.currentThread().getId()+" 正在喫飯");
  12.  
    TimeUnit.SECONDS.sleep(eatTime);
  13.  
    System.out.println(Thread.currentThread().getId()+" 已經喫完");
  14.  
    semaphore.release();//歸還許可
  15.  
    } catch (InterruptedException e) {
  16.  
    e.printStackTrace();
  17.  
    }
  18.  
    }
  19.  
    }
  20.  
    public class SemaphoreTest {
  21.  
    public static void main(String[] args) {
  22.  
    Semaphore semaphore=new Semaphore(5);//總共有5個許可
  23.  
    for(int i=0;i<7;i++){//定義七個喫的線程
  24.  
    new EatThread(semaphore).start();
  25.  
    }
  26.  
    }
  27.  
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

結果如下:

 
  1.  
    9 正在喫飯
  2.  
    15 正在喫飯
  3.  
    13 正在喫飯
  4.  
    13 已經喫完
  5.  
    11 正在喫飯
  6.  
    10 正在喫飯
  7.  
    10 已經喫完
  8.  
    12 正在喫飯
  9.  
    14 正在喫飯
  10.  
    11 已經喫完
  11.  
    15 已經喫完
  12.  
    14 已經喫完
  13.  
    9 已經喫完
  14.  
    12 已經喫完
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

當我們在構造Semaphore對象時,如果設置的許可數量爲1,這時便會達到一個互斥排他鎖的效果,只有一個許可,有一個線程獲取了這個許可,那麼其他線程只有等待這個線程歸還了許可才能獲取到許可,當將Semaphore用作互斥排他鎖的作用時,要注意:

A semaphore initialized to one, and which is used such that it only has at most one permit available, can serve as a mutual exclusion lock. This is more commonly known as a binary semaphore, because it only has two states: one permit available, or zero permits available. When used in this way, the binary semaphore has the property (unlike many Lock implementations), that the “lock” can be released by a thread other than the owner (as semaphores have no notion of ownership). This can be useful in some specialized contexts, such as deadlock recovery.

文檔中提到,Semaphore與jdk中的Lock的區別是 
1. 使用Lock.unlock()之前,該線程必須事先持有這個鎖(通過Lock.lock()獲取),如下:

 
  1.  
    public class LockTest {
  2.  
    public static void main(String[] args) {
  3.  
    Lock lock=new ReentrantLock();
  4.  
    lock.unlock();
  5.  
    }
  6.  
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

則會拋出異常,因爲該線程事先並沒有獲取lock對象的鎖:

 
  1.  
    Exception in thread "main" java.lang.IllegalMonitorStateException
  2.  
    at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:155)
  3.  
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1260)
  4.  
    at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:460)
  5.  
    at LockTest.main(LockTest.java:12)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

對於Semaphore來講,如下:

 
  1.  
    public class SemaphoreTest {
  2.  
    public static void main(String[] args) {
  3.  
    Semaphore semaphore=new Semaphore(1);//總共有1個許可
  4.  
    System.out.println("可用的許可數目爲:"+semaphore.availablePermits());
  5.  
    semaphore.release();
  6.  
    System.out.println("可用的許可數目爲:"+semaphore.availablePermits());
  7.  
    }
  8.  
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

結果如下:

 
  1.  
    可用的許可數目爲:1
  2.  
    可用的許可數目爲:2
  • 1
  • 2
  • 1
  • 2

i. 並沒有拋出異常,也就是線程在調用release()之前並不要求先調用acquire() 
ii. 我們看到可用的許可數目增加了一個,但我們的初衷是保證只有一個許可來達到互斥排他鎖的目的,所以這裏要注意一下

2 Semaphore(1)可以做到一個deadlock recovery,我們來看下面一個例子

 
  1.  
    class WorkThread2 extends Thread{
  2.  
    private Semaphore semaphore1,semaphore2;
  3.  
    public WorkThread2(Semaphore semaphore1,Semaphore semaphore2){
  4.  
    this.semaphore1=semaphore1;
  5.  
    this.semaphore2=semaphore2;
  6.  
    }
  7.  
    public void releaseSemaphore2(){
  8.  
    System.out.println(Thread.currentThread().getId()+" 釋放Semaphore2");
  9.  
    semaphore2.release();
  10.  
    }
  11.  
    public void run() {
  12.  
    try {
  13.  
    semaphore1.acquire(); //先獲取Semaphore1
  14.  
    System.out.println(Thread.currentThread().getId()+" 獲得Semaphore1");
  15.  
    TimeUnit.SECONDS.sleep(5); //等待5秒讓WorkThread1先獲得Semaphore2
  16.  
    semaphore2.acquire();//獲取Semaphore2
  17.  
    System.out.println(Thread.currentThread().getId()+" 獲得Semaphore2");
  18.  
    } catch (InterruptedException e) {
  19.  
    e.printStackTrace();
  20.  
    }
  21.  
    }
  22.  
    }
  23.  
    class WorkThread1 extends Thread{
  24.  
    private Semaphore semaphore1,semaphore2;
  25.  
    public WorkThread1(Semaphore semaphore1,Semaphore semaphore2){
  26.  
    this.semaphore1=semaphore1;
  27.  
    this.semaphore2=semaphore2;
  28.  
    }
  29.  
    public void run() {
  30.  
    try {
  31.  
    semaphore2.acquire();//先獲取Semaphore2
  32.  
    System.out.println(Thread.currentThread().getId()+" 獲得Semaphore2");
  33.  
    TimeUnit.SECONDS.sleep(5);//等待5秒,讓WorkThread1先獲得Semaphore1
  34.  
    semaphore1.acquire();//獲取Semaphore1
  35.  
    System.out.println(Thread.currentThread().getId()+" 獲得Semaphore1");
  36.  
    } catch (InterruptedException e) {
  37.  
    e.printStackTrace();
  38.  
    }
  39.  
    }
  40.  
    }
  41.  
    public class SemphoreTest {
  42.  
    public static void main(String[] args) throws InterruptedException {
  43.  
    Semaphore semaphore1=new Semaphore(1);
  44.  
    Semaphore semaphore2=new Semaphore(1);
  45.  
    new WorkThread1(semaphore1, semaphore2).start();
  46.  
    new WorkThread2(semaphore1, semaphore2).start();
  47.  
    //此時已經陷入了死鎖,WorkThread1持有semaphore1的許可,請求semaphore2的許可
  48.  
    // WorkThread2持有semaphore2的許可,請求semaphore1的許可
  49.  
    // TimeUnit.SECONDS.sleep(10);
  50.  
    // //在主線程是否semaphore1,semaphore2,解決死鎖
  51.  
    // semaphore1.release();
  52.  
    // semaphore2.release();
  53.  
    }
  54.  
     
  55.  
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

在註釋最後面幾行代碼的情況下,結果爲,陷入了一個死鎖:

 
  1.  
    9 獲得Semaphore2
  2.  
    10 獲得Semaphore1
  • 1
  • 2
  • 1
  • 2

把註釋刪除,即在主線程釋放Semaphore,這樣就能解決死鎖:

 
  1.  
    9 獲得Semaphore2
  2.  
    10 獲得Semaphore1
  3.  
    9 獲得Semaphore1
  4.  
    10 獲得Semaphore2
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

這即符合文檔中說的,通過一個非owner的線程來實現死鎖恢復,但如果你使用的是Lock則做不到,可以把代碼中的兩個信號量換成兩個鎖對象試試。很明顯,前面也驗證過了,要使用Lock.unlock()來釋放鎖,首先你得擁有這個鎖對象,因此非owner線程(事先沒有擁有鎖)是無法去釋放別的線程的鎖對象

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章