多線程交替輸出A1B2C3D4...你怎么實(shí)現(xiàn)?
引言
不知道最近有沒有小伙伴去面試,今天了不起回想到了早期去面試遇到的一個(gè)多線程面試問題。
面試問題是一個(gè)筆試題:
兩個(gè)線程依次交替輸出A~Z,1到26,形如A1B2C3D4...
當(dāng)時(shí)的我還很菜,用了原生的線程,借助wait和notify方法實(shí)現(xiàn)。
伙伴們你們也可以先暫停,自己思考下用什么方式來實(shí)現(xiàn)。
今天了不起和伙伴們一起來基于JDK1.8進(jìn)行實(shí)現(xiàn)方式的探索,請看下文。
1. 使用線程方法
wait()方法會(huì)使當(dāng)前線程釋放鎖,并進(jìn)入等待狀態(tài),直到以下情況之一發(fā)生:
- 被其他線程調(diào)用notify()方法喚醒;
- 被其他線程調(diào)用notifyAll()方法喚醒;
- 被其他線程中斷。
notify()方法用于喚醒一個(gè)正在等待的線程,使其從wait()方法中返回。
結(jié)合一個(gè)出讓等待的機(jī)制,就這樣交替實(shí)現(xiàn)。
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();
}
}
運(yùn)行結(jié)果:
圖片
思考:伙伴們,如果我想保證t2在t1之前打印,也就是說保證首先輸出的是A而不是1,這個(gè)時(shí)候該如何做?
2. 使用CountDownLatch鐵門閂
CountDownLatch是Java多線程中的一個(gè)同步工具類,它可以讓一個(gè)或多個(gè)線程等待其他線程完成操作后再繼續(xù)執(zhí)行。
具體來說,CountDownLatch有兩個(gè)主要方法:
- await()方法:調(diào)用該方法的線程會(huì)進(jìn)入等待狀態(tài),直到計(jì)數(shù)器的值為0或者被中斷;
- countDown()方法:調(diào)用該方法會(huì)將計(jì)數(shù)器減1,當(dāng)計(jì)數(shù)器的值為0時(shí),會(huì)喚醒所有等待的線程。
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();
}
}
運(yùn)行結(jié)果:
圖片
3. 使用ReentrantLock
我們可以通過ReentrantLock獲取條件鎖,通過它提供的方法來實(shí)現(xiàn)。
具體來說,ReentrantLock的Condition接口提供了以下三個(gè)方法:
- await()方法:當(dāng)前線程進(jìn)入等待狀態(tài),并釋放鎖,直到其他線程使用signal()或signalAll()方法喚醒它;
- signal()方法:喚醒一個(gè)等待在該條件上的線程;
- 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();
}
}
運(yùn)行結(jié)果:
圖片
Condition本質(zhì)是鎖資源上不同的等待隊(duì)列,我們也可以獲取不同的等待隊(duì)列來實(shí)現(xiàn)。
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阻塞隊(duì)列
TransferQueue是Java并發(fā)包中的一個(gè)阻塞隊(duì)列,它可以用于多線程之間的數(shù)據(jù)交換和同步。
LinkedTransferQueue繼承自TransferQueue,并且還可以支持異步操作。
圖片
LinkedTransferQueue的take()方法和transfer()方法都是用于從隊(duì)列中取出元素的方法,但它們的使用場景和行為有所不同。
take()方法是一個(gè)阻塞方法,它會(huì)一直阻塞直到隊(duì)列中有可用元素,才將隊(duì)列中的元素取出并返回。
transfer()方法也是一個(gè)阻塞方法,它會(huì)將指定的元素插入到隊(duì)列中,并等待另一個(gè)線程從隊(duì)列中取出該元素。如果隊(duì)列中沒有等待的線程,則當(dāng)前線程會(huì)一直阻塞,直到有其他線程從隊(duì)列中取走該元素為止。
那么我們就利用這一點(diǎn)它必須要另外一個(gè)線程來取進(jìn)而實(shí)現(xiàn)把值交替輸出。
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();
}
}
運(yùn)行結(jié)果:
圖片
5. 使用LockSupport
LockSupport是Java并發(fā)包中的一個(gè)工具類,它可以用于線程的阻塞和喚醒。
你可以把它類比成Object的wait()和notify()方法,但LockSupport是比它們更加靈活和可控的。
LockSupport提供了park()和unpark()方法:
當(dāng)一個(gè)線程調(diào)用park()方法時(shí),它會(huì)被阻塞,直到另一個(gè)線程調(diào)用該線程的unpark()方法才會(huì)被喚醒。
如果調(diào)用unpark()方法時(shí),該線程還沒有調(diào)用park()方法,則該線程調(diào)用park()方法時(shí)不會(huì)被阻塞,可以直接返回。
import java.util.concurrent.locks.LockSupport;
//Locksupport park 當(dāng)前線程阻塞(停止)
//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();
}
}
運(yùn)行結(jié)果:
圖片
6. 使用枚舉類作同步標(biāo)志
創(chuàng)建一個(gè)枚舉類ReadyToRun,利用while(true)死等和枚舉類指向?qū)ο蟛煌鳂?biāo)志位交替輸出。
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();
}
}
運(yùn)行結(jié)果:
圖片
總結(jié)
好了,關(guān)于這個(gè)面試題的解法了不起暫時(shí)就想到這6種情況。
這個(gè)面試題也是一道經(jīng)典的多線程面試題,如果你能將這幾種情況掌握,定會(huì)另面試官刮目相看。
如果你們還有新的方法歡迎和了不起一起探討研究,畢竟代碼是死的人是活的。