一、Wait和Notify
1、原理
Wait和Notify用於等待。其原理爲:
- Owner 線程發現條件不滿足,調用 wait 方法,即可進入 WaitSet 變爲 WAITING 狀態
- WAITING 線程會在 Owner 線程調用 notify 或 notifyAll 時喚醒,但喚醒後並不意味者立刻獲得鎖,仍需進入 EntryList 重新競爭
- BLOCKED 和 WAITING 的線程都處於阻塞狀態,不佔用 CPU 時間片
- BLOCKED 線程會在 Owner 線程釋放鎖時喚醒
2、相關API
obj.wait()
讓進入 object 監視器的線程到 waitSet 等待,注意必須是獲得對象鎖的像鎖的線程才能調用。wait方法會釋放對象的鎖,進入 WaitSet 等待區,從而讓其他線程就機會獲取對象的鎖。無限制等待,直到 notify 爲止obj.wait(long n)
有時限的等待, 到 n 毫秒後結束等待,或是被 notifyobj.notify()
在 object 上正在 waitSet 等待的線程中挑一個喚醒obj.notifyAll()
讓 object 上正在 waitSet 等待的線程全部喚醒
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("執行....");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代碼....");
}
}).start();
new Thread(() -> {
synchronized (obj) {
log.debug("執行....");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代碼....");
}
}).start();
sleep(2);
log.debug("喚醒 obj 上其它線程");
synchronized (obj) {
obj.notify();
// obj.notifyAll();
}
}
notify 的結果:
20:00:53.096 [Thread-0] c.TestWaitNotify - 執行....
20:00:53.099 [Thread-1] c.TestWaitNotify - 執行....
20:00:55.096 [main] c.TestWaitNotify - 喚醒 obj 上其它線程
20:00:55.096 [Thread-0] c.TestWaitNotify - 其它代碼....
notifyAll 的結果:
19:58:15.457 [Thread-0] c.TestWaitNotify - 執行....
19:58:15.460 [Thread-1] c.TestWaitNotify - 執行....
19:58:17.456 [main] c.TestWaitNotify - 喚醒 obj 上其它線程
19:58:17.456 [Thread-1] c.TestWaitNotify - 其它代碼....
19:58:17.456 [Thread-0] c.TestWaitNotify - 其它代碼....
3、wait和sleep的異同
- 它們 狀態都是 TIMED_WAITING
- sleep 是 Thread 方法,而 wait 是 Object 的方法
- sleep 不需要強制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用
- sleep 在睡眠的同時,不會釋放對象鎖的,但 wait 在等待的時候會釋放對象鎖
4、wait/notify使用
synchronized(lock){
while (條件不成立) { //方法之虛假喚醒
lock.wait(); //進入等待狀態
}
}
synchronized(lock){
lock.notifyAll(); //喚醒所有,再通過條件判斷喚醒的是否是自己
}
二、同步設計模式之保護性暫停
1、定義
保護性暫停即 Guarded Suspension,用在一個線程等待另一個線程的執行結果
- 有一個結果需要從一個線程傳遞到另一個線程,讓他們關聯同一個 GuardedObject
- 如果有結果不斷從一個線程到另一個線程那麼可以使用消息隊列(見生產者/消費者)
- JDK 中,join 的實現、Future 的實現,採用的就是此模式
- 因爲要等待另一方的結果,因此屬於同步模式
2、實現
該模式的實現主要依靠了wait和notifyAll方法,和上面的使用類似
/**
* 保護性暫停設計模式實現
*/
class GuardedObject {
private Object response;
private final Object lock = new Object();
public Object get(){
synchronized (lock){
while (response == null){
try {
lock.wait(); //等待喚醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return response;
}
public void put(Object obj){
synchronized (lock){
response = obj;
lock.notifyAll(); //喚醒
}
}
}
測試:
public static void main(String[] args) {
GuardedObject guarded = new GuardedObject();
//t1線程等待response結果
new Thread(()->{
System.out.println("t1等待response");
Object obj = guarded.get();
System.out.println("t1獲得response");
System.out.println("response = " + (String)obj);
}, "t1").start();
//t2線程設置response結果
new Thread(()->{
try {
Thread.sleep(2000);
System.out.println("t2設置response");
guarded.put("123");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
}
結果:
t1等待response
t2設置response
t1獲得response
response = 123
3、帶有超時效果的保護性暫停
設置等待一段時間後仍沒有收到response就自動喚醒
/**
* 保護性暫停設計模式實現
*/
class GuardedObject {
private Object response;
private final Object lock = new Object();
public Object get(long timeout){
synchronized (lock){
long begin = System.currentTimeMillis(); //開始時間
long timePassed = 0;
while (response == null){
long lastTime = timeout - timePassed; //剩餘時間
if(lastTime <= 0)
break;
try {
lock.wait(lastTime); //等待喚醒
} catch (InterruptedException e) {
e.printStackTrace();
}
timePassed = System.currentTimeMillis() - begin; //已過去的時間
}
}
return response;
}
public void put(Object obj){
synchronized (lock){
response = obj;
lock.notifyAll(); //喚醒
}
}
}
4、Join的原理
Join的實現原理其就是我們上面的帶有超時效果的保護性暫停,其中只有兩點不同:
- 當超時時間millis爲0時,表示一直等待,沒有超時時間
- 環形的條件不是獲取某個值,而是線程結束
下面是join方法的源碼
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) { //如果millis=0,表示不設置超時時間
while (isAlive()) { //判斷線程是否存活
wait(0);
}
} else {
while (isAlive()) { //判斷線程是否存活
long delay = millis - now; //剩餘時間
if (delay <= 0) {
break;
}
wait(delay); //等待delay時間
now = System.currentTimeMillis() - base; //已經過的時間
}
}
}
5、多任務版 GuardedObject
引入:圖中 Futures 就好比居民樓一層的信箱(每個信箱有房間編號),左側的 t0,t2,t4 就好比等待郵件的居民,右 側的 t1,t3,t5 就好比郵遞員
分析:如果需要在多個類之間使用 GuardedObject 對象,作爲參數傳遞不是很方便,因此設計一個用來解耦的中間類, 這樣不僅能夠解耦【結果等待者】和【結果生產者】,還能夠同時支持多個任務的管理。
實現:
新增 id 用來標識 Guarded Object
class GuardedObject {
//多個GuardedObject時用於標識
private int id;
private Object response;
public GuardedObject(int id) {
this.id = id;
}
public Object get(long timeout){
synchronized (this){
long begin = System.currentTimeMillis(); //開始時間
long timePassed = 0;
while (response == null){
long lastTime = timeout - timePassed; //剩餘時間
if(lastTime <= 0)
break;
try {
this.wait(lastTime); //等待喚醒
} catch (InterruptedException e) {
e.printStackTrace();
}
timePassed = System.currentTimeMillis() - begin; //已過去的時間
}
}
return response;
}
public void put(Object obj){
synchronized (this){
response = obj;
this.notifyAll(); //喚醒
}
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
中間解耦類:使用線程安全的Map來存儲GuardedObject,用於解耦
class MailBoxs{
private static Map<Integer, GuardedObject> map = new Hashtable();
private static int i = 1;
//生成唯一id
private static synchronized int generateId(){
return i++;
}
//生成GuardedObject
public static GuardedObject createGuardedObject(){
GuardedObject go = new GuardedObject(generateId());
map.put(go.getId(), go);
return go;
}
public static GuardedObject getGuardedObject(int i){
return map.remove(i);
}
public static Set<Integer> getIds(){
return map.keySet();
}
}
業務相關類
//收信人
class People extends Thread{
@Override
public void run() {
GuardedObject go = MailBoxs.createGuardedObject();
System.out.println("開始收信" + go.getId());
go.get(20000);
System.out.println("收到信" + go.getId());
}
}
//送信人
class Postman extends Thread{
private int id;
private String mail;
public Postman(int id, String mail) {
this.id = id;
this.mail = mail;
}
@Override
public void run() {
GuardedObject go = MailBoxs.getGuardedObject(id);
System.out.println("開始送信" + go.getId() + "內容爲" + mail);
go.put(mail);
}
}
測試
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++){
new People().start();
}
Thread.sleep(3000);
for (int id : MailBoxs.getIds()){
new Postman(id, "內容"+id).start();
}
}
結果
開始收信1
開始收信2
開始收信3
開始送信2內容爲內容2
開始送信3內容爲內容3
收到信2
收到信3
開始送信1內容爲內容1
收到信1
三、異步設計模式之生產者/消費者
1、定義
要點:用於線程間通信的一種異步模式
- 與前面的保護性暫停中的 GuardObject 不同,不需要產生結果和消費結果的線程一一對應
- 消費隊列可以用來平衡生產和消費的線程資源
- 生產者僅負責產生結果數據,不關心數據該如何處理,而消費者專心處理結果數據
- 消息隊列是有容量限制的,滿時不會再加入數據,空時不會再消耗數據
- JDK 中各種阻塞隊列,採用的就是這種模式
2、實現
消息類,包含id和消息體
//線程安全的消息類
final class Massage{
private int id;
private Object value;
//沒有set方法,只能創建時初始化
public Massage(int id, Object value) {
this.id = id;
this.value = value;
}
public int getId() {
return id;
}
public Object getValue() {
return value;
}
@Override
public String toString() {
return "Massage{" +
"id=" + id +
", value=" + value +
'}';
}
}
異步消息隊列實現
class MassageQueue{
//消息隊列
private LinkedList<Massage> queue = new LinkedList<>();
//消息隊列的容量
private int capcity;
public MassageQueue(int capcity) {
this.capcity = capcity;
}
//消費者消費消息
public Massage get(){
synchronized (queue){
//檢查隊列是否爲空
while (queue.isEmpty()) {
try {
System.out.println("消費隊列已空");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//從隊列的頭部獲取元素返回
Massage massage = queue.removeFirst();
System.out.println("消費者消息"+massage.getId());
queue.notifyAll();
return massage;
}
}
//生產者者生產消息
public void put(Massage massage){
synchronized (queue) {
//檢查隊列是否已滿
while (queue.size() >= capcity) {
try {
System.out.println("消息隊列是否已滿");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "生產消息"+massage.getId());
queue.addLast(massage);
queue.notifyAll();
}
}
}
測試:創建了三個生產者,一個消費者,消息隊列的容量爲2
public static void main(String[] args) {
MassageQueue queue = new MassageQueue(2);
for (int i = 1; i <= 3; i++) {
int finalI = i;
new Thread(()->{
queue.put(new Massage(finalI, "消息"+finalI));
}, "生產者"+i).start();
}
new Thread(()->{
while (true){
try {
Thread.sleep(1000);
Massage massage = queue.get();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消費者").start();
}
結果:
生產者3生產消息3
生產者2生產消息2
消息隊列是否已滿
消費者消息3
生產者1生產消息1
消費者消息2
消費者消息1
消費隊列已空
四、 Park和 Unpark
它們是 LockSupport 類中的方法,用於暫停和喚醒線程
// 暫停當前線程
LockSupport.park();
// 恢復某個線程的運行
LockSupport.unpark(暫停線程對象);
先pack再unpack
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(1);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);
結果:
18:42:52.585 c.TestParkUnpark [t1] - start...
18:42:53.589 c.TestParkUnpark [t1] - park...
18:42:54.583 c.TestParkUnpark [main] - unpark...
18:42:54.583 c.TestParkUnpark [t1] - resume...
先unpack再pack
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(2);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
結果:同樣可以解鎖,沒有順序要求
18:43:50.765 c.TestParkUnpark [t1] - start...
18:43:51.764 c.TestParkUnpark [main] - unpark...
18:43:52.769 c.TestParkUnpark [t1] - park...
18:43:52.769 c.TestParkUnpark [t1] - resume...
特點
與 Object 的 wait & notify 相比
- wait,notify 和 notifyAll 必須配合 Object Monitor 一起使用(必須使用synchronized加鎖),而 park,unpark 不必
- park & unpark 是以線程爲單位來【阻塞】和【喚醒】線程,而 notify 只能隨機喚醒一個等待線程,notifyAll 是喚醒所有等待線程,就不那麼【精確】
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
park unpark 原理
每個線程都有自己的一個 Parker 對象,由三部分組成 _counter , _cond 和 _mutex
1、當前線程調用 Unsafe.park() 方法時(先於unpark)
-
(2)檢查 _counter ,如果爲 0,獲得 _mutex 互斥鎖
-
(3)線程進入 _cond 條件變量阻塞
-
(4)設置 _counter = 0
2、 調用 Unsafe.unpark(Thread_0) 方法 -
(1)調用 Unsafe.unpark(Thread_0) 方法,設置 _counter 爲 1
-
(2)喚醒 _cond 條件變量中的 Thread_0
-
(3) Thread_0 恢復運行
-
(4) 設置 _counter 爲 0
3、先調用 調用 Unsafe.unpark(Thread_0) 方法,再調用park方法
- (1)調用 Unsafe.unpark(Thread_0) 方法,設置 _counter 爲 1
- (2)當前線程調用 Unsafe.park() 方法
- (3)檢查 _counter ,本情況爲 1,這時線程無需阻塞,繼續運行
- (4)設置 _counter 爲 0
五、重新理解線程狀態轉換 ★
Java線程轉換如下圖所示:
1、NEW–>RUNABLE
- 當調用
t.start()
方法時,由NEW --> RUNNABLE
2、RUNABL<–>WATING
t 線程用 synchronized(obj)
獲取了對象鎖後
- 調用
obj.wait()
方法時,t 線程從RUNNABLE --> WAITING
- 調用
obj.notify()
,obj.notifyAll()
,t.interrupt()
時- 如果競爭失敗,t 線程從
WAITING --> RUNNABLE
,線程進入Monitor中EntryList - 如果競爭成功,t 線程從
WAITING --> BLOCKED
,Monitor中Owner指向該線程
- 如果競爭失敗,t 線程從
3、RUNNABLE <–> WAITING
- 當前線程調用
t.join()
方法時,當前線程從RUNNABLE --> WAITING
- 注意是當前線程在t 線程對象的監視器上等待
- t 線程運行結束,或調用了當前線程的
interrupt()
時,當前線程從WAITING --> RUNNABLE
4、RUNNABLE <–> WAITING
- 當前線程調用
LockSupport.park()
方法會讓當前線程從RUNNABLE --> WAITING
- 調用
LockSupport.unpark(目標線程)
或調用了線程 的interrupt()
,會讓目標線程從WAITING --> RUNNABLE
5、RUNNABLE <–> TIMED_WAITING
t 線程用 synchronized(obj)
獲取了對象鎖後
- 調用
obj.wait(long n)
方法時,t 線程從RUNNABLE --> TIMED_WAITING
- t 線程等待時間超過了 n 毫秒,或調用
obj.notify()
,obj.notifyAll()
,t.interrupt()
時- 競爭鎖成功,t 線程從
TIMED_WAITING --> RUNNABLE
- 競爭鎖失敗,t 線程從
TIMED_WAITING --> BLOCKED
- 競爭鎖成功,t 線程從
6、 RUNNABLE <–> TIMED_WAITING
- 當前線程調用
t.join(long n)
方法時,當前線程從RUNNABLE --> TIMED_WAITING
- 注意是當前線程在t 線程對象的監視器上等待
- 當前線程等待時間超過了 n 毫秒,或t 線程運行結束,或調用了當前線程的
interrupt()
時,當前線程從TIMED_WAITING --> RUNNABLE
7、RUNNABLE <–> TIMED_WAITING
-
當前線程調用
Thread.sleep(long n)
,當前線程從RUNNABLE --> TIMED_WAITING
-
當前線程等待時間超過了 n 毫秒,當前線程從
TIMED_WAITING --> RUNNABLE
8、RUNNABLE <–> TIMED_WAITING
- 當前線程調用
LockSupport.parkNanos(long nanos)
或LockSupport.parkUntil(long millis)
時,當前線程從RUNNABLE --> TIMED_WAITING
- 調用
LockSupport.unpark(目標線程)
或調用了線程 的interrupt()
,或是等待超時,會讓目標線程從TIMED_WAITING--> RUNNABLE
9、 RUNNABLE <–> BLOCKED
- t 線程用
synchronized(obj)
獲取了對象鎖時如果競爭失敗,從RUNNABLE --> BLOCKED
- 持 obj 鎖線程的同步代碼塊執行完畢,會喚醒該對象上所有 BLOCKED 的線程重新競爭,如果其中 t 線程競爭 成功,從
BLOCKED --> RUNNABLE
,其它失敗的線程仍然 BLOCKED
10、RUNNABLE <–> TERMINATED
- 當前線程所有代碼運行完畢,進入 TERMINATED
六、多把鎖
將鎖的粒度細分
- 好處,是可以增強併發度
- 壞處,如果一個線程需要同時獲得多把鎖,就容易發生死鎖
七、活躍性
活躍性關注的是“某件正確的事情最終會發生”。
例如,如果A線程等待B線程釋放其持有的資源,而B線程永遠都不釋放該資源,那麼線程A就會永遠的等待下去。這樣就不具備活躍性。
1、死鎖
一個線程需要同時獲取多把鎖,這時就容易發生死鎖,例如:t1 線程 獲得 A對象 鎖,接下來想獲取 B對象 的鎖 。t2 線程 獲得 B對象 鎖,接下來想獲取 A對象 的鎖 如:
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
上面的代碼永遠都不會執行完畢,因爲發生了死鎖。
2、定位死鎖
檢測死鎖可以使用 jconsole工具,或者使用 jps 定位進程 id,再用 jstack 定位死鎖
首先使用jps查看進程ID
再使用jstack <進程ID>
,查看當前的狀態:
同樣使用jconsole可以查看當前是否存在死鎖:連接當前進程,點擊線程---->檢測死鎖。
就當能看到當前死鎖的信息
3、活鎖
活鎖出現在兩個線程互相改變對方的結束條件,後誰也無法結束,例如
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
while (count > 0) {
try {
Thread.sleep(200);
count--;
System.out.println("t1 count: " + count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
new Thread(() -> {
while (count < 20) {
try {
Thread.sleep(200);
count++;
System.out.println("t2 count: " + count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t2").start();
}
上面程序雖然沒有互相持有資源,但是仍然會一直運行下去,這是因爲產生了活鎖。
在併發應用程序中,通過等待隨機長度的時間或者回退可以有效避免活鎖的發生。
4、飢餓
很多教程中把飢餓定義爲,一個線程由於優先級太低,始終得不到 CPU 調度執行,也不能夠結束,飢餓的情況不 易演示,講讀寫鎖時會涉及飢餓問題
先來看看使用順序加鎖的方式解決之前的死鎖問題
順序加鎖的解決方案
但是順序加鎖很有可能產生飢餓。
八、ReentrantLock
1、引入:哲學家進餐問題:
有五位哲學家,圍坐在圓桌旁。
- 他們只做兩件事,思考和喫飯,思考一會喫口飯,喫完飯後接着思考。
- 喫飯時要用兩根筷子喫,桌上共有 5 根筷子,每位哲學家左右手邊各有一根筷子。
- 如果筷子被身邊的人拿着,自己就得等待
哲學家進餐進餐問題如果不加於干預就很容易產生死鎖問題,如果使用順序加鎖的方法解決死鎖又很容易產生飢餓的現象。這時候就需要使用ReentrantLock來解決
2、介紹
相對於 synchronized 它具備如下特點
- 可中斷
- 可以設置超時時間
- 可以設置爲公平鎖
- 支持多個條件變量
與 synchronized 一樣,都支持可重入
基本語法:
// 獲取鎖
reentrantLock.lock();
try {
// 臨界區
} finally {
// 釋放鎖
reentrantLock.unlock();
}
3、可重入
可重入是指同一個線程如果首次獲得了這把鎖,那麼因爲它是這把鎖的擁有者,因此有權利再次獲取這把鎖
如果是不可重入鎖,那麼第二次獲得鎖時,自己也會被鎖擋住
示例:
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
m1();
}
public static void m1(){
lock.lock();
try {
System.out.println("m1 in");
m2();
} finally {
lock.unlock();
}
}
public static void m2() {
lock.lock();
try {
System.out.println("m2 in");
} finally {
lock.unlock();
}
}
結果如下圖所示,m1和m2中都對同一個ReentrantLock對象加了鎖,說明可能重入沒有問題。
4、可打斷性
可打斷表示加鎖時如果失敗進入阻塞隊列,則可以進行打斷,可以使用ReentrantLock::lockInterruptibly()
方法設置,示例:
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
System.out.println("嘗試加鎖");
lock.lockInterruptibly();//設置可打斷鎖
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("加鎖失敗,被打斷");
return;
}
try {
System.out.println("獲得鎖");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
Thread.sleep(1000);
System.out.println("打斷加鎖");
t1.interrupt();
}
結果:
5、鎖超時
鎖超時是指嘗試加鎖時,如果超過一段時間仍然獲得不到鎖就會自動放棄加鎖,可以使用使用ReentrantLock::trylock()
方法設置,示例:
1)立即失敗
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
System.out.println("t1嘗試獲得鎖");
if (!lock.tryLock()) {
System.out.println("獲得鎖失敗, 返回");
return;
}
try {
System.out.println("t1獲得鎖成功");
} finally {
lock.unlock();
}
}, "t1");
System.out.println("main獲得鎖");
lock.lock();
t1.start();
}
結果:
2)超時失敗
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
System.out.println("t1嘗試獲得鎖");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
System.out.println("獲得鎖失敗, 返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("被打斷,返回");
return;
}
try {
System.out.println("t1獲得鎖成功");
} finally {
lock.unlock();
}
}, "t1");
System.out.println("main獲得鎖");
lock.lock();
t1.start();
Thread.sleep(2000);
lock.unlock();
}
結果:
6、解決哲學家進餐問題
使用synchronized時會出現死鎖
可以更改爲使用ReentrantLock::trylock()
方法,這樣就不會出現場死鎖
7、公平鎖
這裏的公平是指阻塞隊列(EntryList)中的線程按照先進先出的順序獲得鎖,ReentrantLock和Synchronized 默認是不公平的,也就是阻塞隊列中的線程通過爭搶的方式獲得鎖。
ReentrantLock可以使用通過構造函數ReentrantLock lock = new ReentrantLock(false);
設置爲公平鎖,公平鎖一般沒有必要,會降低併發度,後面分析原理時會講解
8、條件變量
synchronized 中也有條件變量,就是我們講原理時那個 waitSet 休息室,當條件不滿足時進入 waitSet 等待 ReentrantLock 的條件變量比 synchronized 強大之處在於,它是支持多個條件變量的,這就好比
- synchronized 是那些不滿足條件的線程都在一間休息室等消息
- 而 ReentrantLock 支持多間休息室,有專門等煙的休息室、專門等早餐的休息室、喚醒時也是按休息室來喚 醒
使用要點:
- await 前需要獲得鎖 await 執行後,會釋
- 放鎖,進入 conditionObject 等待
- await 的線程被喚醒(或打斷、或超時)取重新競爭 lock 鎖
- 競爭 lock 鎖成功後,從 await 後繼續執行
語法:
ReentrantLock lock = new ReentrantLock(); //創建ReentrantLock對象
Condition condition = lock.newCondition();//創建條件變量對象
new Thread(()->{
lock.lock(); //加鎖
try {
condition.await(); //condition條件不滿足,進入等待隊列。與wait對應
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
Thread.sleep(1000);
condition.signal(); //喚醒等待condition條件變量的線程,與Notify對應
九、同步模式之順序控制
1、固定運行順序
比如,必須先 2 後 1 打印
1)wait notify 版
//用來同步的對象
static Object obj = new Object();
// t2 運行標記, 代表 t2 是否執行過
static boolean t2runed = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (obj) {
// 如果 t2 沒有執行過
while (!t2runed) {
try {
// t1 先等一會
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(1);
});
Thread t2 = new Thread(() -> {
System.out.println(2);
synchronized (obj) {
//修改運行標記
t2runed = true;
// 通知 obj 上等待的線程(可能有多個,因此需要用 notifyAll)
obj.notifyAll();
}
});
t1.start();
t2.start();
}
2)park unpark版
可以看到,上面的實現上很麻煩:
- 首先,需要保證先 wait 再 notify,否則 wait 線程永遠得不到喚醒。因此使用了『運行標記』來判斷該不該 wait
- 第二,如果有些干擾線程錯誤地 notify 了 wait 線程,條件不滿足時還要重新等待,使用了 while 循環來解決 此問題
- 最後,喚醒對象上的 wait 線程需要使用 notifyAll,因爲『同步對象』上的等待線程可能不止一個
park 和 unpark 方法比較靈活,他倆誰先調用,誰後調用無所謂。並且是以線程爲單位進行『暫停』和『恢復』, 不需要『同步對象』和『運行標記』
可以使用 LockSupport 類的 park 和 unpark 來簡化上面的題目:
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("1");
LockSupport.park(); //暫停等待
}, "t1");
t1.start();
new Thread(()->{
System.out.println("2");
LockSupport.unpark(t1); //喚醒t1
}, "t2").start();
}
2、交替輸出
線程 1 輸出 a 5 次,線程 2 輸出 b 5 次,線程 3 輸出 c 5 次。現在要求輸出 abcabcabcabcabc 怎麼實現
1) wait notify 版
打印控制類,用flag變量控制本次應該打印的a,loopNums控制打印次數
class SyncWaitNotify{
//用於標記本次需要喚醒線程
private int flag;
//打印次數
private int loopNums;
public SyncWaitNotify(int flag, int loopNums) {
this.flag = flag;
this.loopNums = loopNums;
}
public void print(String msg, int flag){
for (int i = 0; i < loopNums; i++) {
synchronized (this) {
while (this.flag != flag){ //如果自己不滿足條件,就一直等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(msg);
this.flag = (flag + 1) % 3; //設置下一次喚醒的flag
this.notifyAll(); //喚醒所有
}
}
}
}
測試:
public static void main(String[] args) {
SyncWaitNotify waitNotify = new SyncWaitNotify(0, 5);
new Thread(()->{
waitNotify.print("a", 0);
}).start();
new Thread(()->{
waitNotify.print("b", 1);
}).start();
new Thread(()->{
waitNotify.print("c", 2);
}).start();
}
結果:
2)、Await和Signal實現
使用Condition作爲條件變量,使用await和Signal用於等待和喚醒
class SyncAwaitSignal extends ReentrantLock {
private int loopNums;
public SyncAwaitSignal(int loopNums) {
this.loopNums = loopNums;
}
public void print(String msg, Condition current, Condition next){
for (int i = 0; i < loopNums; i++) {
lock();
try {
//等待信號
current.await();
//喚醒成功執行輸出,並喚醒下一個
System.out.print(msg);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
unlock();
}
}
}
}
測試:
public static void main(String[] args) throws InterruptedException {
SyncAwaitSignal awaitSignal = new SyncAwaitSignal(5);
Condition condition_a = awaitSignal.newCondition();
Condition condition_b = awaitSignal.newCondition();
Condition condition_c = awaitSignal.newCondition();
new Thread(()->{
awaitSignal.print("a", condition_a, condition_b);
}).start();
new Thread(()->{
awaitSignal.print("b", condition_b, condition_c);
}).start();
new Thread(()->{
awaitSignal.print("c", condition_c, condition_a);
}).start();
Thread.sleep(1000);
System.out.println("開始");
//運行開始時,給喚醒a
awaitSignal.lock();
try {
condition_a.signal();
} finally {
awaitSignal.unlock();
}
}
結果:
3)、 Park Unpark
class SyncPackUnPack {
private int loopNums;
public SyncPackUnPack(int loopNums) {
this.loopNums = loopNums;
}
public void print(String msg, Thread next){
for (int i = 0; i < loopNums; i++) {
//等待喚醒
LockSupport.park();
//輸出並喚醒下一個線程
System.out.print(msg);
LockSupport.unpark(next);
}
}
}
測試:
static Thread t1, t2, t3;
public static void main(String[] args) {
SyncPackUnPack packUnPack = new SyncPackUnPack(5);
t1 = new Thread(()->{
packUnPack.print("a", t2);
});
t2 = new Thread(()->{
packUnPack.print("b", t3);
});
t3 = new Thread(()->{
packUnPack.print("c", t1);
});
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
}
結果: