1. 程式人生 > >016-並發編程-java.util.concurrent.locks之-Lock及ReentrantLock

016-並發編程-java.util.concurrent.locks之-Lock及ReentrantLock

嘗試 div ndt 插入 show abs 編程 seconds false

一、概述

  重入鎖ReentrantLock,就是支持重進入的鎖 ,它表示該鎖能夠支持一個線程對資源的重復加鎖。支持公平性與非公平性選擇,默認為非公平。

  以下梳理ReentrantLock。作為依賴於AbstractQueuedSynchronizer。 所以要理解ReentrantLock,先要理解AQS。013-AbstractQueuedSynchronizer-用於構建鎖和同步容器的框架、獨占鎖與共享鎖的獲取與釋放

aqs有多神奇,讓ReentrantLock沒有使用更“高級”的機器指令,也不依靠JDK編譯時的特殊處理,就完成了代碼的並發訪問控制。

重進入是指任意線程在獲取到鎖之後能夠再次獲取該鎖而不被鎖所阻塞,需要解決兩個問題:

1) 線程再次獲取鎖(鎖需要識別獲取鎖的線程是否未當前占據鎖的線程)

2)鎖的最終釋放(要求鎖對於獲取進行計數自增,鎖釋放技數自減。技數=0表示鎖已經成功釋放)

1.1、Lock定義以及說明

技術分享圖片
public interface Lock {
     // 獲取鎖,若當前lock被其他線程獲取;則此線程阻塞等待lock被釋放
     // 如果采用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖
    void lock();

    // 獲取鎖,若當前鎖不可用(被其他線程獲取);
    // 則阻塞線程,等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態
void lockInterruptibly() throws InterruptedException; // 來嘗試獲取鎖,如果獲取成功,則返回true; // 如果獲取失敗(即鎖已被其他線程獲取),則返回false // 也就是說,這個方法無論如何都會立即返回 boolean tryLock(); // 在拿不到鎖時會等待一定的時間 // 等待過程中,可以被中斷 // 超過時間,依然獲取不到,則返回false;否則返回true boolean tryLock(long time, TimeUnit unit) throws
InterruptedException; // 釋放鎖 void unlock(); // 返回一個綁定該lock的Condtion對象 // 在Condition#await()之前,鎖會被該線程持有 // Condition#await() 會自動釋放鎖,在wait返回之後,會自動獲取鎖 Condition newCondition(); }
View Code

1.2、ReentrantLock

  可重入鎖。jdk中ReentrantLock是唯一實現了Lock接口的類

  可重入的意思是一個線程擁有鎖之後,可以再次獲取鎖,

基本使用:

  1. 創建鎖對象 Lock lock = new ReentrantLock()
  2. 在希望保證線程同步的代碼之前顯示調用 lock.lock() 嘗試獲取鎖,若被其他線程占用,則阻塞
  3. 執行完之後,一定得手動釋放鎖,否則會造成死鎖 lock.unlock(); 一般來講,把釋放鎖的邏輯,放在需要線程同步的代碼包裝外的finally塊中

使用方式

private Lock lock = new ReentrantLock();
try
{ lock.lock(); // ..... } finally { lock.unlock(); }

  ReentrantLock會保證 do something在同一時間只有一個線程在執行這段代碼,或者說,同一時刻只有一個線程的lock方法會返回。其余線程會被掛起,直到獲取鎖。從這裏可以看出,其實ReentrantLock實現的就是一個獨占鎖的功能:有且只有一個線程獲取到鎖,其余線程全部掛起,直到該擁有鎖的線程釋放鎖,被掛起的線程被喚醒重新開始競爭鎖。

二、代碼探究【主要邏輯即AQS的實現+子類實現的tryAcquire和tryRelease】

2.1、獲取鎖lock【持續等待】

詳細參看:013-AbstractQueuedSynchronizer-用於構建鎖和同步容器的框架、獨占鎖與共享鎖的獲取與釋放 的獨占鎖獲取

這裏只做上文沒有的補充

lock獲取鎖是一種阻塞是獲取。該種方式獲取鎖不可中斷,如果獲取不到則一直休眠等待。

2.1.1、查看ReentrantLock的sync的實現非公平鎖與公平鎖

  可見,是Lock接口的操作都委派到一個Sync類上,該類繼承了AbstractQueuedSynchronizer:

abstract static class Sync extends AbstractQueuedSynchronizer{

  鎖的API是面向使用者的,它定義了與鎖交互的公共行為,而每個鎖需要完成特定的操作也是透過這些行為來完成的,但是實現是依托給同步器來完成,這樣貫穿就容易理解代碼。這是一個抽象類,Sync有兩個子類:

static final class FairSync extends Sync {}
static final class NonfairSync extends Sync {}

  分別對應於非公平鎖、公平鎖,默認情況下為非公平鎖。
    公平鎖:每個線程搶占鎖的順序為先後調用lock方法的順序依次獲取鎖。在並發環境中,每個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果為空,或者當前線程是等待隊列的第一個,就占有鎖,否則就會加入到等待隊列中,以後會按照FIFO的規則從隊列中取到自己
    非公平鎖:每個線程搶占鎖的順序不定,誰運氣好,誰就獲取到鎖,和調用lock方法的先後順序無關。上來就直接嘗試占有鎖,如果嘗試失敗,就再采用類似公平鎖那種方式。其中synchronized是非公平鎖。ReentrantLock可以選擇創建,默認是非公平。

2.1.2、查看acquire

  如果獲取鎖失敗的情況,就是 acquire(1),acquire的是調用AQS來實現的。代碼如下:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

2.1.3、查看tryAcquire

  AbstractQueuedSynchronizer中抽象了絕大多數Lock的功能,而只把tryAcquire方法延遲到子類中實現

  技術分享圖片

2.2、其他方法

2.2.1、lockInterruptibly【中斷】

  這個方法和lock方法的區別就是,lock會一直阻塞下去直到獲取到鎖,而lockInterruptibly則不一樣,它可以響應中斷而停止阻塞返回。ReentrantLock對其的實現是調用的Sync的父類AbstractQueuedSynchronizer#acquireInterruptibly方法:

//ReentrantLock#lockInterruptibly 
public void lockInterruptibly() throws InterruptedException { 
    sync.acquireInterruptibly(1);//因為ReentrantLock是排它鎖,故調用AQS的acquireInterruptibly方法 
} 
//AbstractQueuedSynchronizer#acquireInterruptibly 
public final void acquireInterruptibly(int arg) throws InterruptedException{ 
  if (Thread.interrupted()) //線程是否被中斷,中斷則拋出中斷異常,並停止阻塞 
    throw new InterruptedException; 
  if (!tryAcquire(arg)) //首先還是獲取鎖,具體參照上文 
    doAcquireInterruptibly(arg);//獨占模式下中斷獲取同步狀態 
} 

  通過查看doAcquireInterruptibly的方法實現發現它和acquireQueued大同小異,前者拋出異常,後者返回boolean。【參看:013-AbstractQueuedSynchronizer-用於構建鎖和同步容器的框架、獨占鎖與共享鎖的獲取與釋放】獨占鎖的獲取與釋放

同lock區別,

示例一、Lock,lock()忽視interrupt(), 鎖被主線程占有,子線程拿不到鎖就一直阻塞

    @Test
    public void testLock() throws Exception{
        final Lock lock=new ReentrantLock();
        lock.lock();
        Thread.sleep(1000);
        Thread t1 = new Thread(() -> {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " interrupted.");

        });
        t1.start();
        Thread.sleep(1000);
        t1.interrupt();//lock()忽視interrupt(), 拿不到鎖就 一直阻塞
        Thread.sleep(10000);
    }

無任何輸出

示例二、lockInterruptibly()會響應打擾 並catch到InterruptedException

    @Test
    public void testlockInterruptibly() throws Exception {
        final Lock lock = new ReentrantLock();
        lock.lock();
        Thread.sleep(1000);
        Thread t1 = new Thread(() -> {
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " interrupted.");
            }
        });
        t1.start();
        Thread.sleep(1000);
        t1.interrupt();
        Thread.sleep(10000);
    }

輸出:Thread-0 interrupted.

2.2.2、tryLock【立即返回】

此方法為非阻塞式的獲取鎖,不管有沒有獲取鎖都返回一個boolean值。

public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }

  查看實現,它實際調用了Sync#nonfairTryAcquire非公平鎖獲取鎖的方法,這個方法我們在上文lock()方法非公平鎖獲取鎖的時候有提到,而且還特地強調了該方法不是在NonfairSync實現,而是在Sync中實現很有可能這個方法是一個公共方法,果然在非阻塞獲取鎖的時候調用的是此方法。

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

  當獲取鎖時,只有當該鎖資源沒有被其他線程持有才可以獲取到,並且返回true,同時設置持有count為1;
  當獲取鎖時,當前線程已持有該鎖,那麽鎖可用時,返回true,同時設置持有count加1;
  當獲取鎖時,如果其他線程持有該鎖,無可用鎖資源,直接返回false,這時候線程不用阻塞等待,可以先去做其他事情;
  即使該鎖是公平鎖fairLock,使用tryLock()的方式獲取鎖也會是非公平的方式,只要獲取鎖時該鎖可用那麽就會直接獲取並返回true。這種直接插入的特性在一些特定場景是很有用的。但是如果就是想使用公平的方式的話,可以試一試tryLock(0, TimeUnit.SECONDS),幾乎跟公平鎖沒區別,只是會監測中斷事件。

示例

    @Test
    public void testtryLock() throws Exception {
        final Lock lock = new ReentrantLock();
        lock.lock();
        Thread.sleep(1000);
        Thread t1 = new Thread(() -> {
            boolean b = lock.tryLock();
            System.out.println(Thread.currentThread().getName() + " tryLock."+b);

        });
        t1.start();
        Thread.sleep(1000);
        t1.interrupt();
        Thread.sleep(10000);
    }

2.2.3、tryLock(long timeout, TimeUnit unit) 【限時等待】

此方法是表示在超時時間內獲取到同步狀態則返回true,獲取不到則返回false。由此可以聯想到AQStryAcquireNanos(int arg, long nanosTimeOut)方法

//ReentrantLock#tryLock 
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { 
  return sync.tryAcquireNanos(1, unit.toNanos(timeout)); 
} 

Sync實際上調用了父類AQStryAcquireNanos方法

//AbstractQueuedSynchronizer#tryAcquireNanos 
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { 
  if (Thread.interrupted())  
    throw new InterruptedException();//可以看到前面和lockInterruptibly一樣 
  return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);//首先也會先嘗試獲取鎖 
} 

2.2、解鎖

解鎖方式查看:013-AbstractQueuedSynchronizer-用於構建鎖和同步容器的框架、獨占鎖與共享鎖的獲取與釋放 的獨占模式鎖 釋放

三、小結

  1. 創建鎖對象 Lock lock = new ReentrantLock()
  2. 在希望保證線程同步的代碼之前顯示調用 lock.lock() 嘗試獲取鎖,若被其他線程占用,則阻塞
  3. 執行完之後,一定得手動釋放鎖,否則會造成死鎖 lock.unlock(); 一般來講,把釋放鎖的邏輯,放在需要線程同步的代碼包裝外的finally塊中
  4. lock() 和 unlock() 配套使用,不要出現一個比另一個用得多的情況
  5. 不要出現lock(),lock()連續調用的情況,即兩者之間沒有釋放鎖unlock()的顯示調用
  6. Condition與Lock配套使用,通過 Lock#newConditin() 進行實例化
  7. Condition#await() 會釋放lock,線程阻塞;直到線程中斷or Condition#singal()被執行,喚醒阻塞線程,並重新獲取lock
  8. ReentrantLock#lock的流程圖大致如下

  技術分享圖片

參看地址:

  https://my.oschina.net/u/566591/blog/1557978

  https://www.cnblogs.com/yulinfeng/p/6906597.html

  https://www.cnblogs.com/maypattis/p/6403682.html

016-並發編程-java.util.concurrent.locks之-Lock及ReentrantLock