多线程通信实现交替执行 | 面试题

题目:在 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();
		
	}
}
发布了39 篇原创文章 · 获赞 20 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/cong____cong/article/details/102634726