JUC并发工具集:CountDownLatch、Semaphore、CyclicBarrier

57 阅读8分钟

CountDownLatch

概念

  • countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
  • 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

简单使用

下方代码为阻塞多个线程,只有当计数器为0的时候才会执行其他等待线程。开启三个线程,使其处于等待状态,只有主线程中将计数器-1,等待的线程才可以执行。

public class CountDownExample {

    //定义,构造方法中设置计数器
    static CountDownLatch countDownLatch = new CountDownLatch(1);

    static class Thread1 extends Thread {

        @Override
        public void run() {
            try {
                countDownLatch.await();
                System.out.println("thread1");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //表示我已经干完了
        }
    }

    static class Thread2 extends Thread {

        @Override
        public void run() {
            try {
                countDownLatch.await();
                System.out.println("thread2");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class Thread3 extends Thread {
        @Override
        public void run() {
            try {
                countDownLatch.await();
                System.out.println("thread3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread1 t1 = new Thread1();
        t1.start();
        Thread2 t2 = new Thread2();
        t2.start();
        Thread3 t3 = new Thread3();
        t3.start();
        System.out.println("开始执行");
        Thread.sleep(1000);
        //计数器 - 1
        countDownLatch.countDown();
    }
}

演示效果:

开始执行
thread1
thread2
thread3

源码

  • countDownLatch类中只提供了一个构造器:

    //参数count为计数值
    public CountDownLatch(int count) {  }; 
    
  • 类中有三个方法是最重要的:

    //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
    public void await() throws InterruptedException { };   
    //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
    public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
    //将count值减1
    public void countDown() { };  
    

实际应用

在启动应用的时候,去对第三方的应用做健康检测。只有全部合格才可以执行其他操作。

  • 创建一个抽象类
public abstract class BaseHealthChecker implements Runnable {

    private String serviceName; //服务名称

    private boolean serviceUp;

    public BaseHealthChecker(String serviceName) {
        this.serviceName = serviceName;
    }

    @Override
    public void run() {
        try {
            verifyService();
            serviceUp = true;
        } catch (Exception e) {
            serviceUp = false;
            e.printStackTrace();
        }
    }

    /**
     * 检查服务的健康情况
     */
    public abstract void verifyService() throws Exception;

    public String getServiceName() {
        return serviceName;
    }

    public boolean isServiceUp() {
        return serviceUp;
    }
}
  • 创建两个服务
public class CacheHealthChecker extends BaseHealthChecker {

    private CountDownLatch countDownLatch;

    public CacheHealthChecker(CountDownLatch countDownLatch) {
        super("CacheHealthChecker");
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void verifyService() throws Exception {
        System.out.println("Checking:" + this.getServiceName());
        try {
            Thread.sleep(1000);
            // 如果检查失败,throw RuntimeException()
        } catch (Exception e) {
            throw e;
        }
        //计数器 - 1
        countDownLatch.countDown();
        System.out.println(this.getServiceName() + " 健康状态正常");
    }
}
public class DatabaseHealthChecker extends BaseHealthChecker {
    private CountDownLatch countDownLatch;


    public DatabaseHealthChecker(CountDownLatch countDownLatch) {
        super("DatabaseHealthChecker");
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void verifyService() throws Exception {
        System.out.println("Checking:" + this.getServiceName());
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            throw e;
        }
        //计数器 - 1
        countDownLatch.countDown();
        System.out.println(this.getServiceName() + " 健康状态正常");
    }

}
  • 启动类,首先检查服务
public class ApplicationStartup {

    private static List<BaseHealthChecker> services;
    private static CountDownLatch countDownLatch = new CountDownLatch(2);

    static {
        services = new ArrayList<>();
        services.add(new CacheHealthChecker(countDownLatch));
        services.add(new DatabaseHealthChecker(countDownLatch));
    }

    //单例模式
    private final static ApplicationStartup INSTANCE = new ApplicationStartup();

    private ApplicationStartup() {
    }

    public static ApplicationStartup getInstance() {
        return INSTANCE;
    }

    public static boolean checkExternalServices() throws InterruptedException {
        for (BaseHealthChecker bh : services) {
            new Thread(bh).start(); //针对每个服务采用线程来执行
        }
        //等待检查完成
        countDownLatch.await();
        return true;
    }
}
  • 测试类
public class StartupMain {
    public static void main(String[] args) {
        try {
            ApplicationStartup.checkExternalServices();
        } catch (InterruptedException e) {
            //有问题了.进行其他处理
            e.printStackTrace();
        }
        System.out.println("服务启动成功");
    }
}
  • 演示效果
Checking:CacheHealthChecker
Checking:DatabaseHealthChecker
CacheHealthChecker 健康状态正常
DatabaseHealthChecker 健康状态正常
服务启动成功

CountDownLatch和CyclicBarrier区别:
1.countDownLatch是一个计数器,线程完成一个记录一个,计数器递减,只能只用一次
2.CyclicBarrier的计数器更像一个阀门,需要所有线程都到达,然后继续执行,计数器递增,提供reset功能,可以多次使用

Semaphore

概念

Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。

可以把它简单的理解成我们停车场入口立着的那个显示屏,每有一辆车进入停车场显示屏就会显示剩余车位减1,每有一辆车从停车场出去,显示屏上显示的剩余车辆就会加1,当显示屏上的剩余车位为0时,停车场入口的栏杆就不会再打开,车辆就无法进入停车场了,直到有一辆车从停车场出去为止。

使用场景

通常用于那些资源有明确访问数量限制的场景,常用于限流 。

比如:数据库连接池,同时进行连接的线程有数量限制,连接不能超过一定的数量,当连接达到了限制数量后,后面的线程只能排队等前面的线程释放了数据库连接才能获得数据库连接。

比如:停车场场景,车位数量有限,同时只能容纳多少台车,车位满了之后只有等里面的车离开停车场外面的车才可以进入。

方法说明

acquire()  
获取一个令牌,在获取到令牌、或者被其他线程调用中断之前线程一直处于阻塞状态。

acquire(int permits)  
获取一个令牌,在获取到令牌、或者被其他线程调用中断、或超时之前线程一直处于阻塞状态。
    
acquireUninterruptibly() 
获取一个令牌,在获取到令牌之前线程一直处于阻塞状态(忽略中断)。
    
tryAcquire()
尝试获得令牌,返回获取令牌成功或失败,不阻塞线程。

tryAcquire(long timeout, TimeUnit unit)
尝试获得令牌,在超时时间内循环尝试获取,直到尝试获取成功或超时返回,不阻塞线程。

release()
释放一个令牌,唤醒一个获取令牌不成功的阻塞线程。

hasQueuedThreads()
等待队列里是否还存在等待线程。

getQueueLength()
获取等待队列里阻塞的线程数。

drainPermits()
清空令牌把可用令牌数置为0,返回清空令牌的数量。

availablePermits()
返回可用的令牌数量。

定义一个有10个令牌的Semaphore。

Semaphore semaphore=new Semaphore(10);

acquire = 10 - 1

  • 为0的时候,阻塞
  • 有可能同时阻塞N个线程

release = 令牌 + 1

  • 有令牌了,唤醒
  • 从阻塞的线程中去唤醒。

为什么用到共享锁?

因为同时可以释放多个令牌,那么意味着可以同时有多个线程抢占到锁。

初步使用

我们通过信号量模拟一个停车场的使用场景。

当没有车位后,其他车就不能进入;必须等待新的空位。

public class SemaphoreExample {

    public static void main(String[] args) {
        //限制资源的并发数量. 可以理解为只有10个车位。
        Semaphore semaphore = new Semaphore(10);
        for (int i = 0; i < 20; i++) {
            new Car(i, semaphore).start();
        }
    }

    static class Car extends Thread {
        private int num;
        private Semaphore semaphore;

        public Car(int num, Semaphore semaphore) {
            this.num = num;
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
            try {
                semaphore.acquire(); //获得一个令牌
                System.out.println("第 " + num + "俩车抢到一个车位");
                TimeUnit.SECONDS.sleep(2);
                System.out.println("第 " + num + "走喽~");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release(); //释放一个令牌
            }
        }
    }
}

实现原理

初始化

Semaphore semaphore = new Semaphore(10);

1、当调用new Semaphore(10) 方法时,默认会创建一个非公平的锁的同步阻塞队列。

2、把初始令牌数量赋值给同步队列的state状态,state的值就代表当前所剩余的令牌数量。

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

Sync(int permits) {
    setState(permits);
}

获取令牌

semaphore.acquire();

1、当前线程会尝试去同步队列获取一个令牌,获取令牌的过程也就是使用原子的操作去修改同步队列的state ,获取一个令牌则修改为state=state-1。

2、 当计算出来的state<0,则代表令牌数量不足,此时会创建一个Node节点加入阻塞队列,挂起当前线程。

3、当计算出来的state>=0,则代表获取令牌成功。

/**
     *  获取1个令牌
     */
public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}


/**
     * 共享模式下获取令牌,获取成功则返回,失败则加入阻塞队列,挂起线程
     * @param arg
     * @throws InterruptedException
     */
public final void acquireSharedInterruptibly(int arg)
    throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //尝试获取令牌,arg为获取令牌个数,当可用令牌数减当前令牌数结果小于0,则创建一个节点加入阻塞队列,挂起当前线程。
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}


/**
     * 1、创建节点,加入阻塞队列,
     * 2、重双向链表的head,tail节点关系,清空无效节点
     * 3、挂起当前节点线程
     * @param arg
     * @throws InterruptedException
     */
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    //创建节点加入阻塞队列
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            //获得当前节点pre节点
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);//返回锁的state
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            //重组双向链表,清空无效节点,挂起当前线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

释放令牌

semaphore.release();

1、线程会尝试释放一个令牌,释放令牌的过程也就是把同步队列的state修改为state=state+1的过程

2、释放令牌成功之后,同时会唤醒同步队列中的一个线程。

3、被唤醒的节点会重新尝试去修改state=state-1 的操作,如果state>=0则获取令牌成功,否则重新进入阻塞队列,挂起线程。

/**
     * 释放令牌
     */
public void release() {
    sync.releaseShared(1);
}


/**
     *释放共享锁,同时会唤醒同步队列中的一个线程。
     * @param arg
     * @return
     */
public final boolean releaseShared(int arg) {
    //释放共享锁
    if (tryReleaseShared(arg)) {
        //唤醒所有共享节点线程
        doReleaseShared();
        return true;
    }
    return false;
}


/**
     * 唤醒同步队列中的一个线程
     */
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {//是否需要唤醒后继节点
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))//修改状态为初始0
                    continue;
                unparkSuccessor(h);//唤醒h.nex节点线程
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE));
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

CyclicBarrier

概念

从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障。

它的作用就是会让所有线程都等待完成后才会继续下一步行动。相当于 , 多个线程通过CountDownLatch的await阻塞 。然后另外一个线程使用countDown方法来唤醒。

举个例子,就像生活中我们会约朋友们到某个餐厅一起吃饭,有些朋友可能会早到,有些朋友可能会晚到,但是这个餐厅规定必须等到所有人到齐之后才会让我们进去。这里的朋友们就是各个线程,餐厅就是 CyclicBarrier。

使用场景

可以用于多线程计算数据,最后合并计算结果的场景。

方法说明

public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
  • parties 是参与线程的个数
  • 第二个构造方法有一个 Runnable 参数,这个参数的意思是最后一个到达线程要做的任务
public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
  • 线程调用 await() 表示自己已经到达栅栏
  • BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时

简单使用

public class CyclicBarrierExample {

    public static void main(String[] args) {
        //4个任务都到栅栏之后执行下一步
        int n = 4;
        CyclicBarrier barrier = new CyclicBarrier(4, () -> {
            System.out.println("所有线程都写入完成,继续处理其他任务");
        });  // 4
        for (int i = 0; i < n; i++) {
            new Writer(barrier).start();
        }
    }

    static class Writer extends Thread {
        private CyclicBarrier cyclicBarrier;

        public Writer(CyclicBarrier barrier) {
            this.cyclicBarrier = barrier;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + "写入数据完毕,等待其他线程");
                cyclicBarrier.await();  //-1的动作;到栅栏了
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }

        }
    }
}

运行效果:

Thread-3写入数据完毕,等待其他线程
Thread-1写入数据完毕,等待其他线程
Thread-0写入数据完毕,等待其他线程
Thread-2写入数据完毕,等待其他线程
所有线程都写入完成,继续处理其他任务