原創聲明:本文來源於公衆號【胖滾豬學編程】 轉載請註明出處
在JAVA併發編程 如何解決原子性問題 的最後,咱們賣了個關子,互斥鎖不只僅只有synchronized關鍵字,還能夠用J.U.C中的Locks的包來實現,而且它很是強大!今天就來一探究竟吧!java
顧名思義,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 ++; } }
一、重入
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能保證原子性,那能夠保證可見性嗎?答案是必須的。
回憶下JAVA併發編程 如何解決可見性和有序性問題。咱們說 Java 裏多線程的可見性是經過 Happens-Before 規則保證的,好比 synchronized 之因此可以保證可見性,也是由於有一條 synchronized 相關的規則:synchronized 的解鎖 Happens-Before 於後續對這個鎖的加鎖。
那 Java SDK 裏面 Lock 靠什麼保證可見性呢?Java SDK 裏面鎖的實現很是複雜,可是原理仍是須要簡單介紹一下:它是利用了 volatile 相關的 Happens-Before 規則。
ReentrantLock的同步實際上是委託給AbstractQueuedSynchronizer的。加鎖和解鎖是經過改變AbstractQueuedSynchronizer的state屬性,這個屬性是volatile的。
獲取鎖的時候,會讀寫 state 的值;解鎖的時候,也會讀寫 state 的值。類比volatile是如何保證可見性的就能夠解決這個問題了!若是不清楚能夠回顧一下【漫畫】JAVA併發編程 如何解決可見性和有序性問題
synchronized 在JVM層面實現了對臨界資源的同步互斥訪問,但 synchronized 粒度有些大,在處理實際問題時存在諸多侷限性,好比響應中斷等。
Lock 提供了比 synchronized更普遍的鎖操做,它能以更優雅更靈活的方式處理線程同步問題。
咱們以ReentrantLock爲例子進入了Lock的世界,最重要的是記住ReentrantLock的特有功能,好比中斷、超時、非阻塞鎖等。當你的需求符合這些特有功能的時候,那你只能選擇Lock而不是synchronized
附文中代碼github地址:https://github.com/LYL41011/j...
原創聲明:本文來源於公衆號【胖滾豬學編程】 轉載請註明出處本文轉載自公衆號【胖滾豬學編程】 用漫畫讓編程so easy and interesting!