1. 程式人生 > 實用技巧 >聽說你不會Lock,我發了3個夜晚寫給你

聽說你不會Lock,我發了3個夜晚寫給你

我們知道 synchronized 是java內部關鍵字,比較重量級的獨佔鎖,好處就是使用方便,不需要手動釋放鎖;然而

Lock 則需要手動加鎖,手動釋放鎖;

一ReentrantLock使用

ReentrantLock 意為可重入鎖,方法預覽如下

//建立一個 ReentrantLock 的例項
ReentrantLock() 
 //建立一個具有給定公平策略的 ReentrantLock     
ReentrantLock(boolean fair)
//查詢當前執行緒持有鎖的個數
int getHoldCount() 
//返回目前擁有此鎖的執行緒,如果此鎖不被任何執行緒擁有,則返回 null     
protected Thread getOwner() 
//返回一個collection,它包含可能正等待獲取此鎖的執行緒     
protected Collection<Thread> getQueuedThreads() 
int getQueueLength() //返回正等待獲取此鎖的執行緒估計數 
 //返回一個 collection,它包含可能正在等待與此鎖相關給定條件的那些執行緒     
protected Collection<Thread> getWaitingThreads(Condition condition)
//返回等待與此鎖相關的給定條件的執行緒估計數    
int getWaitQueueLength(Condition condition) 
//查詢給定執行緒是否正在等待獲取此鎖    
boolean hasQueuedThread(Thread thread) 
//查詢是否有些執行緒正在等待獲取此鎖    
boolean hasQueuedThreads() 
//查詢是否有些執行緒正在等待與此鎖有關的給定條件    
boolean hasWaiters(Condition condition) 
//如果此鎖的公平設定為 true,則返回true 
boolean isFair() 
//查詢當前執行緒是否保持此鎖    
boolean isHeldByCurrentThread()
//查詢此鎖是否由任意執行緒保持    
boolean isLocked()
//獲取鎖    
void lock()
//如果當前執行緒未被中斷,則獲取鎖。    
void lockInterruptibly() 
//返回用來與此 Lock 例項一起使用的 Condition 例項     
Condition newCondition() 
 //僅在呼叫時鎖未被另一個執行緒保持的情況下,才獲取該鎖    
boolean tryLock()
//如果鎖在給定等待時間內沒有被另一個執行緒保持,且當前執行緒未被中斷,則獲取該鎖 
boolean tryLock(long timeout, TimeUnit unit)
//釋放此鎖
void unlock() 

1.1Lock與unLock 簡單使用

使用 lock() 加鎖, unlock()釋放鎖;

public class RLook {

    private Lock lock = new ReentrantLock();

    private void testLock(){
        // 加鎖
        lock.lock();
        // 執行業務邏輯
        for (int i=0; i<8; i++){
            System.out.println(i+"==="+Thread.currentThread().getName());
        }
        // 釋放鎖
        lock.unlock();
    }

    public static void main(String[] args) {
        // 執行緒1
        RLook rLook = new RLook();
        new Thread(()-> {
            rLook.testLock();
        }).start();
        // 執行緒2 
        new Thread(()-> {
            rLook.testLock();
        }).start();
    }
}

輸出如下,不同執行緒之間應是分組列印;

0===Thread-0
1===Thread-0
2===Thread-0
3===Thread-0
4===Thread-0
5===Thread-0
6===Thread-0
7===Thread-0
0===Thread-1
1===Thread-1
2===Thread-1
3===Thread-1
4===Thread-1
5===Thread-1
6===Thread-1
7===Thread-1

1.2Lock與unLock 正確使用方式

上面的程式碼有個缺點,如果在執行業務程式碼的時候發生了異常就會發生死鎖的現象,所以通常情況下我們會將業務程式碼放在try{},catch{} 程式碼塊中, 最後使用 finally 釋放鎖;

程式碼格式應如下

Lock lock =new ReentrantLock();
....
lock.lock();
try{
    //處理任務
}catch(Exception ex){
     
}finally{
    lock.unlock();   //釋放鎖
}
....

1.3原始碼角度說明Lock

public interface Lock {
	// 加鎖
    void lock();
    // 中斷
    void lockInterruptibly() throws InterruptedException;
    // 嘗試獲取鎖
    boolean tryLock();
    // 嘗試獲取鎖
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 釋放鎖
    void unlock();
    // 訊號通知
    Condition newCondition();
}
  • lock()方法是用來獲取鎖。如果鎖已被其他執行緒獲取,則進行鎖等待。採用Lock必須手動釋放鎖,並且在發生異常時,不會自動釋放鎖,所以需要放在try,cath程式碼塊中, 最後使用 finally 釋放鎖;

  • tryLock()方法用來表示嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗,則返回false;

  • tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,區別在於可以設定鎖等待時間,在等待時間內為獲取到鎖,則返回false,獲取到則返回true;

Lock lock=new ReentrantLock();
....
if(lock.tryLock()) {
     try{
         //業務邏輯
     }catch(Exception ex){
         
     }finally{
     //釋放鎖
         lock.unlock();   
     } 
}else {
    // 未獲取鎖邏輯
}
  • lockInterruptibly()為中斷方法,當通過這個方法去獲取鎖時,如果執行緒正在處於獲取鎖狀態,則該執行緒能夠響應中斷(中斷執行緒的等待狀態),丟擲中斷異常。也就使說,當兩個執行緒同時通過lock.lockInterruptibly()獲取某個鎖時,如果執行緒A獲取到了鎖,而執行緒B在等待狀態,那麼對執行緒B呼叫threadB.interrupt()方法能夠中斷執行緒B的等待狀態;
  • Condition類是JDK5的新API,可以實現多路通知功能,一個Lock物件可以建立多個Condition, 通過注入不同的Condition靈活實現不同的執行緒通知功能;然而synchronized 的 wait(), notify(), notify All() 通知機制是隨機無法實現選擇性通知功能;

1.4使用lockInterruptibly

public class InterruptTest {

    private Lock lock = new ReentrantLock();

    public void interrupt() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了鎖");
            long startTime = System.currentTimeMillis();

            for (; ; ) {
                if (System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
                    break;
            }
        } finally {
            lock.unlock();
            System.out.println(Thread.currentThread().getName() + "釋放鎖");
        }
    }

    public static void main(String[] args) {
        // 執行緒1
        InterruptTest rLook = new InterruptTest();
        Thread threadA = new Thread(() -> {
            try {
                rLook.interrupt();
            } catch (InterruptedException e) {
                System.out.println("執行緒A進行了中斷");
                e.printStackTrace();
            }
        });
        // 執行緒2
        Thread threadB = new Thread(() -> {
            try {
                rLook.interrupt();
            } catch (InterruptedException e) {
                System.out.println("執行緒B進行了中斷");
                e.printStackTrace();
            }
        });
        threadA.setName("A");
        threadB.setName("B");
        threadA.start();
        threadB.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadB.interrupt();
    }
}

輸出結果如下,執行緒A通過中斷獲取鎖,執行緒B再通過中斷獲取鎖時處於等待狀態,直接中斷丟擲異常;

A得到了鎖
執行緒B進行了中斷

1.5使用Condition實現等待通知機制

codition 介面主要方法如下

 //造成當前執行緒在接到訊號或被中斷之前一直處於等待狀態。 
 void await() 
 //造成當前執行緒在接到訊號、被中斷或到達指定等待時間之前一直處於等待狀態。 
 boolean await(long time, TimeUnit unit) 
 //造成當前執行緒在接到訊號、被中斷或到達指定等待時間之前一直處於等待狀態。 
 long awaitNanos(long nanosTimeout) 
 //造成當前執行緒在接到訊號之前一直處於等待狀態。 
 void awaitUninterruptibly() 
 //造成當前執行緒在接到訊號、被中斷或到達指定最後期限之前一直處於等待狀態。 
 boolean awaitUntil(Date deadline) 
 //喚醒一個等待執行緒。
 void signal()  
 //喚醒所有等待執行緒。
 void signalAll()  

利用 Condition 的await() 方法和 signal() 方法 實現 等待通知機制;

public class ConditionTest {

    private Lock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    // 執行緒等待
    private void await(){
        try {
            // 加鎖
            lock.lock();
            System.out.println("await時間為:"+System.currentTimeMillis());
            // 等待
            condition.await();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 釋放鎖
            lock.unlock();
        }
    }

    // 執行緒喚醒
    public void signal() {
        lock.lock();
        try {
            System.out.println("signal時間為" + System.currentTimeMillis());
            condition.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 執行緒1
        ConditionTest rLook = new ConditionTest();
        Thread thread = new Thread(() -> {
            rLook.await();
        });
        thread.start();
        //
        thread.sleep(3000);
        //
        rLook.signal();
    }
}

輸出輸出結果如下

await時間為:1606707478993
signal時間為1606707481993

多個Condition 使用方式

多個Condition 使用時,互不干涉; 執行緒 A, B 使用 不同Condition 的都 進入等待狀態, 當使用 對應的 Condition 的 singal 才喚醒對應的執行緒;

public class MultiCondition {

    private Lock lock = new ReentrantLock();

    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();

    // 執行緒等待
    private void awaitA(){
        try {
            // 加鎖
            lock.lock();
            System.out.println("執行緒"+Thread.currentThread().getName()+"----await時間為:"+System.currentTimeMillis());
            // 等待
            conditionA.await();
            System.out.println("執行緒"+Thread.currentThread().getName()+"繼續執行");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 釋放鎖
            lock.unlock();
        }
    }

    // 執行緒等待
    private void awaitB(){
        try {
            // 加鎖
            lock.lock();
            System.out.println("執行緒"+Thread.currentThread().getName()+"----await時間為:"+System.currentTimeMillis());
            // 等待
            conditionB.await();
            System.out.println("執行緒"+Thread.currentThread().getName()+"繼續執行");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 釋放鎖
            lock.unlock();
        }
    }

    // 執行緒喚醒
    public void signalAllA() {
        lock.lock();
        try {
            System.out.println("執行緒"+Thread.currentThread().getName()+"----signal時間為" + System.currentTimeMillis());
            conditionA.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    // 執行緒喚醒
    public void signalAllB() {
        lock.lock();
        try {
            System.out.println("執行緒"+Thread.currentThread().getName()+"----signal時間為" + System.currentTimeMillis());
            conditionB.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 執行緒1
        MultiCondition rLook = new MultiCondition();
        Thread threadA = new Thread(() -> {
            rLook.awaitA();
        });
        threadA.setName("A");
        threadA.start();
        Thread threadB = new Thread(() -> {
            rLook.awaitB();
        });
        threadB.setName("B");
        threadB.start();
        //
        Thread.sleep(3000);
        //
        rLook.signalAllA();
    }
}

輸出結果如下,執行緒Condition A 對應的執行緒會被喚醒,B 執行緒 還是處於等待狀態;

執行緒A----await時間為:1606708895650
執行緒B----await時間為:1606708895650
執行緒main----signal時間為1606708898651
執行緒A繼續執行

1.6使用Condition實現生產消費模式

public class ConsumerProductCondition {

    private Lock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    private Boolean has = false;

    // 生產
    private void set(){
        try {
            // 加鎖
            lock.lock();
            while (has == true){
                condition.await();
            }
            System.out.println("生產GG");
            has = true;
            // 喚醒一個執行緒
            condition.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 釋放鎖
            lock.unlock();
        }
    }

    // 消費
    private void get(){
        try {
            // 加鎖
            lock.lock();
            while (has == false){
                condition.await();
            }
            System.out.println("消費MM");
            has = false;
            // 喚醒一個執行緒
            condition.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 釋放鎖
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 執行緒1
        ConsumerProductCondition rLook = new ConsumerProductCondition();
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 200; i++) {
                rLook.set();
            }
        });
       // 執行緒2
        Thread threadB = new Thread(() -> {
            for (int i = 0; i < 200; i++) {
                rLook.get();
            }
        });
        threadA.start();
        threadB.start();
    }
}

輸出交替列印

生產GG
消費MM
生產GG
消費MM
生產GG
消費MM
生產GG
消費MM
生產GG
消費MM
.....

1.7 公平鎖與非公平鎖

Lock 鎖分為公平鎖和非公平鎖,公平鎖表示通過執行緒加鎖的順序獲取鎖,非公平鎖表示通過搶佔機制獲取鎖;

公平鎖獲示例

public class FairLockTest {


    private Lock lock = new ReentrantLock(true);

    private void testLock(){
        // 加鎖
        lock.lock();
        // 執行業務邏輯
        System.out.println("執行緒"+Thread.currentThread().getName()+"獲取到鎖");
        // 釋放鎖
        lock.unlock();
    }

    public static void main(String[] args) {
        // 執行緒1
        FairLockTest rLook = new FairLockTest();


        for (int i = 0; i <100 ; i++) {
            // 執行緒2
            Thread thread = new Thread(() -> {
                rLook.testLock();
            });
            thread.setName(""+i);
            thread.start();
        }

    }

}

輸出結果基本有序

執行緒0獲取到鎖
執行緒1獲取到鎖
執行緒2獲取到鎖
執行緒3獲取到鎖
執行緒4獲取到鎖
執行緒5獲取到鎖
執行緒6獲取到鎖
執行緒8獲取到鎖
執行緒7獲取到鎖
執行緒9獲取到鎖
執行緒10獲取到鎖
....

非公平鎖示例如下

public class FairLockTest {


    private Lock lock = new ReentrantLock(false);

    private void testLock(){
        // 加鎖
        lock.lock();
        // 執行業務邏輯
        System.out.println("執行緒"+Thread.currentThread().getName()+"獲取到鎖");
        // 釋放鎖
        lock.unlock();
    }

    public static void main(String[] args) {
        // 執行緒1
        FairLockTest rLook = new FairLockTest();


        for (int i = 0; i <100 ; i++) {
            // 執行緒2
            Thread thread = new Thread(() -> {
                rLook.testLock();
            });
            thread.setName(""+i);
            thread.start();
        }

    }

}

輸出結果基本無序

執行緒0獲取到鎖
執行緒1獲取到鎖
執行緒7獲取到鎖
執行緒2獲取到鎖
執行緒10獲取到鎖
執行緒3獲取到鎖
執行緒6獲取到鎖
執行緒11獲取到鎖
執行緒4獲取到鎖
執行緒5獲取到鎖
.........

二 ReentrantReadWriteLock 使用

ReentrantLock 與 synchronized 都是 獨佔鎖, 安全性較高,但是相對來說效率低下;ReentrantReadWriteLock讀寫鎖提供了 readLock()和writeLock()用來獲取讀鎖和寫鎖; 讀鎖之間讀取資料不互斥(故讀鎖也成為共享鎖)。讀鎖寫鎖之間互斥,寫鎖寫鎖之間互斥;

2.1讀鎖

讀鎖示例

public class ReadLockTest {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private void testLock(){
        // 加鎖
        lock.readLock().lock();
        // 執行業務邏輯
        System.out.println("執行緒"+Thread.currentThread().getName()+"獲取到鎖"+System.currentTimeMillis());
        // 睡眠,保證B執行緒進來時A執行緒還是獲取鎖狀態
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 釋放鎖
        lock.readLock().unlock();
    }

    public static void main(String[] args) {
        // 執行緒1
        ReadLockTest rLook = new ReadLockTest();
        // 執行緒2
        Thread threadA = new Thread(() -> {
            rLook.testLock();
        });
        threadA.setName("A");
        Thread threadB = new Thread(() -> {
            rLook.testLock();
        });
        threadB.setName("B");
        threadA.start();
        threadB.start();


    }
}

執行緒A與執行緒B幾乎時同時獲取到鎖

執行緒B獲取到鎖1606723001641
執行緒A獲取到鎖1606723001641

2.2 寫鎖

寫鎖示例

public class WriteLockTest {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private void testLock(){
        // 加鎖
        lock.writeLock().lock();
        // 執行業務邏輯
        System.out.println("執行緒"+Thread.currentThread().getName()+"獲取到鎖"+System.currentTimeMillis());
        // 睡眠,保證B執行緒進來時A執行緒還是獲取鎖狀態
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 釋放鎖
        lock.writeLock().unlock();
    }

    public static void main(String[] args) {
        // 執行緒1
        WriteLockTest rLook = new WriteLockTest();
        // 執行緒2
        Thread threadA = new Thread(() -> {
            rLook.testLock();
        });
        threadA.setName("A");
        Thread threadB = new Thread(() -> {
            rLook.testLock();
        });
        threadB.setName("B");
        threadA.start();
        threadB.start();


    }
}

輸出結果基本就是相差 2 秒,即執行緒A獲取鎖後,執行緒B需要等執行緒A釋放鎖才能獲取鎖

執行緒A獲取到鎖1606723216664
執行緒B獲取到鎖1606723218664

2.3 讀寫鎖

讀寫鎖示例

public class ReadAndWriteTest {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private void writeLock(){
        // 加鎖
        lock.writeLock().lock();
        // 執行業務邏輯
        System.out.println("執行緒"+Thread.currentThread().getName()+"獲取到寫鎖"+System.currentTimeMillis());
        // 睡眠,保證B執行緒進來時A執行緒還是獲取鎖狀態
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 釋放鎖
        lock.writeLock().unlock();
    }

    private void readLock(){
        // 加鎖
        lock.readLock().lock();
        // 執行業務邏輯
        System.out.println("執行緒"+Thread.currentThread().getName()+"獲取到讀鎖"+System.currentTimeMillis());
        // 釋放鎖
        lock.readLock().unlock();
    }

    public static void main(String[] args) {
        // 執行緒1
        ReadAndWriteTest rLook = new ReadAndWriteTest();
        // 執行緒2
        Thread threadA = new Thread(() -> {
            rLook.writeLock();
        });
        threadA.setName("A");
        Thread threadB = new Thread(() -> {
            rLook.readLock();
        });
        threadB.setName("B");
        threadA.start();
        threadB.start();


    }
}

輸出結果如下,剛好相差2秒;當執行緒A獲取到寫鎖,執行緒B必須等待執行緒A釋放寫鎖後才能獲取到讀鎖;

執行緒A獲取到寫鎖1606724500217
執行緒B獲取到讀鎖1606724502217

三 總結

通過本篇文章,我們大概知道常見的鎖的使用方式;

  • ReentrantLock 和 synchronized 都是 獨佔鎖,又是可重入鎖;獨佔鎖想必大家都知道,重點解釋下可重入鎖,

當執行緒A獲取到鎖進入 methodA 時,再次呼叫 methodB 就不需要再次獲取鎖,即代表可重入鎖;

class Test{
    public synchronized void methodA() {
        methodB();
    }
     
    public synchronized void methodB() {
         
    }
}
  • synchronized不是可中斷鎖,而Lock是可中斷鎖。如果某一執行緒A正在執行鎖中的程式碼,執行緒B由於等待時間過長去處理其它業務,就稱執行緒B獲得的鎖為可中斷鎖;

  • 公平鎖儘量以請求鎖的順序來獲取鎖。比如有多個執行緒在等待一個鎖,當這個鎖被釋放時,等待時間最久的執行緒(最先請求的執行緒)會獲得該鎖,這就是公平鎖;非公平鎖獲取方式就是完全隨機,搶佔模式;

  • 讀寫鎖一般用於操作檔案,只要涉及到寫鎖都是獨佔鎖;讀鎖是共享鎖;

有關 其它鎖的知識,可以閱讀知識追尋者以前釋出的併發程式設計系列文章

歡迎關注我的公眾號:知識追尋者,送原創PDF,面經,開源系統