疫情居家隔離期間,在網上看了幾個技術教學視頻,意在查漏補缺,雖然網上這些視頻的水平魚龍混雜,但也有講得相當不錯的,這是昨晚看到的馬老師講的一道面試題,記錄一下:
如上圖,有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也是類似原理。