题目:在 Java 中使用两个线程交替输出 1A2B3C.....26Z
题目很简单,但是做起来并不容易,这主要考的是多线程通信相关的知识点。怎么让多个线程之间进行通信,怎么知道多个线程之间的状态信息。当时看到这道面试题,我的脑海中只有一个解题思路,就是 synchronized(对象) 去锁定线程,使其拿到锁对象后才能正常运行,代码如下:
public class ThreadMessage1 {
public static void main(String[] args) {
char zimu[] = new char[26];
int shuzi[] = new int[26];
for(int i=0;i<26;i++) {
zimu[i] = (char) ('A'+i);
shuzi[i] = (i+1);
}
Object obj = new Object();
new Thread(()->{
synchronized (obj) {
for(int i:shuzi) {
System.out.println(i);
obj.notify();
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"t1").start();
new Thread(()->{
synchronized (obj) {
for(char c:zimu) {
System.out.println(c);
obj.notify();
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"t2").start();
}
}
虽然第一次输出了自己想要的结果,但是多试几次就不行了,因为那个线程先运行并不取决你代码的顺序,而且取决于操作系统的调度,所以这个答案其实是不完善的。
然后我在视频中看到马士兵老师讲的第一种解法,用的是 LockSupport ,使用 park(线程) 来暂停当前线程的运行,使用 unpark(线程) 来唤醒另一个线程继续运行。
// 最优雅的解法
public class ThreadMessage2 {
static Thread t1=null,t2=null;
public static void main(String[] args) {
char zimu[] = new char[26];
int shuzi[] = new int[26];
for(int i=0;i<26;i++) {
zimu[i] = (char) ('A'+i);
shuzi[i] = (i+1);
}
t1 = new Thread(()->{
for(int i:shuzi) {
System.out.println(i);
LockSupport.unpark(t2);
LockSupport.park();
}
},"t1");
t2 = new Thread(()->{
for(char c:zimu) {
LockSupport.park();
System.out.println(c);
LockSupport.unpark(t1);
}
},"t2");
t1.start();
t2.start();
}
}
第二种解法:使用自旋锁解决这个问题,可以理解为一个线程在 cpu 中空转,另一个输出,输出完毕后当前空转,让另一个线程输出,往返交替执行。两个线程都一直在占用 CPU 的资源,所以自旋锁适合于那种频繁交替执行的情况使用。
锁的级别:偏向锁->自旋锁->重量级锁
public class ThreadMessage3 {
enum ReadyToRun {T1,T2};
static volatile ReadyToRun r = ReadyToRun.T1;
public static void main(String[] args) {
char zimu[] = new char[26];
int shuzi[] = new int[26];
for(int i=0;i<26;i++) {
zimu[i] = (char) ('A'+i);
shuzi[i] = (i+1);
}
new Thread(()->{
for(int i:shuzi) {
while(r!=ReadyToRun.T1) {}
System.out.println(i);
r=ReadyToRun.T2;
}
},"t1").start();
new Thread(()->{
for(char c:zimu) {
while(r!=ReadyToRun.T2) {}
System.out.println(c);
r=ReadyToRun.T1;
}
},"t2").start();;
}
}
第三种解法实现的思想还是自旋锁,只不过是用了 AtomicInteger 整型原子操作类来实现
public class ThreadMessage4 {
static AtomicInteger thredNo = new AtomicInteger(1);
public static void main(String[] args) {
char zimu[] = new char[26];
int shuzi[] = new int[26];
for(int i=0;i<26;i++) {
zimu[i] = (char) ('A'+i);
shuzi[i] = (i+1);
}
new Thread(()->{
for(int i:shuzi) {
while(thredNo.get()!=1) {}
System.out.println(i);
thredNo.set(2);
}
},"t1").start();
new Thread(()->{
for(char c:zimu) {
while(thredNo.get()!=2) {}
System.out.println(c);
thredNo.set(1);
}
},"t2").start();;
}
}
第四种解法使用的是阻塞队列的特性,判断阻塞队列中是否有值,如果没有就一直处于阻塞状态,直到阻塞队列不为空的时候继续运行,BlockingQueue 是线程安全容器。
public class ThreadMessage5 {
static BlockingQueue<String> q1 = new ArrayBlockingQueue<>(1);//阻塞队列
static BlockingQueue<String> q2 = new ArrayBlockingQueue<>(1);
public static void main(String[] args) {
char zimu[] = new char[26];
int shuzi[] = new int[26];
for(int i=0;i<26;i++) {
zimu[i] = (char) ('A'+i);
shuzi[i] = (i+1);
}
new Thread(()->{
for(int i:shuzi) {
System.out.println(i);
try {
q1.put("ok");
q2.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1").start();
new Thread(()->{
for(char c:zimu) {
try {
q1.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(c);
try {
q2.put("ok");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t2").start();;
}
}
第五种解法比较奇葩,主要是用两个管道流 PipedInputStream 和 PipedOutStream ,通过判断管道流接收的内容来实现通信,由于这种管道流是半双工的,所以需要两对来实现,通过 connect 方法连接两个管道,同样利用了阻塞的原理 read 和 write 方法。内部是很多锁机制实现的,所以效率比较慢,有兴趣可以自行运行一下试试。
public class ThreadMessage6 {
public static void main(String[] args) throws IOException {
char zimu[] = new char[26];
int shuzi[] = new int[26];
PipedInputStream input1 = new PipedInputStream();
PipedInputStream input2 = new PipedInputStream();
PipedOutputStream out1 = new PipedOutputStream();
PipedOutputStream out2 = new PipedOutputStream();
input1.connect(out2);
input2.connect(out1);
String msg = "Your Turn";
for(int i=0;i<26;i++) {
zimu[i] = (char) ('A'+i);
shuzi[i] = (i+1);
}
new Thread(()->{
byte[] buffer = new byte[9];
try {
for(int i:shuzi) {
input1.read(buffer);
if(new String(buffer).equals(msg))
System.out.print(i);
out1.write(msg.getBytes());
}
} catch (IOException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
byte[] buffer = new byte[9];
try {
for(char c:zimu) {
System.out.print(c);
out2.write(msg.getBytes());
input2.read(buffer);
if(new String(buffer).equals(msg))
continue;
}
} catch (IOException e) {
e.printStackTrace();
}
},"t2").start();;
}
}
第六种解法就是我一开始的思路,使用 synchronized 实现线程同步,但是细心的同学会发现我开始的写的那个程序是不完善的:1. 程序不会终止 2.程序不一定是按照 1A2B3C...26Z的方式输出
先解决第一个问题,也就是程序为什么不会终止?
在执行到最后的时候,无论运行到 Thread1 还是 Thread2 都会执行 wait() 方法,也就是说将当前方法暂停,让出锁对象,进入等待队列唤醒,但是程序循环已经结束了,所以这个线程只能在等待队列中一直等,所以这就是线程不会终止的问题。
然后解决第二个问题,让线程有序执行
可以用到上面讲到的自旋锁,当变量为 true 的时候,一个线程自旋,一个线程输出,为 false 是相反就可以了。
public class ThreadMessage1plus {
public static volatile boolean flag = false;//设置第一次执行
public static void main(String[] args) {
char zimu[] = new char[26];
int shuzi[] = new int[26];
for(int i=0;i<26;i++) {
zimu[i] = (char) ('A'+i);
shuzi[i] = (i+1);
}
Object obj = new Object();
new Thread(()->{
synchronized (obj) {
while(flag==true) {
try {
obj.wait();//让出锁对象,同时暂定线程进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for(int i:shuzi) {
System.out.println(i);
obj.notify();
try {
obj.wait();//让出锁对象,同时暂定线程进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
obj.notify();//唤醒线程,解决不会终止问题
}
},"t1").start();
new Thread(()->{
synchronized (obj) {
for(char c:zimu) {
System.out.println(c);
flag=true;
try {
obj.notify();
obj.wait();//让出锁对象,同时暂定线程进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
obj.notify();//唤醒线程,解决不会终止问题
}
},"t2").start();
}
}