多線程交替輸出A1B2C3D4...你怎么實現?
引言
不知道最近有沒有小伙伴去面試,今天了不起回想到了早期去面試遇到的一個多線程面試問題。
面試問題是一個筆試題:
兩個線程依次交替輸出A~Z,1到26,形如A1B2C3D4...
當時的我還很菜,用了原生的線程,借助wait和notify方法實現。
伙伴們你們也可以先暫停,自己思考下用什么方式來實現。
今天了不起和伙伴們一起來基于JDK1.8進行實現方式的探索,請看下文。
1. 使用線程方法
wait()方法會使當前線程釋放鎖,并進入等待狀態,直到以下情況之一發生:
- 被其他線程調用notify()方法喚醒;
- 被其他線程調用notifyAll()方法喚醒;
- 被其他線程中斷。
notify()方法用于喚醒一個正在等待的線程,使其從wait()方法中返回。
結合一個出讓等待的機制,就這樣交替實現。
public class T06_00_sync_wait_notify {
public static void main(String[] args) {
final Object o = new Object();
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
new Thread(()->{
synchronized (o) {
for(char c : aI) {
System.out.print(c);
try {
o.notify();
o.wait(); //讓出鎖
} catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify(); //必須,否則無法停止程序
}
}, "t1").start();
new Thread(()->{
synchronized (o) {
for(char c : aC) {
System.out.print(c);
try {
o.notify();
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify();
}
}, "t2").start();
}
}
運行結果:
圖片
思考:伙伴們,如果我想保證t2在t1之前打印,也就是說保證首先輸出的是A而不是1,這個時候該如何做?
2. 使用CountDownLatch鐵門閂
CountDownLatch是Java多線程中的一個同步工具類,它可以讓一個或多個線程等待其他線程完成操作后再繼續執行。
具體來說,CountDownLatch有兩個主要方法:
- await()方法:調用該方法的線程會進入等待狀態,直到計數器的值為0或者被中斷;
- countDown()方法:調用該方法會將計數器減1,當計數器的值為0時,會喚醒所有等待的線程。
public class T07_00_sync_wait_notify {
private static CountDownLatch latch = new CountDownLatch(1);
public static void main(String[] args) {
final Object o = new Object();
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
new Thread(()->{
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (o) {
for(char c : aI) {
System.out.print(c);
try {
o.notify();
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify();
}
}, "t1").start();
new Thread(()->{
synchronized (o) {
for(char c : aC) {
System.out.print(c);
latch.countDown();
try {
o.notify();
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify();
}
}, "t2").start();
}
}
運行結果:
圖片
3. 使用ReentrantLock
我們可以通過ReentrantLock獲取條件鎖,通過它提供的方法來實現。
具體來說,ReentrantLock的Condition接口提供了以下三個方法:
- await()方法:當前線程進入等待狀態,并釋放鎖,直到其他線程使用signal()或signalAll()方法喚醒它;
- signal()方法:喚醒一個等待在該條件上的線程;
- signalAll()方法:喚醒所有等待在該條件上的線程。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class T08_00_lock_condition {
public static void main(String[] args) {
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(()->{
try {
lock.lock();
for(char c : aI) {
System.out.print(c);
condition.signal();
condition.await();
}
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start();
new Thread(()->{
try {
lock.lock();
for(char c : aC) {
System.out.print(c);
condition.signal();
condition.await();
}
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t2").start();
}
}
運行結果:
圖片
Condition本質是鎖資源上不同的等待隊列,我們也可以獲取不同的等待隊列來實現。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class T09_00_lock_condition {
public static void main(String[] args) {
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
Lock lock = new ReentrantLock();
Condition conditionT1 = lock.newCondition();
Condition conditionT2 = lock.newCondition();
new Thread(()->{
try {
lock.lock();
for(char c : aI) {
System.out.print(c);
conditionT2.signal();
conditionT1.await();
}
conditionT2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start();
new Thread(()->{
try {
lock.lock();
for(char c : aC) {
System.out.print(c);
conditionT1.signal();
conditionT2.await();
}
conditionT1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t2").start();
}
}
4. 使用TransferQueue阻塞隊列
TransferQueue是Java并發包中的一個阻塞隊列,它可以用于多線程之間的數據交換和同步。
LinkedTransferQueue繼承自TransferQueue,并且還可以支持異步操作。
圖片
LinkedTransferQueue的take()方法和transfer()方法都是用于從隊列中取出元素的方法,但它們的使用場景和行為有所不同。
take()方法是一個阻塞方法,它會一直阻塞直到隊列中有可用元素,才將隊列中的元素取出并返回。
transfer()方法也是一個阻塞方法,它會將指定的元素插入到隊列中,并等待另一個線程從隊列中取出該元素。如果隊列中沒有等待的線程,則當前線程會一直阻塞,直到有其他線程從隊列中取走該元素為止。
那么我們就利用這一點它必須要另外一個線程來取進而實現把值交替輸出。
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;
public class T13_TransferQueue {
public static void main(String[] args) {
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
TransferQueue<Character> queue = new LinkedTransferQueue<Character>();
new Thread(()->{
try {
for (char c : aI) {
System.out.print(queue.take());
queue.transfer(c);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(()->{
try {
for (char c : aC) {
queue.transfer(c);
System.out.print(queue.take());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
}
}
運行結果:
圖片
5. 使用LockSupport
LockSupport是Java并發包中的一個工具類,它可以用于線程的阻塞和喚醒。
你可以把它類比成Object的wait()和notify()方法,但LockSupport是比它們更加靈活和可控的。
LockSupport提供了park()和unpark()方法:
當一個線程調用park()方法時,它會被阻塞,直到另一個線程調用該線程的unpark()方法才會被喚醒。
如果調用unpark()方法時,該線程還沒有調用park()方法,則該線程調用park()方法時不會被阻塞,可以直接返回。
import java.util.concurrent.locks.LockSupport;
//Locksupport park 當前線程阻塞(停止)
//unpark(Thread t)
public class T02_00_LockSupport {
static Thread t1 = null, t2 = null;
public static void main(String[] args) throws Exception {
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
t1 = new Thread(() -> {
for(char c : aI) {
System.out.print(c);
LockSupport.unpark(t2); //叫醒T2
LockSupport.park(); //T1阻塞
}
}, "t1");
t2 = new Thread(() -> {
for(char c : aC) {
LockSupport.park(); //t2阻塞
System.out.print(c);
LockSupport.unpark(t1); //叫醒t1
}
}, "t2");
t1.start();
t2.start();
}
}
運行結果:
圖片
6. 使用枚舉類作同步標志
創建一個枚舉類ReadyToRun,利用while(true)死等和枚舉類指向對象不同作標志位交替輸出。
public class T03_00_cas {
enum ReadyToRun {T1, T2}
static volatile ReadyToRun r = ReadyToRun.T1;
public static void main(String[] args) {
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
new Thread(() -> {
for (char c : aI) {
while (r != ReadyToRun.T1) {}
System.out.print(c);
r = ReadyToRun.T2;
}
}, "t1").start();
new Thread(() -> {
for (char c : aC) {
while (r != ReadyToRun.T2) {}
System.out.print(c);
r = ReadyToRun.T1;
}
}, "t2").start();
}
}
運行結果:
圖片
總結
好了,關于這個面試題的解法了不起暫時就想到這6種情況。
這個面試題也是一道經典的多線程面試題,如果你能將這幾種情況掌握,定會另面試官刮目相看。
如果你們還有新的方法歡迎和了不起一起探討研究,畢竟代碼是死的人是活的。