本篇文章開始將juc中經常使用的一些類,估計會有十來篇。java
synchronized是java內置的關鍵字,它提供了一種獨佔的加鎖方式。synchronized的獲取和釋放鎖由jvm實現,用戶不須要顯示的釋放鎖,很是方便,然而synchronized也有必定的侷限性,例如:安全
JDK1.5以後發佈,加入了Doug Lea實現的java.util.concurrent包。包內提供了Lock類,用來提供更多擴展的加鎖功能。Lock彌補了synchronized的侷限,提供了更加細粒度的加鎖功能。微信
ReentrantLock是Lock的默認實現,在聊ReentranLock以前,咱們須要先弄清楚一些概念:併發
咱們使用3個線程來對一個共享變量++操做,先使用synchronized實現,而後使用ReentrantLock實現。異步
synchronized方式:jvm
package com.itsoku.chat06; /** * 微信公衆號:路人甲Java,專一於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注! */ public class Demo2 { private static int num = 0; private static synchronized void add() { num++; } public static class T extends Thread { @Override public void run() { for (int i = 0; i < 10000; i++) { Demo2.add(); } } } public static void main(String[] args) throws InterruptedException { T t1 = new T(); T t2 = new T(); T t3 = new T(); t1.start(); t2.start(); t3.start(); t1.join(); t2.join(); t3.join(); System.out.println(Demo2.num); } }
ReentrantLock方式:分佈式
package com.itsoku.chat06; import java.util.concurrent.locks.ReentrantLock; /** * 微信公衆號:路人甲Java,專一於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注! */ public class Demo3 { private static int num = 0; private static ReentrantLock lock = new ReentrantLock(); private static void add() { lock.lock(); try { num++; } finally { lock.unlock(); } } public static class T extends Thread { @Override public void run() { for (int i = 0; i < 10000; i++) { Demo3.add(); } } } public static void main(String[] args) throws InterruptedException { T t1 = new T(); T t2 = new T(); T t3 = new T(); t1.start(); t2.start(); t3.start(); t1.join(); t2.join(); t3.join(); System.out.println(Demo3.num); } }
ReentrantLock的使用過程:ide
對比上面的代碼,與關鍵字synchronized相比,ReentrantLock鎖有明顯的操做過程,開發人員必須手動的指定什麼時候加鎖,什麼時候釋放鎖,正是由於這樣手動控制,ReentrantLock對邏輯控制的靈活度要遠遠勝於關鍵字synchronized,上面代碼須要注意lock.unlock()必定要放在finally中,不然,若程序出現了異常,鎖沒有釋放,那麼其餘線程就再也沒有機會獲取這個鎖了。高併發
來驗證一下ReentrantLock是可重入鎖,實例代碼:性能
package com.itsoku.chat06; import java.util.concurrent.locks.ReentrantLock; /** * 微信公衆號:路人甲Java,專一於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注! */ public class Demo4 { private static int num = 0; private static ReentrantLock lock = new ReentrantLock(); private static void add() { lock.lock(); lock.lock(); try { num++; } finally { lock.unlock(); lock.unlock(); } } public static class T extends Thread { @Override public void run() { for (int i = 0; i < 10000; i++) { Demo4.add(); } } } public static void main(String[] args) throws InterruptedException { T t1 = new T(); T t2 = new T(); T t3 = new T(); t1.start(); t2.start(); t3.start(); t1.join(); t2.join(); t3.join(); System.out.println(Demo4.num); } }
上面代碼中add()方法中,當一個線程進入的時候,會執行2次獲取鎖的操做,運行程序能夠正常結束,並輸出和指望值同樣的30000,假如ReentrantLock是不可重入的鎖,那麼同一個線程第2次獲取鎖的時候因爲前面的鎖還未釋放而致使死鎖,程序是沒法正常結束的。ReentrantLock命名也挺好的Re entrant Lock,和其名字同樣,可重入鎖。
代碼中還有幾點須要注意:
在大多數狀況下,鎖的申請都是非公平的,也就是說,線程1首先請求鎖A,接着線程2也請求了鎖A。那麼當鎖A可用時,是線程1可得到鎖仍是線程2可得到鎖呢?這是不必定的,系統只是會從這個鎖的等待隊列中隨機挑選一個,所以不能保證其公平性。這就比如買票不排隊,你們都圍在售票窗口前,售票員忙的焦頭爛額,也顧及不上誰先誰後,隨便找我的出票就完事了,最終致使的結果是,有些人可能一直買不到票。而公平鎖,則不是這樣,它會按照到達的前後順序得到資源。公平鎖的一大特色是:它不會產生飢餓現象,只要你排隊,最終仍是能夠等到資源的;synchronized關鍵字默認是有jvm內部實現控制的,是非公平鎖。而ReentrantLock運行開發者本身設置鎖的公平性。
看一下jdk中ReentrantLock的源碼,2個構造方法:
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
默認構造方法建立的是非公平鎖。
第2個構造方法,有個fair參數,當fair爲true的時候建立的是公平鎖,公平鎖看起來很不錯,不過要實現公平鎖,系統內部確定須要維護一個有序隊列,所以公平鎖的實現成本比較高,性能相對於非公平鎖來講相對低一些。所以,在默認狀況下,鎖是非公平的,若是沒有特別要求,則不建議使用公平鎖。
公平鎖和非公平鎖在程序調度上是很不同,來一個公平鎖示例看一下:
package com.itsoku.chat06; import java.util.concurrent.locks.ReentrantLock; /** * 微信公衆號:路人甲Java,專一於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注! */ public class Demo5 { private static int num = 0; private static ReentrantLock fairLock = new ReentrantLock(true); public static class T extends Thread { public T(String name) { super(name); } @Override public void run() { for (int i = 0; i < 5; i++) { fairLock.lock(); try { System.out.println(this.getName() + "得到鎖!"); } finally { fairLock.unlock(); } } } } public static void main(String[] args) throws InterruptedException { T t1 = new T("t1"); T t2 = new T("t2"); T t3 = new T("t3"); t1.start(); t2.start(); t3.start(); t1.join(); t2.join(); t3.join(); } }
運行結果輸出:
t1得到鎖! t2得到鎖! t3得到鎖! t1得到鎖! t2得到鎖! t3得到鎖! t1得到鎖! t2得到鎖! t3得到鎖! t1得到鎖! t2得到鎖! t3得到鎖! t1得到鎖! t2得到鎖! t3得到鎖!
看一下輸出的結果,鎖時按照前後順序得到的。
修改一下上面代碼,改成非公平鎖試試,以下:
ReentrantLock fairLock = new ReentrantLock(false);
運行結果以下:
t1得到鎖! t3得到鎖! t3得到鎖! t3得到鎖! t3得到鎖! t1得到鎖! t1得到鎖! t1得到鎖! t1得到鎖! t2得到鎖! t2得到鎖! t2得到鎖! t2得到鎖! t2得到鎖! t3得到鎖!
能夠看到t3可能會連續得到鎖,結果是比較隨機的,不公平的。
對於synchronized關鍵字,若是一個線程在等待獲取鎖,最終只有2種結果:
而ReentrantLock提供了另一種可能,就是在等的獲取鎖的過程當中(發起獲取鎖請求到還未獲取到鎖這段時間內)是能夠被中斷的,也就是說在等待鎖的過程當中,程序能夠根據須要取消獲取鎖的請求。有些使用這個操做是很是有必要的。好比:你和好朋友越好一塊兒去打球,若是你等了半小時朋友還沒到,忽然你接到一個電話,朋友因爲突發情況,不能來了,那麼你必定達到回府。中斷操做正是提供了一套相似的機制,若是一個線程正在等待獲取鎖,那麼它依然能夠收到一個通知,被告知無需等待,能夠中止工做了。
示例代碼:
package com.itsoku.chat06; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * 微信公衆號:路人甲Java,專一於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注! */ public class Demo6 { private static ReentrantLock lock1 = new ReentrantLock(false); private static ReentrantLock lock2 = new ReentrantLock(false); public static class T extends Thread { int lock; public T(String name, int lock) { super(name); this.lock = lock; } @Override public void run() { try { if (this.lock == 1) { lock1.lockInterruptibly(); TimeUnit.SECONDS.sleep(1); lock2.lockInterruptibly(); } else { lock2.lockInterruptibly(); TimeUnit.SECONDS.sleep(1); lock1.lockInterruptibly(); } } catch (InterruptedException e) { System.out.println("中斷標誌:" + this.isInterrupted()); e.printStackTrace(); } finally { if (lock1.isHeldByCurrentThread()) { lock1.unlock(); } if (lock2.isHeldByCurrentThread()) { lock2.unlock(); } } } } public static void main(String[] args) throws InterruptedException { T t1 = new T("t1", 1); T t2 = new T("t2", 2); t1.start(); t2.start(); } }
先運行一下上面代碼,發現程序沒法結束,使用jstack查看線程堆棧信息,發現2個線程死鎖了。
Found one Java-level deadlock: ============================= "t2": waiting for ownable synchronizer 0x0000000717380c20, (a java.util.concurrent.locks.ReentrantLock$NonfairSync), which is held by "t1" "t1": waiting for ownable synchronizer 0x0000000717380c50, (a java.util.concurrent.locks.ReentrantLock$NonfairSync), which is held by "t2"
lock1被線程t1佔用,lock2倍線程t2佔用,線程t1在等待獲取lock2,線程t2在等待獲取lock1,都在相互等待獲取對方持有的鎖,最終產生了死鎖,若是是在synchronized關鍵字狀況下發生了死鎖現象,程序是沒法結束的。
咱們隊上面代碼改造一下,線程t2一直沒法獲取到lock1,那麼等待5秒以後,咱們中斷獲取鎖的操做。主要修改一下main方法,以下:
T t1 = new T("t1", 1); T t2 = new T("t2", 2); t1.start(); t2.start(); TimeUnit.SECONDS.sleep(5); t2.interrupt();
新增了2行代碼TimeUnit.SECONDS.sleep(5);t2.interrupt();
,程序能夠結束了,運行結果:
java.lang.InterruptedException at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222) at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) at com.itsoku.chat06.Demo6$T.run(Demo6.java:31) 中斷標誌:false
從上面信息中能夠看出,代碼的31行觸發了異常,中斷標誌輸出:false
t2在31行一直獲取不到lock1的鎖,主線程中等待了5秒以後,t2線程調用了interrupt()
方法,將線程的中斷標誌置爲true,此時31行會觸發InterruptedException
異常,而後線程t2能夠繼續向下執行,釋放了lock2的鎖,而後線程t1能夠正常獲取鎖,程序得以繼續進行。線程發送中斷信號觸發InterruptedException異常以後,中斷標誌將被清空。
關於獲取鎖的過程當中被中斷,注意幾點:
lockInterruptibly()
獲取鎖時,在線程調用interrupt()方法以後,纔會引起InterruptedException
異常申請鎖等待限時是什麼意思?通常狀況下,獲取鎖的時間咱們是不知道的,synchronized關鍵字獲取鎖的過程當中,只能等待其餘線程把鎖釋放以後纔可以有機會獲取到所。因此獲取鎖的時間有長有短。若是獲取鎖的時間可以設置超時時間,那就很是好了。
ReentrantLock恰好提供了這樣功能,給咱們提供了獲取鎖限時等待的方法tryLock()
,能夠選擇傳入時間參數,表示等待指定的時間,無參則表示當即返回鎖申請的結果:true表示獲取鎖成功,false表示獲取鎖失敗。
看一下源碼中tryLock方法:
public boolean tryLock()
返回boolean類型的值,此方法會當即返回,結果表示獲取鎖是否成功,示例:
package com.itsoku.chat06; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * 微信公衆號:路人甲Java,專一於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注! */ public class Demo8 { private static ReentrantLock lock1 = new ReentrantLock(false); public static class T extends Thread { public T(String name) { super(name); } @Override public void run() { try { System.out.println(System.currentTimeMillis() + ":" + this.getName() + "開始獲取鎖!"); //獲取鎖超時時間設置爲3秒,3秒內是否可否獲取鎖都會返回 if (lock1.tryLock()) { System.out.println(System.currentTimeMillis() + ":" + this.getName() + "獲取到了鎖!"); //獲取到鎖以後,休眠5秒 TimeUnit.SECONDS.sleep(5); } else { System.out.println(System.currentTimeMillis() + ":" + this.getName() + "未能獲取到鎖!"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lock1.isHeldByCurrentThread()) { lock1.unlock(); } } } } public static void main(String[] args) throws InterruptedException { T t1 = new T("t1"); T t2 = new T("t2"); t1.start(); t2.start(); } }
代碼中獲取鎖成功以後,休眠5秒,會致使另一個線程獲取鎖失敗,運行代碼,輸出:
1563356291081:t2開始獲取鎖! 1563356291081:t2獲取到了鎖! 1563356291081:t1開始獲取鎖! 1563356291081:t1未能獲取到鎖!
能夠看到t2獲取成功,t1獲取失敗了,tryLock()是當即響應的,中間不會有阻塞。
能夠明確設置獲取鎖的超時時間,該方法簽名:
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
該方法在指定的時間內不論是否能夠獲取鎖,都會返回結果,返回true,表示獲取鎖成功,返回false表示獲取失敗。此方法由2個參數,第一個參數是時間類型,是一個枚舉,能夠表示時、分、秒、毫秒等待,使用比較方便,第1個參數表示在時間類型上的時間長短。此方法在執行的過程當中,若是調用了線程的中斷interrupt()方法,會觸發InterruptedException異常。
示例:
package com.itsoku.chat06; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * 微信公衆號:路人甲Java,專一於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注! */ public class Demo7 { private static ReentrantLock lock1 = new ReentrantLock(false); public static class T extends Thread { public T(String name) { super(name); } @Override public void run() { try { System.out.println(System.currentTimeMillis() + ":" + this.getName() + "開始獲取鎖!"); //獲取鎖超時時間設置爲3秒,3秒內是否可否獲取鎖都會返回 if (lock1.tryLock(3, TimeUnit.SECONDS)) { System.out.println(System.currentTimeMillis() + ":" + this.getName() + "獲取到了鎖!"); //獲取到鎖以後,休眠5秒 TimeUnit.SECONDS.sleep(5); } else { System.out.println(System.currentTimeMillis() + ":" + this.getName() + "未能獲取到鎖!"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lock1.isHeldByCurrentThread()) { lock1.unlock(); } } } } public static void main(String[] args) throws InterruptedException { T t1 = new T("t1"); T t2 = new T("t2"); t1.start(); t2.start(); } }
程序中調用了ReentrantLock的實例方法tryLock(3, TimeUnit.SECONDS)
,表示獲取鎖的超時時間是3秒,3秒後不論是否可否獲取鎖,該方法都會有返回值,獲取到鎖以後,內部休眠了5秒,會致使另一個線程獲取鎖失敗。
運行程序,輸出:
1563355512901:t2開始獲取鎖! 1563355512901:t1開始獲取鎖! 1563355512902:t2獲取到了鎖! 1563355515904:t1未能獲取到鎖!
輸出結果中分析,t2獲取到鎖了,而後休眠了5秒,t1獲取鎖失敗,t1打印了2條信息,時間相差3秒左右。
關於tryLock()方法和tryLock(long timeout, TimeUnit unit)方法,說明一下:
InterruptedException
異常,這個從2個方法的聲明上能夠能夠看出來獲取鎖的方法 | 是否當即響應(不會阻塞) | 是否響應中斷 |
---|---|---|
lock() | × | × |
lockInterruptibly() | × | √ |
tryLock() | √ | × |
tryLock(long timeout, TimeUnit unit) | × | √ |
InterruptedException
異常InterruptedException
異常說一下,看到方法聲明上帶有 throws InterruptedException
,表示該方法能夠相應線程中斷,調用線程的interrupt()方法時,這些方法會觸發InterruptedException
異常,觸發InterruptedException時,線程的中斷中斷狀態會被清除。因此若是程序因爲調用interrupt()
方法而觸發InterruptedException
異常,線程的標誌由默認的false變爲ture,而後又變爲falsejava高併發系列連載中,總計估計會有四五十篇文章,能夠關注公衆號:javacode2018,獲取最新文章。