1. 闭锁:CountDownLatch
1.1 使用场景
若有多条线程,其中一条线程需要等到其他所有线程准备完所需的资源后才能运行,这样的情况可以使用闭锁。
CountDownLatch允许一个或者多个线程等待其他线程完成操作。
1.2 代码实现
public class CountDownLatchTest { static CountDownLatch latch = new CountDownLatch(3); public static void main(String[] args) { // 初始化闭锁,并设置资源个 new Thread(() -> { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } // 加载资源1 System.out.println("加载资源的代码1..."); // 本资源加载完后,闭锁-1 latch.countDown(); }).start(); new Thread(() -> { // 加载资源2 System.out.println("加载资源的代码2..."); // 本资源加载完后,闭锁-1 latch.countDown(); System.out.println("加载资源的代码3..."); }).start(); new Thread( new Runnable(){ public void run(){ // 本线程必须等待所有资源加载完后才能执行 try { latch.await(6, TimeUnit.SECONDS); // 当闭锁数量为0时,await返回,执行接下来的任务 System.out.println("资源加载完毕,执行任务..."); } catch (InterruptedException e) { e.printStackTrace(); } } } ).start(); } } /** 执行结果: 加载资源的代码2... 加载资源的代码3... 加载资源的代码1... 资源加载完毕,执行任务... */
countDown方法每调用一次,节点数减一,直到节点数为0时,等待的线程被唤醒。由于countDown可以用在任何地方,所以这里说的N个节点,可以是N个线程,也可以是一个线程里的N个执行步骤。
await还有一个重载方法await(long time, TimeUnit unit),超时不等待
2. 同步屏障:CyclicBarrier
2.1 使用场景
若有多条线程,他们到达屏障时将会被阻塞,只有当所有线程都到达屏障时才能打开屏障,所有线程同时执行,若有这样的需求可以使用同步屏障。此外,当屏障打开的同时还能指定执行的任务。
可以用于多线程计算数据,最后合并计算结果的场景。每个线程计算完毕之后插入一个屏障,都处理完之后在执行一个run方法去处理汇总后的计算结果。
2.2 闭锁 与 同步屏障 的区别
- 闭锁只会阻塞一条线程,目的是为了让该条任务线程满足条件后执行;
- 而同步屏障会阻塞所有线程,目的是为了让所有线程同时执行(实际上并不会同时执行,而是尽量把线程启动的时间间隔降为最少)。
- CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。
2.3 代码实现
public class BarrierTest { // 创建同步屏障对象,并制定需要等待的线程个数 和 打开屏障时需要执行的任务 static CyclicBarrier barrier = new CyclicBarrier(3,new Runnable(){ public void run(){ //当所有线程准备完毕后触发此任务 System.out.println("准备好了"); } }); public static void main(String[] args) { // 启动三条线程 for( int i=0; i<3; i++ ){ new Thread( new Runnable(){ public void run(){ // 等待,(每执行一次barrier.await,同步屏障数量-1,直到为0时,打开屏障) try { System.out.println("屏障未打开"); barrier.await(); System.out.println("屏障打开"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } } ).start(); } } } /** 屏障未打开 屏障未打开 屏障未打开 准备好了 屏障打开 屏障打开 屏障打开 * */
3. 信号量:Semaphore
3.1 使用场景
若有m个资源,但有n条线程(n>m),因此同一时刻只能允许m条线程访问资源,此时可以使用Semaphore控制访问该资源的线程数量。
信号量是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
3.2 代码实现
public class SemaphoreTest { // 创建信号量对象,并给予3个资源 static Semaphore semaphore = new Semaphore(3); private static ExecutorService threadPool = Executors.newCachedThreadPool(); public static void main(String[] args) { // 开启10条线程 for ( int i=0; i<10; i++ ) { final int j=i; threadPool.execute(() -> { try { // 获取资源,若此时资源被用光,则阻塞,直到有线程归还资源 semaphore.acquire(); // 任务代码 TimeUnit.SECONDS.sleep(2); System.out.println("任务执行完毕"+j+",等待的线程数"+semaphore.getQueueLength()); // 释放资源 semaphore.release(); } catch (InterruptedException e) { e.printStackTrace(); } }); } } } /** 任务执行完毕1,等待的线程数7 任务执行完毕3,等待的线程数6 任务执行完毕5,等待的线程数5 任务执行完毕0,等待的线程数4 任务执行完毕2,等待的线程数3 任务执行完毕4,等待的线程数2 任务执行完毕7,等待的线程数1 任务执行完毕6,等待的线程数0 任务执行完毕8,等待的线程数0 任务执行完毕9,等待的线程数0 */
4、线程间交换数据的Exchanger
4.1简介
Exchanger(交换者)是一个用于线程间协作的工具类,用于进行线程间的数据交换。
4.2使用场景
可以用于遗传算法,也可以用于校对工作,查看两个线程返回的数据是否一致。
public class ExchangerTest { private static final Exchanger<String> exgr = new Exchanger<>(); private static ExecutorService threadPool = Executors.newFixedThreadPool(2); public static void main(String[] args) { threadPool.execute(() -> { try { String A = "银行流水A"; exgr.exchange(A); } catch (InterruptedException e) { e.printStackTrace(); } }); threadPool.execute(() -> { try { String B = "银行流水B"; String A = exgr.exchange(B); System.out.println("A和B数据是否一致:" + A.equals(B) + ",A的录入是"+ A + ",B录入的是" + B); } catch (InterruptedException e) { e.printStackTrace(); } }); threadPool.shutdown(); } } /** A和B数据是否一致:false,A的录入是银行流水A,B录入的是银行流水B * */