【漫畫】JAVA併發編程 J.U.C Lock包之ReentrantLock互斥鎖

原創聲明:本文來源於公衆號【胖滾豬學編程】 轉載請註明出處

JAVA併發編程 如何解決原子性問題 的最後,咱們賣了個關子,互斥鎖不只僅只有synchronized關鍵字,還能夠用J.U.C中的Locks的包來實現,而且它很是強大!今天就來一探究竟吧!java

_1
image

ReentrantLock

顧名思義,ReentrantLock叫作可重入鎖,所謂可重入鎖,顧名思義,指的是線程能夠重複獲取同一把鎖。git

ReentrantLock也是互斥鎖,所以也能夠保證原子性。github

先寫一個簡單的demo上手吧,就拿原子性問題中兩個線程分別作累加的demo爲例,如今使用ReentrantLock來改寫:編程

private void add10K() {
        // 獲取鎖
        reentrantLock.lock();
        try {
            int idx = 0;
            while (idx++ < 10000) {
                count++;
            }
        } finally {
            // 保證鎖能釋放
            reentrantLock.unlock();
        }

    }

ReentrantLock在這裏能夠達到和synchronized同樣的效果,爲了方便你回憶,我再次把synchronized實現互斥的代碼貼上來:多線程

private synchronized void add10K(){
        int start = 0;
        while (start ++ < 10000){
            this.count ++;
        }
    }

_2

ReentrantLock與synchronized的區別

一、重入
synchronized可重入,由於加鎖和解鎖自動進行,沒必要擔憂最後是否釋放鎖;ReentrantLock也可重入,但加鎖和解鎖須要手動進行,且次數需同樣,不然其餘線程沒法得到鎖。併發

二、實現
synchronized是JVM實現的、而ReentrantLock是JDK實現的。說白了就是,是操做系統來實現,仍是用戶本身敲代碼實現。app

三、性能
在 Java 的 1.5 版本中,synchronized 性能不如 SDK 裏面的 Lock,但 1.6 版本以後,synchronized 作了不少優化,將性能追了上來。函數

四、功能
ReentrantLock鎖的細粒度和靈活度,都明顯優於synchronized ,畢竟越麻煩使用的東西確定功能越多啦!性能

特有功能一:可指定是公平鎖仍是非公平鎖,而synchronized只能是非公平鎖。測試

公平的意思是先等待的線程先獲取鎖。能夠在構造函數中指定公平策略。

// 分別測試爲true 和 爲false的輸出。爲true則輸出順序必定是A B C 可是爲false的話有可能輸出A C B
    private static final ReentrantLock reentrantLock = new ReentrantLock(true);
    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo2 demo2 = new ReentrantLockDemo2();
        Thread a = new Thread(() -> { test(); }, "A");
        Thread b = new Thread(() -> { test(); }, "B");
        Thread c = new Thread(() -> { test(); }, "C");
        a.start();b.start();c.start();

    }
    public static void test() {
        reentrantLock.lock();
        try {
            System.out.println("線程" + Thread.currentThread().getName());
        } finally {
            reentrantLock.unlock();//必定要釋放鎖
        }
    }

在原子性文章的最後,咱們還賣了個關子,以轉帳爲例,說明synchronized會致使死鎖的問題,即兩個線程你等個人鎖,我等你的鎖,兩方都阻塞,不會釋放!爲了方便,我再次把代碼貼上來:

static void transfer(Account source,Account target, int amt) throws InterruptedException {
        // 鎖定轉出帳戶  Thread1鎖定了A Thread2鎖定了B
        synchronized (source) {
            Thread.sleep(1000);
            log.info("持有鎖{} 等待鎖{}",source,target);
            // 鎖定轉入帳戶  Thread1須要獲取到B,但是被Thread2鎖定了。Thread2須要獲取到A,但是被Thread1鎖定了。因此互相等待、死鎖
            synchronized (target) {
                if (source.getBalance() > amt) {
                    source.setBalance(source.getBalance() - amt);
                    target.setBalance(target.getBalance() + amt);
                }
            }
        }
    }

而ReentrantLock能夠完美避免死鎖問題,由於它能夠破壞死鎖四大必要條件之一的:不可搶佔條件。這得益於它這麼幾個功能:

特有功能二:非阻塞地獲取鎖。若是嘗試獲取鎖失敗,並不進入阻塞狀態,而是直接返回false,這時候線程不用阻塞等待,能夠先去作其餘事情。因此不會形成死鎖。

// 支持非阻塞獲取鎖的 API 
boolean tryLock();

如今咱們用ReentrantLock來改造一下死鎖代碼

static void transfer(Account source, Account target, int amt) throws InterruptedException {
        Boolean isContinue = true;
        while (isContinue) {
            if (source.getLock().tryLock()) {
                log.info("{}已獲取鎖 time{}", source.getLock(),System.currentTimeMillis());
                try {
                    if (target.getLock().tryLock()) {
                        log.info("{}已獲取鎖 time{}", target.getLock(),System.currentTimeMillis());
                        try {
                            log.info("開始轉帳操做");
                            source.setBalance(source.getBalance() - amt);
                            target.setBalance(target.getBalance() + amt);
                            log.info("結束轉帳操做 source{} target{}", source.getBalance(), target.getBalance());
                            isContinue=false;
                        } finally {
                            log.info("{}釋放鎖 time{}", target.getLock(),System.currentTimeMillis());
                            target.getLock().unlock();
                        }
                    }
                } finally {
                    log.info("{}釋放鎖 time{}", source.getLock(),System.currentTimeMillis());
                    source.getLock().unlock();
                }
            }
        }
    }

tryLock還支持超時。調用tryLock時沒有獲取到鎖,會等待一段時間,若是線程在一段時間以內仍是沒有獲取到鎖,不是進入阻塞狀態,而是throws InterruptedException,那這個線程也有機會釋放曾經持有的鎖,這樣也能破壞死鎖不可搶佔條件。
boolean tryLock(long time, TimeUnit unit)

特有功能三:提供可以中斷等待鎖的線程的機制

synchronized 的問題是,持有鎖 A 後,若是嘗試獲取鎖 B 失敗,那麼線程就進入阻塞狀態,一旦發生死鎖,就沒有任何機會來喚醒阻塞的線程。

但若是阻塞狀態的線程可以響應中斷信號,也就是說當咱們給阻塞的線程發送中斷信號的時候,可以喚醒它,那它就有機會釋放曾經持有的鎖 A。這樣就破壞了不可搶佔條件了。ReentrantLock能夠用lockInterruptibly方法來實現。

public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo5 demo2 = new ReentrantLockDemo5();
        Thread th1 = new Thread(() -> {
            try {
                deadLock(reentrantLock1, reentrantLock2);
            } catch (InterruptedException e) {
                System.out.println("線程A被中斷");
            }
        }, "A");
        Thread th2 = new Thread(() -> {
            try {
                deadLock(reentrantLock2, reentrantLock1);
            } catch (InterruptedException e) {
                System.out.println("線程B被中斷");
            }
        }, "B");
        th1.start();
        th2.start();
        th1.interrupt();

    }


    public static void deadLock(Lock lock1, Lock lock2) throws InterruptedException {
        lock1.lockInterruptibly(); //若是改爲用lock那麼是會一直死鎖的
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock2.lockInterruptibly();
        try {
            System.out.println("執行完成");
        } finally {
            lock1.unlock();
            lock2.unlock();
        }

    }

特有功能4、能夠用J.U.C包中的Condition實現分組喚醒須要等待的線程。而synchronized只能notify或者notifyAll。這裏涉及到線程之間的協做,在後續章節會詳細講解,敬請關注公衆號【胖滾豬學編程】。

ReentrantLock如何保證可見性

剛剛咱們證實了ReentrantLock能保證原子性,那能夠保證可見性嗎?答案是必須的。

回憶下JAVA併發編程 如何解決可見性和有序性問題。咱們說 Java 裏多線程的可見性是經過 Happens-Before 規則保證的,好比 synchronized 之因此可以保證可見性,也是由於有一條 synchronized 相關的規則:synchronized 的解鎖 Happens-Before 於後續對這個鎖的加鎖。

那 Java SDK 裏面 Lock 靠什麼保證可見性呢?Java SDK 裏面鎖的實現很是複雜,可是原理仍是須要簡單介紹一下:它是利用了 volatile 相關的 Happens-Before 規則。

ReentrantLock的同步實際上是委託給AbstractQueuedSynchronizer的。加鎖和解鎖是經過改變AbstractQueuedSynchronizer的state屬性,這個屬性是volatile的。

image

獲取鎖的時候,會讀寫 state 的值;解鎖的時候,也會讀寫 state 的值。類比volatile是如何保證可見性的就能夠解決這個問題了!若是不清楚能夠回顧一下【漫畫】JAVA併發編程 如何解決可見性和有序性問題

總結

synchronized 在JVM層面實現了對臨界資源的同步互斥訪問,但 synchronized 粒度有些大,在處理實際問題時存在諸多侷限性,好比響應中斷等。

Lock 提供了比 synchronized更普遍的鎖操做,它能以更優雅更靈活的方式處理線程同步問題。

咱們以ReentrantLock爲例子進入了Lock的世界,最重要的是記住ReentrantLock的特有功能,好比中斷、超時、非阻塞鎖等。當你的需求符合這些特有功能的時候,那你只能選擇Lock而不是synchronized

附文中代碼github地址:https://github.com/LYL41011/j...

原創聲明:本文來源於公衆號【胖滾豬學編程】 轉載請註明出處

本文轉載自公衆號【胖滾豬學編程】 用漫畫讓編程so easy and interesting!

相關文章
相關標籤/搜索