面試手撕併發演算法題 固定列印順序 使用 wait-notify 實現以下功能:先列印 b,再列印 a 思路一 線程t1和t2同時運行,t1中列印 a,t2中列印 b,但 t1 列印得有個前提,就是 t1要在t2運行完釋放鎖了才能列印 a。如果t1先得到鎖,但t2沒有執行,還是得釋放鎖,讓t2得到鎖先 ...
面試手撕併發演算法題
固定列印順序
使用 wait-notify 實現以下功能:先列印 b,再列印 a
思路一
線程t1和t2同時運行,t1中列印 a,t2中列印 b,但 t1 列印得有個前提,就是 t1要在t2運行完釋放鎖了才能列印 a。如果t1先得到鎖,但t2沒有執行,還是得釋放鎖,讓t2得到鎖先列印b,t2執行完,喚醒t1再列印a,實現固定順序列印。
@Slf4j
public class OrderPrint {
static Object obj = new Object();
static boolean is2Run = false; // t2 運行標記, 代表 t2 是否執行過
public static void main(String[] args) {
m1();
}
private static void m1() {
Thread t1 = new Thread(() -> {
synchronized (obj) {
// 如果 t2 沒有執行過
while (!is2Run) {
try {
// t1 先等一會,釋放鎖
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
log.debug("a");
}, "t1");
Thread t2 = new Thread(() -> {
log.debug("b");
synchronized (obj) {
is2Run = true; // syn 自帶可見性
obj.notifyAll(); // 通知 obj 上等待的線程
}
}, "t2");
t1.start();
t2.start();
}
}
思路二
思路一有以下幾個問題需要註意:
- 需要保證先 wait 再 notify,否則 wait 線程永遠得不到喚醒。因此使用了『運行標記』來判斷該不該 wait;
- 如果有些干擾線程錯誤地 notify 了 wait 線程,條件不滿足時還要重新等待,因此使用了 while 迴圈來解決此問題;
可以使用 LockSupport 類的 park 和 unpark 來簡化上面的題目:
private static void m2() {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 線程暫停
LockSupport.park();
log.debug("a");
}, "t1");
Thread t2 = new Thread(() -> {
log.debug("b");
// 喚醒線程
LockSupport.unpark(t1);
}, "t2");
t1.start();
t2.start();
}
park 和 unpark 比較靈活,有以下特點:
- park 和 unpark 不需要先獲取鎖,這一點和Object中的
wait-notify
,Condition介面提供的await-signal
都不同。- 喚醒方法 unpark 在 等待方法 park 之前或之後運行,線程都能夠被喚醒,這一點其他兩種機制都不行,Object 和 Condition 中的喚醒必須在等待之後調用,線程才能被喚醒;
交替列印
準備三個線程t1、t2和t3,其中 t1 線程列印a,t2 線程列印 b,t3 線程列印 c,交替列印 ABCABC,列印100個字元。
思路:可以使用一個狀態變數來表示列印abc,如 state=1 代表列印 a, state=2代表列印 b,state=3代表列印 c;使用synchronized 加鎖。
@Slf4j
public class AlternatePrint {
// 初始狀態為1
private static int state = 1;
private static int loopNumber = 5;
private static int count;
private static Object obj = new Object();
public static void print(int waitState, int nextState, String str) {
while (true) {
synchronized (obj) {
// 當前獲得鎖的線程狀態不是 state,等待釋放鎖
while (state != waitState) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果 count 達到了100,進入if的線程修改為下一狀態,並喚醒另兩個線程,本線程結束
// 另兩個線程爭奪鎖,最終都會執行結束(可以分析一波)
if(count == 100) {
state = nextState; // syn 保證可見性
obj.notifyAll();
break;
}
// 當前獲得鎖的線程狀態就是 state,列印要列印的字元串
log.debug(str);
count++;
// 列印完,輪到下一個線程列印了,把狀態改成 nextState
state = nextState; // syn 保證可見性
obj.notifyAll();
}
}
}
public static void main(String[] args) {
// t1線程,如果當前狀態是1,就列印a,下一個線程的狀態是2
new Thread(() -> {
print(1, 2, "a");
}, "t1").start();
// t2線程,如果當前狀態是2,就列印b,下一個線程的狀態是3
new Thread(() -> {
print(2, 3, "b");
}, "t2").start();
// t3線程,如果當前狀態是3,就列印c,下一個線程的狀態是1
new Thread(() -> {
print(3, 1, "c");
}, "t3").start();
}
}
交替列印變式
準備三個線程t1、t2和t3,其中 t1 線程列印a,t2 線程列印 b,t3 線程列印 c,交替列印 abcabc...
思路一:wait - notify
定義一個全局變數 state
來實現按順序列印,使用wait - notify
機制來處理條件不滿足時等待釋放鎖,滿足時列印並喚醒所有處於等待的線程;
@Slf4j
public class AlternatePrint {
// 初始狀態為1
private static int state = 1;
private static int loopNumber = 5;
private static Object obj = new Object();
public static void print(int waitState, int nextState, String str) {
for (int i = 0; i < loopNumber; i++) {
synchronized (obj) {
// 當前獲得鎖的線程狀態不是 state,等待釋放鎖
while(state != waitState) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 當前獲得鎖的線程狀態就是 state,列印要列印的字元串
log.debug(str);
// 列印完,輪到下一個線程列印了,把狀態改成 nextState,確保下一次列印的線程的狀態一定是 nextState
state = nextState; // syn 保證可見性
obj.notifyAll();
}
}
}
public static void main(String[] args) {
// t1線程,如果當前狀態是1,就列印a,下一個線程的狀態是2
new Thread(() -> {
print(1, 2, "a");
}, "t1").start();
// t2線程,如果當前狀態是2,就列印b,下一個線程的狀態是3
new Thread(() -> {
print(2, 3, "b");
}, "t2").start();
// t3線程,如果當前狀態是3,就列印c,下一個線程的狀態是1
new Thread(() -> {
print(3, 1, "c");
}, "t3").start();
}
}
思路二:Lock條件變數
使用 ReentrantLock
中的條件變數 Condition
實現等待喚醒機制,每次只會有一個線程在列印,而其他兩個線程在各自的休息室等待(可以把創建出來的Condition 對象 理解成線程休息室);
剛開始運行時,三個線程都會在各自的休息室等待,所以這個時候需要有一個發起者,就讓主線程發起,喚醒a休息室中等待的線程 t1;
@Slf4j
public class AlternatePrint1 extends ReentrantLock {
private int loopNumber; // 迴圈次數
public AlternatePrint1(int loopNumber) {
this.loopNumber = loopNumber;
}
/**
* @param cur 進入哪一件休息室等待
* @param next cur休息室的線程列印完後,要喚醒下一個休息室中等待的線程
* @param str 要列印的字元串
*/
public void print(Condition cur, Condition next, String str) {
for (int i = 0; i < loopNumber; i++) {
this.lock();
try {
cur.await(); // 每個線程獲取鎖進來,直接去各自的休息室等待
log.debug(str);
next.signal(); // 喚醒下一間休息室的線程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 創建對象,設置列印次數為5
AlternatePrint1 ap = new AlternatePrint1(5);
// 為三個線程分別創建三個休息室
Condition a = ap.newCondition();
Condition b = ap.newCondition();
Condition c = ap.newCondition();
new Thread(() -> {
ap.print(a, b, "a");
}, "t1").start();
new Thread(() -> {
ap.print(b, c, "b");
}, "t2").start();
new Thread(() -> {
ap.print(c, a, "c");
}, "t3").start();
// 以上三個線程都去各自的休息室等待去了,1s之後,主線程喚醒a休息室中的線程
TimeUnit.SECONDS.sleep(1L);
try {
ap.lock();
// 讓主線程先喚醒在a休息室中等待的線程
a.signal();
} finally {
ap.unlock();
}
}
}