Java线程通信
螣蛇乘雾,终为土灰。
多个线程协同工作完成某个任务时就会涉及到线程间通信问题。如何使各个线程之间同时执行,顺序执行、交叉执行等。
一、线程同时执行
创建两个线程a和b,两个线程内调用同一个打印 1-3 三个数字的方法。
1 package tjt; 2 3 import java.time.LocalDate; 4 5 public class Test { 6 7 /** 8 * 创建两个线程a和b,两个线程内调用同一个打印 1-3 三个数字的方法。 9 */ 10 private static void situationOne() { 11 Thread a = new Thread(new Runnable() { 12 @Override 13 public void run() { 14 doSomething("a"); 15 } 16 }); 17 Thread b = new Thread(new Runnable() { 18 @Override 19 public void run() { 20 doSomething("b"); 21 } 22 }); 23 a.start(); 24 b.start(); 25 } 26 27 /** 28 * 依次打印 1, 2, 3 三个数字 29 * 30 * @param threadName 31 */ 32 private static void doSomething(String threadName) { 33 int i = 0; 34 while (i++ < 3) { 35 try { 36 Thread.sleep(200); 37 } catch (InterruptedException e) { 38 e.printStackTrace(); 39 } 40 System.out.println(LocalDate.now() + " Thread " + threadName + " is doing, printing: " + i); 41 } 42 } 43 44 public static void main(String[] args) { 45 situationOne(); 46 } 47 }
多次运行发现a和b是同时打印的,无执行顺序可言。
二、线程顺序执行
创建两个线程a和b,要求b 在 a 全部打印完后再开始打印。使用 thread.join() 方法,在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行,即必须a执行完毕后才轮到b。
1 package tjt; 2 3 import java.time.LocalDate; 4 5 public class Test { 6 7 /** 8 * 创建两个线程a和b,要求b 在 a 全部打印完后再开始打印,使用 thread.join() 方法。 9 * 保证线程a执行完毕后才轮到b 10 */ 11 private static void situationOne() { 12 Thread a = new Thread(new Runnable() { 13 @Override 14 public void run() { 15 doSomething("a"); 16 } 17 }); 18 Thread b = new Thread(new Runnable() { 19 @Override 20 public void run() { 21 try { 22 System.out.println("线程 b 正在通过thread.join()等待线程 a 执行完毕后再润"); 23 // thread.join() 在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行,即必须a执行完毕后才轮到b 24 a.join(); 25 } catch (InterruptedException e) { 26 e.printStackTrace(); 27 } 28 doSomething("b"); 29 } 30 }); 31 a.start(); 32 b.start(); 33 } 34 35 /** 36 * 依次打印 1, 2, 3 三个数字 37 * 38 * @param threadName 39 */ 40 private static void doSomething(String threadName) { 41 int i = 0; 42 while (i++ < 3) { 43 try { 44 Thread.sleep(200); 45 } catch (InterruptedException e) { 46 e.printStackTrace(); 47 } 48 System.out.println(LocalDate.now() + " Thread " + threadName + " is doing, printing: " + i); 49 } 50 } 51 52 public static void main(String[] args) { 53 situationOne(); 54 } 55 }
无论运行多少次,都是线程a先执行完毕再到线程b。
三、线程顺序交叉执行
创建两个线程a和b,要求 a 在打印完 1 后,再让 b 打印 1、2、 3,接着再回到 a 继续打印 2、3。如此顺序交叉执行仅靠 Thread.join() 是无法满足需求的,需要更细粒度的锁来控制执行顺序,以及object.wait() 和 object.notify() 两个方法来实现。
1 package tjt; 2 3 import java.time.LocalDate; 4 5 public class TestAgain { 6 7 private static void situationTwo() { 8 // a 和 b 的共享对象锁 lock 9 Object lock = new Object(); 10 Thread a = new Thread(new Runnable() { 11 @Override 12 public void run() { 13 // 同步锁 lock 14 synchronized (lock) { 15 // a 获得锁后执行 16 doSomething("a", 1); 17 try { 18 // 调用 lock.wait() 方法,交出锁的控制权,进入 wait 状态,等待notify唤醒 19 lock.wait(); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 doSomething("a", 2); 24 doSomething("a", 3); 25 } 26 } 27 }); 28 Thread b = new Thread(new Runnable() { 29 @Override 30 public void run() { 31 // 同步锁 lock 32 synchronized (lock) { 33 // a 获得锁后执行 34 doSomething("b", 1); 35 doSomething("b", 2); 36 doSomething("b", 3); 37 // 调用 lock.notify() 方法,唤醒正在 wait 的线程 a 38 lock.notify(); 39 } 40 } 41 }); 42 a.start(); 43 b.start(); 44 } 45 46 /** 47 * 打印 48 * 49 * @param threadName 50 * @param num 51 */ 52 private static void doSomething(String threadName, int num) { 53 System.out.println(LocalDate.now() + " Thread " + threadName + " is doing, printing: " + num); 54 } 55 56 public static void main(String[] args) { 57 situationTwo(); 58 } 59 }
无论运行多少次,都是线程a先执行打印1,然后线程b执行打印1、2、3,最后线程a执行打印2、3。
四、CountDownLatch
CountDownLatch 计数器适用于一个线程去等待多个线程的情况。例如A B C 三个线程同时运行,各自独立运行完后通知线程 D 执行,就可以利用 CountdownLatch 来实现这类通信方式。
对比之前的join方法,thread.join()可以让一个线程等另一个线程运行完毕后再继续执行,其可以在 D 线程里依次 join A B C,但这样 A B C 必须依次执行,无法实现ABC三者能同步运行。
1 package tjt; 2 3 import java.time.LocalDate; 4 import java.util.concurrent.CountDownLatch; 5 6 public class TestCountDownLatch { 7 8 /** 9 * countDownLatch 适用于一个线程去等待多个线程的情况 10 * 四个线程A、B、C、D, 11 * 其中 D 要等到 A B C 全执行完毕后才执行,且 A B C 是同步运行的,即ABC无顺序执行 12 */ 13 private static void situationThree() { 14 // 初始计数值设置为3,即总共四个线程A、B、C、D 15 CountDownLatch latch = new CountDownLatch(3); 16 new Thread(new Runnable() { 17 @Override 18 public void run() { 19 System.out.println(LocalDate.now() + "线程 D 等待线程A B C 执行完毕后才可执行"); 20 try { 21 // await() 检查计数器值是否为 0,若不为 0 则保持等待状态 22 latch.await(); 23 // 其他线程 的 countDown() 方法把计数值变成 0 时,等待线程里的 countDownLatch.await() 立即退出,继续执行下面的代码 24 System.out.println(LocalDate.now() + "线程A B C 执行完毕,轮到线程D 执行了,当前latch:" + latch.getCount()); 25 } catch (InterruptedException e) { 26 e.printStackTrace(); 27 } 28 } 29 }).start(); 30 31 // 循环执行线程 A B C 32 for (char threadName = 'A'; threadName <= 'C'; threadName++) { 33 String name = String.valueOf(threadName); 34 new Thread(new Runnable() { 35 @Override 36 public void run() { 37 System.out.println("线程 " + name + " is running"); 38 // countDown(),将倒计数器减 1, 计数器被减至 0 时立即触发D 的 await() 39 try { 40 Thread.sleep(200); 41 } catch (InterruptedException e) { 42 e.printStackTrace(); 43 } 44 System.out.println("线程 " + name + " 执行完毕计数器"); 45 latch.countDown(); 46 } 47 }).start(); 48 } 49 } 50 51 public static void main(String[] args) { 52 situationThree(); 53 } 54 55 }
五、CyclicBarrier
实现线程间互相等待,可以利用 CyclicBarrier 栅栏。CountDownLatch 可以用来倒计数,但当计数完毕,只有一个线程的 await() 会得到响应,无法让多个线程同时触发。如要求线程 A B C 各自开始准备,直到三者都准备完毕再同时运行其就无法满足需求,而用CyclicBarrier则完全OK。
螣蛇乘雾
终为土灰