承接上文
https://www.cnblogs.com/wkfvawl/p/15489569.html
一、多把鎖
- 小故事
- 一間大屋子有兩個功能:
睡覺、學習,互不相干
。 - 現在小南要學習,小女要睡覺,但如果只用一間屋子(一個對象鎖)的話,那么
並發度很低。
小南獲得鎖之后, 學完習之后, 小女才能進來睡覺。
@Slf4j(topic = "c.BigRoom") public class BigRoomTest { public static void main(String[] args) { BigRoom bigRoom = new BigRoom(); new Thread(() -> bigRoom.sleep(), "小南").start(); new Thread(() -> bigRoom.study(), "小女").start(); } } @Slf4j(topic = "c.BigRoom") class BigRoom { public void sleep() { synchronized (this) { log.debug("sleeping 2 小時"); Sleeper.sleep(2); } } public void study() { synchronized (this) { log.debug("study 1 小時"); Sleeper.sleep(1); } } }
改進方法是准備多個房間(多個對象鎖)
小南, 小女
獲取不同的鎖即可
@Slf4j(topic = "c.BigRoom") class BigRoom { private final Object studyRoom = new Object(); private final Object bedRoom = new Object(); public void sleep() { synchronized (bedRoom) { log.debug("sleeping 2 小時"); Sleeper.sleep(2); } } public void study() { synchronized (studyRoom) { log.debug("study 1 小時"); Sleeper.sleep(1); } } }
將鎖的粒度細分
- 好處,是可以
增強並發度
- 壞處,如果一個線程需要同時獲得多把鎖,就
容易發生死鎖
二、 活躍性
因為某種原因,使得代碼一直無法執行完畢,這樣的現象叫做 活躍性
活躍性相關的一系列問題都可以用 ReentrantLock 進行解決。
2.1、死鎖 (重點)
有這樣的情況:一個線程需要 同時獲取多把鎖,這時就容易發生死鎖
如:線程1獲取A對象鎖, 線程2獲取B對象鎖; 此時線程1又想獲取B對象鎖, 線程2又想獲取A對象鎖; 它們都等着對象釋放鎖, 此時就稱為死鎖
public static void main(String[] args) { final Object A = new Object(); final Object B = new Object(); new Thread(()->{ synchronized (A) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (B) { } } }).start(); new Thread(()->{ synchronized (B) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (A) { } } }).start(); }
發生死鎖的必要條件 (重點)
互斥條件
在一段時間內,一種資源只能被一個進程所使用
請求和保持條件
進程已經擁有了至少一種資源,同時又去申請其他資源。因為其他資源被別的進程所使用,該進程進入阻塞狀態,並且不釋放自己已有的資源
不可搶占條件
進程對已獲得的資源在未使用完成前不能被強占,只能在進程使用完后自己釋放
循環等待條件
發生死鎖時,必然存在一個進程——資源的循環鏈。
定位死鎖的方法
方式一、JPS + JStack 進程ID
- jps先找到JVM進程
- jstack 進程ID
在Java控制台中的Terminal中輸入 jps 指令可以查看正在運行中的進程ID,使用 jstack 進程ID 可以查看進程狀態。
方式二、 jconsole檢測死鎖
打開jconsole,連接到死鎖程序的線程
死鎖舉例 - 哲學家就餐問題 (重點)
有五位哲學家,圍坐在圓桌旁。
他們只做兩件事,思考和吃飯,思考一會吃口飯,吃完飯后接着思考。
吃飯時要用兩根筷子吃,桌上共有 5 根筷子,每位哲學家左右手邊各有一根筷子。
如果筷子被身邊的人拿着,自己就得等待
當每個哲學家即線程持有一根筷子時,他們都在等待另一個線程釋放鎖,因此造成了死鎖。
public class TestDeadLock { public static void main(String[] args) { Chopstick c1 = new Chopstick("1"); Chopstick c2 = new Chopstick("2"); Chopstick c3 = new Chopstick("3"); Chopstick c4 = new Chopstick("4"); Chopstick c5 = new Chopstick("5"); new Philosopher("蘇格拉底", c1, c2).start(); new Philosopher("柏拉圖", c2, c3).start(); new Philosopher("亞里士多德", c3, c4).start(); new Philosopher("赫拉克利特", c4, c5).start(); new Philosopher("阿基米德", c5, c1).start(); } } @Slf4j(topic = "c.Philosopher") class Philosopher extends Thread { Chopstick left; Chopstick right; public Philosopher(String name, Chopstick left, Chopstick right) { super(name); this.left = left; this.right = right; } @Override public void run() { while (true) { // 嘗試獲得左手筷子 synchronized (left) { // 嘗試獲得右手筷子 synchronized (right) { eat(); } } } } Random random = new Random(); private void eat() { log.debug("eating..."); Sleeper.sleep(0.5); } } class Chopstick { String name; public Chopstick(String name) { this.name = name; } @Override public String toString() { return "筷子{" + name + '}'; } }
通過jps, jstack 進程id
查看死鎖原因
Found one Java-level deadlock:
發現了一個Java級別的死鎖
Found one Java-level deadlock: ============================= "阿基米德": waiting to lock monitor 0x000000001fd941a8 (object 0x000000076b735028, a cn.itcast.n4.deadlock.v1.Chopstick), which is held by "蘇格拉底" "蘇格拉底": waiting to lock monitor 0x000000001ccd33c8 (object 0x000000076b735068, a cn.itcast.n4.deadlock.v1.Chopstick), which is held by "柏拉圖" "柏拉圖": waiting to lock monitor 0x000000001ccd3318 (object 0x000000076b7350a8, a cn.itcast.n4.deadlock.v1.Chopstick), which is held by "亞里士多德" "亞里士多德": waiting to lock monitor 0x000000001ccd0a88 (object 0x000000076b7350e8, a cn.itcast.n4.deadlock.v1.Chopstick), which is held by "赫拉克利特" "赫拉克利特": waiting to lock monitor 0x000000001ccd0b38 (object 0x000000076b735128, a cn.itcast.n4.deadlock.v1.Chopstick), which is held by "阿基米德" Java stack information for the threads listed above: =================================================== "阿基米德": at cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41) - waiting to lock <0x000000076b735028> (a cn.itcast.n4.deadlock.v1.Chopstick) - locked <0x000000076b735128> (a cn.itcast.n4.deadlock.v1.Chopstick) "蘇格拉底": at cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41) - waiting to lock <0x000000076b735068> (a cn.itcast.n4.deadlock.v1.Chopstick) - locked <0x000000076b735028> (a cn.itcast.n4.deadlock.v1.Chopstick) "柏拉圖": at cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41) - waiting to lock <0x000000076b7350a8> (a cn.itcast.n4.deadlock.v1.Chopstick) - locked <0x000000076b735068> (a cn.itcast.n4.deadlock.v1.Chopstick) "亞里士多德": at cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41) - waiting to lock <0x000000076b7350e8> (a cn.itcast.n4.deadlock.v1.Chopstick) - locked <0x000000076b7350a8> (a cn.itcast.n4.deadlock.v1.Chopstick) "赫???克利特": at cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41) - waiting to lock <0x000000076b735128> (a cn.itcast.n4.deadlock.v1.Chopstick) - locked <0x000000076b7350e8> (a cn.itcast.n4.deadlock.v1.Chopstick) Found 1 deadlock.
避免死鎖的方法
- 在線程使用鎖對象時, 采用固定加鎖的順序, 可以使用Hash值的大小來確定加鎖的先后
- 盡可能縮減加鎖的范圍, 等到操作共享變量的時候才加鎖
- 使用可釋放的定時鎖 (一段時間申請不到鎖的權限了, 直接釋放掉)
2.2 活鎖
活鎖
出現在兩個線程 互相改變對方的結束條件
,誰也無法結束。
@Slf4j(topic = "c.TestLiveLock") public class TestLiveLock { static volatile int count = 10; static final Object lock = new Object(); public static void main(String[] args) { new Thread(() -> { // 期望減到 0 退出循環 while (count > 0) { sleep(0.2); count--; log.debug("count: {}", count); } }, "t1").start(); new Thread(() -> { // 期望超過 20 退出循環 while (count < 20) { sleep(0.2); count++; log.debug("count: {}", count); } }, "t2").start(); } }
避免活鎖的方法
在線程執行時,中途給予 不同的間隔時間, 讓某個線程先結束即可。
死鎖與活鎖的區別
- 死鎖是因為線程互相持有對象想要的鎖,並且都不釋放,最后到時線程阻塞,停止運行的現象。
- 活鎖是因為線程間修改了對方的結束條件,而導致代碼一直在運行,卻一直運行不完的現象。
2.3 飢餓
- 某些線程因為優先級太低,導致一直無法獲得資源的現象。
- 在使用
順序加鎖
時,可能會出現飢餓現象
三、 ReentrantLock (重點)
相對於synchronized,ReentrantLock 所具備的特點
支持鎖重入
可重入鎖是指同一個線程如果首次獲得了這把鎖,那么因為它是這把鎖的擁有者,因此 有權利再次獲取這把鎖
可中斷
lock.lockInterruptibly() : 可以被其他線程打斷的中斷鎖
可以設置超時時間
lock.tryLock(時間) : 嘗試獲取鎖對象, 如果超過了設置的時間, 還沒有獲取到鎖, 此時就退出阻塞隊列, 並釋放掉自己擁有的鎖
可以設置為公平鎖
(先到先得) 默認是非公平, true為公平 new ReentrantLock(true)
支持多個條件變量( 有多個waitset)
(可避免虛假喚醒) - lock.newCondition()創建條件變量對象; 通過條件變量對象調用 await/signal方法, 等待/喚醒
synchronized是關鍵字級別的加鎖,ReentrantLock則是對象級別的,基本語法如下:
//獲取ReentrantLock對象 private ReentrantLock lock = new ReentrantLock(); //加鎖 lock.lock(); try { //需要執行的代碼 }finally { //釋放鎖 lock.unlock(); }
3.1、支持鎖重入
- 可重入鎖是指
同一個線程如果首次獲得了這把鎖
,那么因為它是這把鎖的擁有者
,因此 有權利再次獲取這把鎖 - 如果是不可重入鎖,那么第二次獲得鎖時,自己也會被鎖擋住
@Slf4j(topic = "c.TestReentrant") public class TestReentrant { static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { method1(); } public static void method1() { lock.lock(); try { log.debug("execute method1"); method2(); } finally { lock.unlock(); } } public static void method2() { lock.lock(); try { log.debug("execute method2"); method3(); } finally { lock.unlock(); } } public static void method3() { lock.lock(); try { log.debug("execute method3"); } finally { lock.unlock(); } } }
3.2、可中斷
(針對於lockInterruptibly()方法獲得的中斷鎖) 直接退出阻塞隊列, 獲取鎖失敗
synchronized 和 reentrantlock.lock() 的鎖, 是不可被打斷的; 也就是說別的線程已經獲得了鎖, 我的線程就需要一直等待下去. 不能中斷 可被中斷的鎖, 通過lock.lockInterruptibly()獲取的鎖對象, 可以通過調用阻塞線程的interrupt()方法
如果某個線程處於阻塞狀態,可以調用其interrupt方法讓其停止阻塞,獲得鎖失敗
處於阻塞狀態的線程,被打斷了就不用阻塞了,直接停止運行
可中斷的鎖, 在一定程度上可以被動的減少死鎖的概率, 之所以被動, 是因為我們需要手動調用阻塞線程的interrupt方法;
測試使用lock.lockInterruptibly()可以從阻塞隊列中,打斷
private static void test1() { ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { log.debug("啟動..."); try { lock.lockInterruptibly(); } catch (InterruptedException e) { e.printStackTrace(); log.debug("等鎖的過程中被打斷"); return; } try { log.debug("獲得了鎖"); } finally { lock.unlock(); } }, "t1"); //主線程上鎖 lock.lock(); log.debug("獲得了鎖"); t1.start(); try { sleep(1); t1.interrupt(); log.debug("執行打斷"); } finally { lock.unlock(); } }
3.3、鎖超時 (lock.tryLock())
直接退出阻塞隊列, 獲取鎖失敗
防止無限制等待, 減少死鎖
- 使用 lock.tryLock() 方法會返回獲取鎖是否成功。如果成功則返回true,反之則返回false。
- 並且tryLock方法可以設置指定等待時間,參數為:tryLock(long timeout, TimeUnit unit) , 其中timeout為最長等待時間,TimeUnit為時間單位
獲取鎖的過程中, 如果超過等待時間, 或者被打斷, 就直接從阻塞隊列移除, 此時獲取鎖就失敗了, 不會一直阻塞着 ! (可以用來實現死鎖問題)
不設置等待時間, 立即失敗
@Slf4j(topic = "c.ReentrantTest") public class ReentrantTest { private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { Thread t1 = new Thread(() -> { log.debug("嘗試獲得鎖"); // 此時肯定獲取失敗, 因為主線程已經獲得了鎖對象 if (!lock.tryLock()) { log.debug("獲取立刻失敗,返回"); return; } try { log.debug("獲得到鎖"); } finally { lock.unlock(); } }, "t1"); lock.lock(); log.debug("獲得到鎖"); t1.start(); // 主線程2s之后才釋放鎖 sleep(2); log.debug("釋放了鎖"); lock.unlock(); } }
設置等待時間, 超過等待時間還沒有獲得鎖, 失敗, 從阻塞隊列移除該線程
@Slf4j(topic = "c.ReentrantTest") public class ReentrantTest { private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { Thread t1 = new Thread(() -> { log.debug("嘗試獲得鎖"); try { // 設置等待時間, 超過等待時間 / 被打斷, 都會獲取鎖失敗; 退出阻塞隊列 if (!lock.tryLock(1, TimeUnit.SECONDS)) { log.debug("獲取鎖超時,返回"); return; } } catch (InterruptedException e) { log.debug("被打斷了, 獲取鎖失敗, 返回"); e.printStackTrace(); return; } try { log.debug("獲得到鎖"); } finally { lock.unlock(); } }, "t1"); lock.lock(); log.debug("獲得到鎖"); t1.start(); // t1.interrupt(); // 主線程2s之后才釋放鎖 sleep(2); log.debug("main線程釋放了鎖"); lock.unlock(); } }
超時的打印
中斷的打印
通過lock.tryLock()
來解決, 哲學家就餐
問題 (重點
)
lock.tryLock(時間)
: 嘗試獲取鎖對象, 如果超過了設置的時間, 還沒有獲取到鎖, 此時就退出阻塞隊列, 並釋放掉自己擁有的鎖
@Override public void run() { while (true) { // 獲得了left左手邊筷子 (針對五個哲學家, 它們剛開始肯定都可獲得左筷子) if (left.tryLock()) { try { // 此時發現它的right筷子被占用了, 使用tryLock(), // 嘗試獲取失敗, 此時它就會將自己左筷子也釋放掉 // 臨界區代碼 if (right.tryLock()) {//嘗試獲取右手邊筷子, 如果獲取失敗, 則會釋放左邊的筷子 try { eat(); } finally { right.unlock(); } } } finally { left.unlock(); } } } }
3.4、公平鎖 new ReentrantLock(true)
ReentrantLock默認是非公平鎖, 可以指定為公平鎖。
/** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
在線程獲取鎖失敗,進入阻塞隊列時,先進入的會在鎖被釋放后先獲得鎖。這樣的獲取方式就是公平的。一般不設置ReentrantLock為公平的, 沒必要,會降低並發度
Synchronized底層的Monitor鎖就是不公平的, 和誰先進入阻塞隊列是沒有關系的。
//默認是不公平鎖,需要在創建時指定為公平鎖 ReentrantLock lock = new ReentrantLock(true);
公平鎖與非公平鎖
公平鎖 (new ReentrantLock(true))
- 公平鎖, 可以把競爭的線程放在一個先進先出的阻塞隊列上
- 只要持有鎖的線程執行完了, 喚醒阻塞隊列中的下一個線程獲取鎖即可; 此時先進入阻塞隊列的線程先獲取到鎖
非公平鎖 (synchronized, new ReentrantLock())
- 非公平鎖, 當阻塞隊列中已經有等待的線程A了, 此時后到的線程B, 先去嘗試看能否獲得到鎖對象. 如果獲取成功, 此時就不需要進入阻塞隊列了. 這樣以來后來的線程B就先活的到鎖了
所以公平和非公平的區別 : 線程執行同步代碼塊時, 是否回去嘗試獲取鎖, 如果會嘗試獲取鎖, 那就是非公平的, 如果不會嘗試獲取鎖, 直接進入阻塞隊列, 再等待被喚醒, 那就是公平的
如果不進如隊列呢? 線程一直嘗試獲取鎖不就行了?
一直嘗試獲取鎖, 在synchronized輕量級鎖升級為重量級鎖時, 做的一個優化, 叫做自旋鎖, 一般很消耗資源, cpu一直空轉, 最后獲取鎖也失敗, 所以不推薦使用。在jdk6對於自旋鎖有一個機制, 在重試獲得鎖指定次數就失敗等等
3.5、條件變量
(可避免虛假喚醒) - lock.newCondition()創建條件變量對象; 通過條件變量對象調用await/signal
方法, 等待/喚醒
- Synchronized 中也有條件變量,就是Monitor監視器中的 waitSet等待集合,當條件不滿足時進入waitSet 等待
- ReentrantLock 的條件變量比 synchronized 強大之處在於,它是 支持多個條件變量。
- 這就好比synchronized 是那些不滿足條件的線程都在一間休息室等通知; (此時會造成虛假喚醒), 而 ReentrantLock 支持多間休息室,有專門等煙的休息室、專門等早餐的休息室、喚醒時也是按休息室來喚醒; (可以避免虛假喚醒)
使用要點:
- await 前需要 獲得鎖
- await 執行后,會釋放鎖,進入 conditionObject (條件變量)中等待
- await 的線程被喚醒(或打斷、或超時)取重新競爭 lock ;競爭 lock 鎖成功后,從 await 后繼續執行
- signal 方法用來喚醒條件變量(等待室)匯總的某一個等待的線程
- signalAll方法, 喚醒條件變量(休息室)中的所有線程
ReentrantLock可以設置多個條件變量(多個休息室), 相對於synchronized底層monitor鎖中waitSet
@Slf4j(topic = "c.ConditionVariable") public class ConditionVariable { private static boolean hasCigarette = false; private static boolean hasTakeout = false; private static final ReentrantLock lock = new ReentrantLock(); // 等待煙的休息室 static Condition waitCigaretteSet = lock.newCondition(); // 等外賣的休息室 static Condition waitTakeoutSet = lock.newCondition(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { log.debug("有煙沒?[{}]", hasCigarette); while (!hasCigarette) { log.debug("沒煙,先歇會!"); try { // 此時小南進入到 等煙的休息室 waitCigaretteSet.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("煙來咯, 可以開始干活了"); } finally { lock.unlock(); } }, "小南").start(); new Thread(() -> { lock.lock(); try { log.debug("外賣送到沒?[{}]", hasTakeout); while (!hasTakeout) { log.debug("沒外賣,先歇會!"); try { // 此時小女進入到 等外賣的休息室 waitTakeoutSet.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("外賣來咯, 可以開始干活了"); } finally { lock.unlock(); } }, "小女").start(); sleep(1); new Thread(() -> { lock.lock(); try { log.debug("送外賣的來咯~"); hasTakeout = true; // 喚醒等外賣的小女線程 waitTakeoutSet.signal(); } finally { lock.unlock(); } }, "送外賣的").start(); sleep(1); new Thread(() -> { lock.lock(); try { log.debug("送煙的來咯~"); hasCigarette = true; // 喚醒等煙的小南線程 waitCigaretteSet.signal(); } finally { lock.unlock(); } }, "送煙的").start(); } }
四、同步模式之順序控制 (案例)
- 假如有兩個線程, 線程A打印1, 線程B打印2.
- 要求: 程序先打印2, 再打印1
4.1、Wait/Notify版本實現
里面一些代碼細節參見之前的博客,wait/notify的正確使用:https://www.cnblogs.com/wkfvawl/p/15489569.html#scroller-5
@Slf4j(topic = "c.Test25") public class Test25 { //定義鎖對象 static final Object lock = new Object(); // 表示 t2 是否運行過 static boolean t2runned = false; public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (lock) { //使用while循環來解決虛假喚醒 while (!t2runned) { try { // 進入等待(waitset), 會釋放鎖 lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("1"); } }, "t1"); Thread t2 = new Thread(() -> { synchronized (lock) { log.debug("2"); t2runned = true; lock.notify(); } }, "t2"); t1.start(); t2.start(); } }
4.2、ReentrantLock的await/signal版本實現
@Slf4j(topic = "c.SyncPrintWaitTest") public class SyncPrintWaitTest { public static final ReentrantLock lock = new ReentrantLock(); public static Condition condi tion = lock.newCondition(); // t2線程釋放執行過 public static boolean t2Runned = false; public static void main(String[] args) { Thread t1 = new Thread(() -> { lock.lock(); try { // 臨界區 while (!t2Runned) { try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("1"); } finally { lock.unlock(); } }, "t1"); Thread t2 = new Thread(() -> { lock.lock(); try { log.debug("2"); t2Runned = true; condition.signal(); } finally { lock.unlock(); } }, "t2"); t1.start(); t2.start(); } }
4.3、使用LockSupport中的park/unpart
@Slf4j(topic = "c.SyncPrintWaitTest") public class SyncPrintWaitTest { public static void main(String[] args) { Thread t1 = new Thread(() -> {
// 暫停 LockSupport.park(); log.debug("1"); }, "t1"); t1.start(); new Thread(() -> { log.debug("2");
// 喚醒t1 LockSupport.unpark(t1); }, "t2").start(); } }
五、同步模式之交替輸出
需求
- 線程1 輸出 a 5次, 線程2 輸出 b 5次, 線程3 輸出 c 5次。現在要求輸出 abcabcabcabcabcab
5.1、wait/notify版本
@Slf4j(topic = "c.Test27") public class Test27 { public static void main(String[] args) { // 最開始的等待標記是1 循環次數5次 WaitNotify wn = new WaitNotify(1, 5); new Thread(() -> { wn.print("a", 1, 2); }).start(); new Thread(() -> { wn.print("b", 2, 3); }).start(); new Thread(() -> { wn.print("c", 3, 1); }).start(); } } /* 輸出內容 等待標記 下一個標記 a 1 2 b 2 3 c 3 1 */ class WaitNotify { // 打印 a 1 2 public void print(String str, int waitFlag, int nextFlag) { for (int i = 0; i < loopNumber; i++) { synchronized (this) { while(flag != waitFlag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print(str); // 修改等待標記 讓下一個線程打印 flag = nextFlag; // 喚醒等待線程 this.notifyAll(); } } } // 當前等待標記 private int flag; // 循環次數 private int loopNumber; public WaitNotify(int flag, int loopNumber) { this.flag = flag; this.loopNumber = loopNumber; } }
5.2、await/signal版本
@Slf4j(topic = "c.TestWaitNotify") public class TestAwaitSignal { public static void main(String[] args) throws InterruptedException { AwaitSignal awaitSignal = new AwaitSignal(5); Condition a_condition = awaitSignal.newCondition(); Condition b_condition = awaitSignal.newCondition(); Condition c_condition = awaitSignal.newCondition(); new Thread(() -> { awaitSignal.print("a", a_condition, b_condition); }, "a").start(); new Thread(() -> { awaitSignal.print("b", b_condition, c_condition); }, "b").start(); new Thread(() -> { awaitSignal.print("c", c_condition, a_condition); }, "c").start(); Thread.sleep(1000); System.out.println("==========開始========="); awaitSignal.lock(); try { a_condition.signal(); //首先喚醒a線程 } finally { awaitSignal.unlock(); } } } class AwaitSignal extends ReentrantLock { private final int loopNumber; public AwaitSignal(int loopNumber) { this.loopNumber = loopNumber; } // 參數1 打印內容;參數2 進入那一間休息室;參數3 下一間休息室 public void print(String str, Condition condition, Condition next) { for (int i = 0; i < loopNumber; i++) { //加鎖 繼承自ReentrantLock lock(); try { try { //進入休息室等待 condition.await(); //System.out.print("i:==="+i); System.out.print(str); // 喚醒下一個休息室的線程 next.signal(); } catch (InterruptedException e) { e.printStackTrace(); } } finally { //解鎖 unlock(); } } } }
5.3、LockSupport的park/unpark實現
park和unpark沒有對象鎖的概念了,停止和恢復線程的運行都是以線程自身為單位的,所以實現更為簡單。
@Slf4j(topic = "c.TestWaitNotify") public class TestParkUnpark { static Thread a; static Thread b; static Thread c; public static void main(String[] args) { ParkUnpark parkUnpark = new ParkUnpark(5); a = new Thread(() -> { parkUnpark.print("a", b); }, "a"); b = new Thread(() -> { parkUnpark.print("b", c); }, "b"); c = new Thread(() -> { parkUnpark.print("c", a); }, "c"); a.start(); b.start(); c.start(); //主線程先喚醒a LockSupport.unpark(a); } } class ParkUnpark { private final int loopNumber; public ParkUnpark(int loopNumber) { this.loopNumber = loopNumber; } public void print(String str, Thread nextThread) { for (int i = 0; i < loopNumber; i++) { //當前線程先暫停 LockSupport.park(); System.out.print(str); //喚醒下一個線程 LockSupport.unpark(nextThread); } } }
六、本章小結
本章我們需要重點掌握的是
-
分析多線程訪問共享資源時,哪些代碼片段屬於臨界區
-
-
掌握 synchronized 鎖對象語法
-
掌握 synchronzied 加載成員方法和靜態方法語法
-
掌握 wait/notify 同步方法
-
-
使用 lock 互斥解決臨界區的線程安全問題
-
掌握 lock 的使用細節:可打斷、鎖超時、公平鎖、條件變量
-
-
學會分析變量的線程安全性、掌握常見線程安全類的使用
-
了解線程活躍性問題:死鎖、活鎖、飢餓
-
應用方面
-
互斥:使用 synchronized 或 Lock 達到共享資源互斥效果
-
同步:使用 wait/notify 或 Lock 的條件變量來達到線程間通信效果
-
-
原理方面
-
monitor、synchronized 、wait/notify 原理(monitor是在jvm層面實現的,源碼是c++,java基本的monitor則是ReentrantLock,實現細節二者可以相互參照)
-
synchronized 進階原理
-
park & unpark 原理
-
-
模式方面
-
同步模式之保護性暫停
-
異步模式之生產者消費者
-
同步模式之順序控制
-