线程同步器

在执行线程任务时,时常会遇到需要多个线程执行完成之后,才进行下一步操作。线程中有三个工具可以实现此功能,下面进行一一介绍:

(一) CountDownLatch

CountDownLatch依赖于抽象同步队列实现,当new 一个countDownLatch对象时传入计数器参数,执行await方法时进入等待。计数器在每个线程执行完之后通过执行countDown方法减去1 ,直到所有线程执行完后计数器为0,此时await方法退出并进入下一步操作。

由于计数器是在线程中干完工作后减少的,所以在创建线程任务时需要将countDownLatch对象传入。

复制代码
package concurrent.executorService;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyCountDownLatch {
    private ExecutorService executorService;
    private CountDownLatch countDown;
    public MyCountDownLatch(int poolSize, int countDownNum){
         this.executorService = Executors.newFixedThreadPool(poolSize);
         this.countDown = new CountDownLatch(countDownNum);
    }

    public void useCountDownLatch() throws InterruptedException {
        long taskNum = this.countDown.getCount();
        for(int i = 0; i < taskNum; i++){
            executorService.submit(new MyTask(countDown, i));
        }
        countDown.await();
    }

    public class MyTask implements Runnable{
        private CountDownLatch countDownLatch;
        private int taskSequence;

        public  MyTask(CountDownLatch countDown, int i){
            this.countDownLatch = countDown;
            this.taskSequence = i;
        }
        @Override
        public void run() {
            doTask(taskSequence);
            this.countDownLatch.countDown();
        }
    }

    public void doTask(int taskSequence) {
        try{
            Thread.sleep(1000);
        }catch (Exception e){
            System.out.println(e.getMessage());
        }

        System.out.println("execute task job :" + taskSequence);
    }

}
复制代码

测试代码如下:

public class MyCountDownLatchTest extends TestCase {

    public void testUseCountDownLatch() throws InterruptedException {
        MyCountDownLatch myCountDownLatch = new MyCountDownLatch(5,3);
        myCountDownLatch.useCountDownLatch();
        System.out.println("所有任务已完成");
    }
}

测试结果:

execute task job :1
execute task job :2
execute task job :0
所有任务已完成

 

(二)CyclicBarrier

cyclicBarrier回环屏障也被成为栅栏,在创建cyclicBarrier对象时,需要传入计数器的值(设置的栅栏数)和需要等待所有线程完成之后要做的事情(Runnable接口)。 在每个线程执行完内部的工作之后执行await方法即将计数器减去1(跨过了一个栅栏),待到计数器的值为0时,就执行Runnable接口实现的方法。此外计数器为0 时,也会重新将计数器设置为原来的栅栏数,此举就是为了复用。

代码示例如下:

复制代码
package concurrent.executorService;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyCyclicBarrier {
    private CyclicBarrier cyclicBarrier;
    private ExecutorService service;
    private Integer integer = new Integer(1);

    public MyCyclicBarrier() {
        cyclicBarrier = new CyclicBarrier(2, () -> {
            System.out.println("step " + integer + " finished");
            ++integer;
        });
         service = Executors.newFixedThreadPool(2);
    }

    public void useCyclicBarrier() {
        service.submit(() -> {
            System.out.println(Thread.currentThread().getName() + " do step one");
            cyclicBarrierAwait();
            System.out.println(Thread.currentThread().getName() + " do step two");
            cyclicBarrierAwait();
            System.out.println(Thread.currentThread().getName() + " do step three");
            cyclicBarrierAwait();
        });

        service.submit(() -> {
            System.out.println(Thread.currentThread().getName() + " do step one");
            cyclicBarrierAwait();
            System.out.println(Thread.currentThread().getName() + " do step two");
            cyclicBarrierAwait();
            System.out.println(Thread.currentThread().getName() + " do step three");
            cyclicBarrierAwait();
        });
        service.shutdown();
        System.out.println("over");
    }

    private void cyclicBarrierAwait() {
        try {
            cyclicBarrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}
复制代码

测试代码如下:

public class MyCyclicBarrierTest extends TestCase {

    public void testUseCyclicBarrier() {
        MyCyclicBarrier cyclicBarrier = new MyCyclicBarrier();
        cyclicBarrier.useCyclicBarrier();
    }
}

测试结果:

pool-1-thread-1 do step one

pool-1-thread-2 do step one

over

step 1 finished

pool-1-thread-1 do step two

pool-1-thread-2 do step two

step 2 finished

pool-1-thread-1 do step three

pool-1-thread-2 do step three

step 3 finished

从以上结果可以看出,主线程的执行顺序是不受栅栏影响的,栅栏只规定了线程中哪些步骤做完之后再去做栅栏中的Runnable接口的方法。此外,栅栏的计数器是可以复用的。

 

(三)Semaphore 信号量

信号量也是依赖于抽象同步队列实现的。在创建信号量对象时传入的计数器值是0,执行信号量的acquire方法时会传入一个参数,该参数表示计数器需要达到该值后才会退出等待,执行下一步操作。每个线程中通过执行信号量的release方法,将计数器加1.

复制代码
package concurrent.executorService;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class MySemaphore {
    private Semaphore semaphore = new Semaphore(0);
    private ExecutorService service;

    public MySemaphore(){
        service = Executors.newFixedThreadPool(2);
    }

    public void useSemaphore() throws InterruptedException {
        service.submit(() -> {
            System.out.println(Thread.currentThread().getName() + " do step one");
            semaphore.release();
        });
        service.submit(() -> {
            System.out.println(Thread.currentThread().getName() + " do step one");
            semaphore.release();
        });
        semaphore.acquire(2);
        service.shutdown();
        System.out.println("over");
    }
}
复制代码

测试代码:

public class MySemaphoreTest extends TestCase {

    public void testUseSemaphore() throws InterruptedException {
        MySemaphore mySemaphore = new MySemaphore();
        mySemaphore.useSemaphore();
    }
}

测试结果:

pool-1-thread-1 do step one

pool-1-thread-2 do step one

over

从这里可以看出Semphore和CountDownLatch的效果非常的类似。只是Semphore的计数器也是可以复用的。

(四) cyclicBarrier与CountDownLatch的区别

它的命名形象的表示了其能力属性,Count代表着计数,Down代表着计数器的递减操作,而Latch表示计数器递减后的结果动作。
它通常被应用于:允许一个或多个线程,等待其他一组线程完成操作,再继续执行countDownLatch的计数器为0后,不可重用
countDownLatch基于抽象同步队列, 三个重要的方法:
//通过构造函数传递计数器的值,实际是传给了抽象队列同步器的state
复制代码
public CountDownLatch(int count) {
   if (count < 0) throw new IllegalArgumentException("count < 0");
   this.sync = new Sync(count);
}
 
//减少锁存器的计数,如果计数达到零,则释放所有等待线程; 如果当前计数大于零,则递减。
public void countDown() {
    sync.releaseShared(1);
}
 
//将当前线程阻塞住,直到count值减为0才会放开执行
public void await() throws InterruptedException {
  sync.acquireSharedInterruptibly(1);
}
复制代码
 
cyclicBarrier:Cyclic是循环的意思而Barrier则表示栅栏、障碍的意思,字面的意思就是可循环的栅栏。
它经常被应用于:允许一组线程相互之间等待,达到一个共同点,再继续执行。可复用。
CyclicBarrier 的源码实现和 CountDownLatch 大同小异,CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 ReentrantLock来实现的。
CyclicBarrier内部维护了parties和count变量,parties表示每次参与到一个Generation中需要被拦截的线程数量,而count是内部计数器,在初始化的时候count与parties相等,当每次调用await方法的时候计数器count就会减1,这和上文中的countDown类似。如果计数器为0, 唤醒所有线程,并进入下一个线程协调器。
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();
 
//指定每次需要进行协调的线程个数以及解除阻塞之后需要后续执行的任务
复制代码
public CyclicBarrier(int parties, Runnable barrierAction) {
       if (parties <= 0) throw new IllegalArgumentException();
       this.parties = parties;
       this.count = parties;
       this.barrierCommand = barrierAction;
   }
 //指定每次需要进行协调的线程个数
 public CyclicBarrier(int parties) {
       this(parties, null);
   }
 
private int dowait(boolean timed, long nanos)
       throws InterruptedException, BrokenBarrierException,
               TimeoutException {
       final ReentrantLock lock = this.lock;
        lock.lock();
       try {
           final Generation g = generation;
 
           if (g.broken)
               throw new BrokenBarrierException();
 
           if (Thread.interrupted()) {
               breakBarrier();
               throw new InterruptedException();
           }
           //如果计数器为0, 唤醒所有线程,并进入下一个线程协调器。
           int index = --count;
           if (index == 0) {  // tripped
               boolean ranAction = false;
               try {
                   final Runnable command = barrierCommand;
                   if (command != null)
                        command.run();
                    ranAction = true;
                   nextGeneration();
                   return 0;
               } finally {
                   if (!ranAction)
                       breakBarrier();
               }
           }
 
           //计数器不为0,继续进行循环
           for (;;) {
               try {
                   if (!timed)
                        trip.await();
                   else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
               } catch (InterruptedException ie) {
                   if (g == generation && ! g.broken) {
                       breakBarrier();
                       throw ie;
                   } else {
                       // We're about to finish waiting even if we had not
                       // been interrupted, so this interrupt is deemed to
                       // "belong" to subsequent execution.
                       Thread.currentThread().interrupt();
                   }
               }
 
               if (g.broken)
                   throw new BrokenBarrierException();
 
               if (g != generation)
                   return index;
 
               if (timed && nanos <= 0L) {
                   breakBarrier();
                   throw new TimeoutException();
               }
           }
       } finally {
            lock.unlock();
       }
   }
 
private void nextGeneration() {
       // signal completion of last generation
        trip.signalAll();
       // set up next generation
        count = parties;
        generation = new Generation();
}
 
private void breakBarrier() {
        generation.broken = true;
        count = parties;
        trip.signalAll();
}
复制代码
区别 CountDownLatch CyclicBarrier
计数方式

递减计数

调用countDown,N-1

加法计数

调用await, N+1

可重复利用性 不可重复利用 可重复利用
初始值 初始值为N,N>0 N为0
阻塞条件 N>0, 调用await一直阻塞 N小于指定值
何时释放等待线程 计数为0时 计数达到指定的N值
是否阻塞主进程 不会,只会阻塞子进程
posted @   小兵要进步  阅读(144)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)

侧边栏
点击右上角即可分享
微信分享提示