疫情居家隔离期间,在网上看了几个技术教学视频,意在查漏补缺,虽然网上这些视频的水平鱼龙混杂,但也有讲得相当不错的,这是昨晚看到的马老师讲的一道面试题,记录一下:
如上图,有2个同时运行的线程,一个输出ABCDE,一个输出12345,要求交替输出,即:最终输出A1B2C3D4E5,而且要求thread-1先执行。
主要考点:二个线程如何通信?通俗点讲,1个线程干到一半,怎么让另1个线程知道我在等他?
方法1:利用LockSupport
import java.util.concurrent.locks.LockSupport; public class Test01 { //这里一定要初始化成null,否则在线程内部无法引用,会提示未初始化 static Thread t1 = null, t2 = null; public static void main(String[] args) { char[] cA = "ABCDEFG".toCharArray(); char[] cB = "1234567".toCharArray(); t1 = new Thread(() -> { for (char c : cA) { System.out.print(c); //解锁T2线程(注:unpark线程t2后,t2即使再调用LockSupport.park也锁不住) LockSupport.unpark(t2); //再把自己T1卡住(直到T2为它解锁) LockSupport.park(t1); } }, "t1"); t2 = new Thread(() -> { for (char c : cB) { //先把T2自己卡住(直到T1为它解锁) LockSupport.park(t2); System.out.print(c); //再把T1解锁 LockSupport.unpark(t1); } }, "t2"); t1.start(); t2.start(); } }
优点:逻辑清晰,代码简洁,可认为是最优解。
方法2:模拟自旋锁的做法,利用标志位不断尝试
import java.util.concurrent.atomic.AtomicInteger; public class Test02a { public static void main(String[] args) { char[] cA = "ABCDEFG".toCharArray(); char[] cB = "1234567".toCharArray(); //AtomicInteger保证线程安全,值1表示t1可继续 ,值2表示t2可继续 AtomicInteger flag = new AtomicInteger(1); new Thread(() -> { for (char c : cA) { //不断"自旋"重试 while (flag.get() != 1) { } System.out.print(c); //标志位指向t2 flag.set(2); } }, "t1").start(); new Thread(() -> { for (char c : cB) { while (flag.get() != 2) { } System.out.print(c); //标志位指向t1 flag.set(1); } }, "t2").start(); } }
优点:思路纯朴无华,容易理解。缺点:自旋尝试比较占用cpu,如果有更多线程参与竞争,cpu可能会较高。
这个方法还有一个变体,不借助并发包下的AtomicInteger,可以改用static valatile + enum变量保证线程安全:
public class Test02b { enum ReadyToGo { T1, T2 } static volatile ReadyToGo r = ReadyToGo.T1; public static void main(String[] args) { char[] cA = "ABCDEFG".toCharArray(); char[] cB = "1234567".toCharArray(); new Thread(() -> { for (char c : cA) { while (!r.equals(ReadyToGo.T1)) { } System.out.print(c); r = ReadyToGo.T2; } }).start(); new Thread(() -> { for (char c : cB) { while (!r.equals(ReadyToGo.T2)) { } System.out.print(c); r = ReadyToGo.T1; } }).start(); } }
方法3:利用ReentrantLock可重入锁及Condition条件
import java.util.concurrent.CountDownLatch; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Test03 { public static void main(String[] args) { char[] cA = "ABCDEFG".toCharArray(); char[] cB = "1234567".toCharArray(); Lock lock = new ReentrantLock(); Condition cond1 = lock.newCondition(); Condition cond2 = lock.newCondition(); CountDownLatch latch = new CountDownLatch(1); new Thread(() -> { //保证t1先执行 latch.countDown(); lock.lock(); try { for (char c : cA) { System.out.print(c); //"唤醒"满足条件2的线程t2 cond2.signal(); //卡住满足条件1的线程t1 cond1.await(); } //输出最后1个字符后,把t2也唤醒(否则t2一直await永远退出不了) cond2.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }, "t1").start(); new Thread(() -> { try { //先把t2卡住,保证t1先输出 latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } lock.lock(); try { for (char c : cB) { System.out.print(c); //"唤醒"满足条件1的线程t1 cond1.signal(); //卡住满足条件2的线程t2 cond2.await(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }, "t2").start(); } }
方法4:利用阻塞队列BlockingQueue
import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class Test04 { public static void main(String[] args) { char[] cA = "ABCDEFG".toCharArray(); char[] cB = "1234567".toCharArray(); BlockingQueue<Boolean> q1 = new LinkedBlockingQueue<>(1); BlockingQueue<Boolean> q2 = new LinkedBlockingQueue<>(1); new Thread(() -> { for (char c : cA) { System.out.print(c); try { //放行t2 q2.put(true); //阻塞t1 q1.take(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "t1").start(); new Thread(() -> { for (char c : cB) { try { //先阻塞t2 q2.take(); System.out.print(c); //再放行t1 q1.put(true); } catch (InterruptedException e) { e.printStackTrace(); } } }, "t2").start(); } }
点评:巧妙利用了阻塞队列的特性,思路新颖
方法5:利用IO管道输入/输出流
import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; public class Test05 { public static void main(String[] args) throws IOException { char[] cA = "ABCDEFG".toCharArray(); char[] cB = "1234567".toCharArray(); PipedInputStream input1 = new PipedInputStream(); PipedInputStream input2 = new PipedInputStream(); PipedOutputStream output1 = new PipedOutputStream(); PipedOutputStream output2 = new PipedOutputStream(); input1.connect(output2); input2.connect(output1); //相当于令牌(在2个管道中流转) String flag = "1"; new Thread(() -> { byte[] buffer = new byte[1]; for (char c : cA) { try { System.out.print(c); //将令牌通过output1->input2给到t2 output1.write(flag.getBytes()); //从output2->input1读取令牌(没有数据时,该方法会block,即:相当于卡住自己) input1.read(buffer); } catch (IOException e) { e.printStackTrace(); } } }, "t1").start(); new Thread(() -> { byte[] buffer = new byte[1]; for (char c : cB) { try { //读取t1通过output1->input2传过来的令牌(无数据时,会block住自己) input2.read(buffer); System.out.print(c); //将令牌通过output2->input1给到t1 output2.write(flag.getBytes()); } catch (IOException e) { e.printStackTrace(); } } }, "t2").start(); } }
效率极低,纯属炫技。主要利用了管道流read操作,无数据时,会block的特性,类似阻塞队列。
方法6:利用synchronized/notify/wait
import java.util.concurrent.CountDownLatch; public class Test06 { public static void main(String[] args) { char[] cA = "ABCDEFG".toCharArray(); char[] cB = "1234567".toCharArray(); Object lockObj = new Object(); CountDownLatch latch = new CountDownLatch(1); new Thread(() -> { //保证t1先输出 latch.countDown(); synchronized (lockObj) { for (char c : cA) { System.out.print(c); //通知等待锁释放的其它线程,即:交出锁,然后通知t2去抢 lockObj.notify(); try { //自己进入等待锁的队列(即:卡住自己) lockObj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //输出完后,把自己唤醒,以便线程能结束 lockObj.notify(); } }, "t1").start(); new Thread(() -> { try { //先卡住t2,让t1先输入 latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockObj) { for (char c : cB) { System.out.print(c); //通知等待锁释放的其它线程,即:交出锁,然后通知t1去抢 lockObj.notify(); try { //自己进入等待锁的队列(即:卡住自己) lockObj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // lockObj.notify(); } }, "t2").start(); } }
这是正统解法,原理是先让t1抢到锁(这时t2在等待锁),然后输出1个字符串后,通知t2抢锁,然后t1开始等锁,t2也是类似原理。