java并发三剑客之CyclicBarrier、CountDownLatch、Semaphore

看了大佬的专栏,https://blog.csdn.net/heihaozi/category_10085170.html。感叹,写的真好,清楚明晰。决定用自己的逻辑总结记录下。

CyclicBarrier

CyclicBarrier: 循环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。这个状态可以说是barrier,当调用await之后,线程就处于barrier状态了。
怎么理解循环?
循环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。

举个例子:
假设小明、小红、小亮兄妹三个要吃早吃饭,妈妈说先洗手,洗完手之后大家一起吃,等三个人吃完饭,再一起去玩。在这个例子中第一个barrier状态是大家都洗好手,第二个barrier状态是大家都吃完饭。第二个barrier在第一个barrier释放后可以重用。

CyclicBarrierTest.java

package com.example.demo;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier barrier  = new CyclicBarrier(3);
        List<Thread> threads = new ArrayList<>(3);
        threads.add(new Thread(new Child(barrier, "小明")));
        threads.add(new Thread(new Child(barrier, "小红")));
        threads.add(new Thread(new Child(barrier, "小亮")));

        for (Thread thread : threads) {
            thread.start();
        }

    }
    static class Child extends Thread{
        private CyclicBarrier cyclicBarrier;
        private String name;

        public Child(CyclicBarrier cyclicBarrier, String name) {
            this.cyclicBarrier = cyclicBarrier;
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(this.name + "正在洗手...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟洗手
                System.out.println(this.name +"洗好了,等待其他小朋友洗完...");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("所有小朋友都洗好手了,开始吃饭吧...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟吃饭
                System.out.println(this.name+"吃好了,等待其他小朋友吃完.....");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("所有小朋友吃好了,一起去玩吧...");
        }
    }
}

执行结果:

小明正在洗手...
小红正在洗手...
小亮正在洗手...
小红洗好了,等待其他小朋友洗完...
小明洗好了,等待其他小朋友洗完...
小亮洗好了,等待其他小朋友洗完...
所有小朋友都洗好手了,开始吃饭吧...
所有小朋友都洗好手了,开始吃饭吧...
所有小朋友都洗好手了,开始吃饭吧...
小红吃好了,等待其他小朋友吃完.....
小亮吃好了,等待其他小朋友吃完.....
小明吃好了,等待其他小朋友吃完.....
所有小朋友吃好了,一起去玩吧...
所有小朋友吃好了,一起去玩吧...
所有小朋友吃好了,一起去玩吧...

CyclicBarrier类位于java.util.concurrent包下,CyclicBarrier提供2个构造器:

public CyclicBarrier(int parties) {

}
public CyclicBarrier(int parties, Runnable barrierAction) {

}

上面的例子中我们用到了第一个,下面的构造器多了一个参数Runable对象,当所有线程到达该屏障时执行该Runable对象。

参数parties指让多少个线程或者任务等待至barrier状态;参数barrierAction为当这些线程都达到barrier状态时会执行的内容,当所有线程到达该屏障时执行该Runable对象。比如下面的代码中,传入Runnable对象,当所有线程都到达barrier状态时,会执行该Runable对象的run方法。

package com.example.demo;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier barrier  = new CyclicBarrier(3,new Runnable() {
            @Override
            public void run() {
                System.out.println("开始做下一件事吧...");
            }});
        List<Thread> threads = new ArrayList<>(3);
        threads.add(new Thread(new Child(barrier, "小明")));
        threads.add(new Thread(new Child(barrier, "小红")));
        threads.add(new Thread(new Child(barrier, "小亮")));

        for (Thread thread : threads) {
            thread.start();
        }

    }
    static class Child extends Thread{
        private CyclicBarrier cyclicBarrier;
        private String name;

        public Child(CyclicBarrier cyclicBarrier, String name) {
            this.cyclicBarrier = cyclicBarrier;
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(this.name + "正在洗手...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟洗手
                System.out.println(this.name +"洗好了,等待其他小朋友洗完...");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            //System.out.println("所有小朋友都洗好手了,开始吃饭吧...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟吃饭
                System.out.println(this.name+"吃好了,等待其他小朋友吃完.....");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            //System.out.println("所有小朋友吃好了,一起去玩吧...");
        }
    }
}

结果:

小明正在洗手...
小红正在洗手...
小亮正在洗手...
小明洗好了,等待其他小朋友洗完...
小红洗好了,等待其他小朋友洗完...
小亮洗好了,等待其他小朋友洗完...
开始做下一件事吧...
小亮吃好了,等待其他小朋友吃完.....
小明吃好了,等待其他小朋友吃完.....
小红吃好了,等待其他小朋友吃完.....
开始做下一件事吧...

CyclicBarrier有哪些常用的方法?

从上面的例子我们也可以看到,await是其最重要的方法。它有2个重载版本:

public int await() throws InterruptedException, BrokenBarrierException { };
public int await(long timeout, TimeUnit unit)throws InterruptedException,BrokenBarrierException,TimeoutException { };

第一个版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;第二个版本是让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。

比如:

package com.example.demo;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier barrier  = new CyclicBarrier(3,new Runnable() {
            @Override
            public void run() {
                System.out.println("开始做下一件事吧...");
            }});
        List<Thread> threads = new ArrayList<>(3);
        threads.add(new Thread(new Child(barrier, "小明")));
        threads.add(new Thread(new Child(barrier, "小红")));
        threads.add(new Thread(new Child(barrier, "小亮")));

        for (Thread thread : threads) {
            thread.start();
        }

    }
    static class Child extends Thread{
        private CyclicBarrier cyclicBarrier;
        private String name;

        public Child(CyclicBarrier cyclicBarrier, String name) {
            this.cyclicBarrier = cyclicBarrier;
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(this.name + "正在洗手...");
            try {
                if(this.name.equalsIgnoreCase("小亮")){
                    Thread.sleep(8000);
                }else{
                    Thread.sleep(5000);
                }//以睡眠来模拟洗手
                System.out.println(this.name +"洗好了,等待其他小朋友洗完...");
                cyclicBarrier.await(2000, TimeUnit.MILLISECONDS);
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

结果:

小明正在洗手...
小红正在洗手...
小亮正在洗手...
小明洗好了,等待其他小朋友洗完...
小红洗好了,等待其他小朋友洗完...
java.util.concurrent.TimeoutException
	at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:257)
	at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
	at com.example.demo.Test$Child.run(Test.java:46)
	at java.lang.Thread.run(Thread.java:748)
java.util.concurrent.BrokenBarrierException
	at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
	at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
	at com.example.demo.Test$Child.run(Test.java:46)
	at java.lang.Thread.run(Thread.java:748)
小亮洗好了,等待其他小朋友洗完...
java.util.concurrent.BrokenBarrierException
	at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
	at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
	at com.example.demo.Test$Child.run(Test.java:46)
	at java.lang.Thread.run(Thread.java:748)

上面的例子中,我们故意让小亮延迟了3000ms,而await的超时时间是2000ms,所以小亮在执行的时候就会抛异常,继续执行后面的任务。

CyclicBarrier实现栅栏原理?

在CyclicBarrier的内部定义了一个ReentrantLock的对象,然后再利用这个ReentrantLock对象生成一个Condition的对象。每当一个线程调用CyclicBarrier的await方法时,首先把剩余屏障的线程数减1,然后判断剩余屏障数是否为0:如果不是,利用Condition的await方法阻塞当前线程;如果是,首先利用Condition的signalAll方法唤醒所有线程,最后重新生成Generation对象以实现屏障的循环使用。
 

CountDownLatch

CountDownLatch是JDK提供的一个同步工具,CyclicBarrier类也位于java.util.concurrent包下。它的作用是让一个或多个线程等待,一直等到其他线程中执行完成一组操作。

比如游戏英雄联盟,主线程为控制游戏开始的线程,其他线程为游戏玩家。在所有的玩家都准备好之前,主线程是处于等待状态的,也就是游戏不能开始。当所有的玩家准备好之后,主线程才能开始游戏。这时候可以用到CountDownLatch。

CountDownLatchTest.java

package com.example.demo;

import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5);
        for(int i = 0; i < latch.getCount(); i++){
            new Thread(new MyThread(latch), "player"+i).start();
        }
        System.out.println("正在等待所有玩家准备好");
        latch.await();
        System.out.println("开始游戏");
    }

    private static class MyThread implements Runnable{
        private CountDownLatch latch ;

        public MyThread(CountDownLatch latch){
            this.latch = latch;
        }

        @Override
        public void run() {
            try {
                Random rand = new Random();
                int randomNum = rand.nextInt((3000 - 1000) + 1) + 1000;//产生1000到3000之间的随机整数
                Thread.sleep(randomNum);
                System.out.println(Thread.currentThread().getName()+" 已经准备好了, 所使用的时间为 "+((double)randomNum/1000)+"s");
                latch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

在上面的代码中,我们模拟5个玩家,只有在5个玩家都准备好之后,才能开始游戏。

结果:

正在等待所有玩家准备好
player4 已经准备好了, 所使用的时间为 1.801s
player3 已经准备好了, 所使用的时间为 1.835s
player2 已经准备好了, 所使用的时间为 2.439s
player0 已经准备好了, 所使用的时间为 2.763s
player1 已经准备好了, 所使用的时间为 2.816s
开始游戏

CountDownLatch有哪些常用的方法?

countDown方法和await方法。CountDownLatch在初始化时,需要指定用给定一个整数作为计数器。当调用countDown方法时,计数器会被减1;当调用await方法时,如果计数器大于0时,线程会被阻塞,一直到计数器被countDown方法减到0时,线程才会继续执行。计数器是无法重置的,当计数器被减到0时,调用await方法都会直接返回。

其中await方法同CyclicBarrier一样,有两个重载版本。

public void await() throws InterruptedException { };   //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行

public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行

下面看下await带有timeout参数的用法:

package com.example.demo;

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class CountDownLatchTest {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5);
        for(int i = 0; i < latch.getCount(); i++){
            new Thread(new MyThread(latch,"player"+i), "player"+i).start();
        }
        System.out.println("正在等待所有玩家准备好");
        latch.await(3, TimeUnit.SECONDS);
        System.out.println("开始游戏");
    }

    private static class MyThread implements Runnable{
        private CountDownLatch latch ;
        private String name ;

        public MyThread(CountDownLatch latch){
            this.latch = latch;
        }

        public MyThread(CountDownLatch latch, String name){
            this.latch = latch;
            this.name = name;
        }

        @Override
        public void run() {
            try {
                Random rand = new Random();

                int randomNum = rand.nextInt((3000 - 1000) + 1) + 1000;//产生1000到3000之间的随机整数
                if(("player2").equalsIgnoreCase(this.name)){
                    randomNum = 4000;
                }
                Thread.sleep(randomNum);
                System.out.println(Thread.currentThread().getName()+" 已经准备好了, 所使用的时间为 "+((double)randomNum/1000)+"s");
                latch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

上面的代码中latch.await(3, TimeUnit.SECONDS);设置主程序等待时间为3秒,而针对palyer2玩家设置准备时间4秒,延迟了1秒。

运行结果:

正在等待所有玩家准备好
player4 已经准备好了, 所使用的时间为 1.994s
player0 已经准备好了, 所使用的时间为 2.006s
player1 已经准备好了, 所使用的时间为 2.119s
player3 已经准备好了, 所使用的时间为 2.895s
开始游戏
player2 已经准备好了, 所使用的时间为 4.0s

我们可以看到主程序没有等待palyer2玩家,直接开始了游戏。

CountDownLatch的实现原理是什么?

CountDownLatch有一个内部类叫做Sync,它继承了AbstractQueuedSynchronizer类,其中维护了一个整数state,并且保证了修改state的可见性和原子性。state使用volatile修饰,保证其可见性。
在countDown方法中,调用Sync实例的releaseShared方法,在该方法中,先获取当前当前计数器的值,如果计数器为0,则直接唤醒所有被await阻塞的线程,如果计数器不为0, 先利用CAS对计数器进行减1,如果减1后的计数器为0,则直接唤醒所有被await阻塞的线程。state减一操作利用CAS原理,保证其原子性。在await方法中,判断计数器是否为0,如果不为0,则阻塞当前线程。

 

乍一看CountDownLatch和CyclicBarrier的功能很相似:从字面上理解,CountDown表示减法计数,Latch表示门闩的意思,计数为0的时候就可以打开门闩了。Cyclic Barrier表示循环的障碍物。两个类都含有这一个意思:对应的线程都完成工作之后再进行下一步动作,也就是大家都准备好之后再进行下一步。然而两者最大的区别是,进行下一步动作的动作实施者是不一样的。这里的“动作实施者”可以看做两种,一种是主线程(即执行main函数,此处说主线程不太准确,更确切的说法应该是调用countDownLatch.await的线程),另一种是执行任务的其他线程,后面叫这种线程为“其他线程”,区别于主线程。对于CountDownLatch,当计数为0的时候,下一步的动作实施者是main函数;对于CyclicBarrier,下一步动作实施者是“其他线程”。

比如上面CountDownLatch的例子,当CountDownLatch计数为0的时候,下一步的动作实施者是main函数,执行的动作是开始游戏。而上面CyclicBarrier,当所有小朋友线程都到达Barrier状态时,下一步的动作实施者仍然是小朋友线程,此例中是等所有小朋友洗好手后,小朋友们再一起吃饭。

Semaphore

Semaphore翻译成字面意思为信号量,它通过维护若干个许可证来控制线程对共享资源的访问。如果许可证剩余数量大于零时,线程则允许访问该共享资源,如果许可证剩余数量为零,则拒绝线程访问该共享资源。Semaphore所维护的许可证数量就是允许访问共享资源的最大线程数量。线程想要访问共享资源必须从Semaphore中获取到许可证。

举个例子:

比如小明、小红、小亮、小兰一起去饭店吃饭,而饭店卫生间只有2个洗手池,这种场景共享资源就是洗手池。可以用到Semaphore。

SemaphoreTest.java

package com.example.demo;


import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Semaphore;

public class SemaphoreTest {
    public static void main(String[] args) throws InterruptedException {
        //饭店里只用两个洗手池,所以初始化许可证的总数为2。
        Semaphore washbasin = new Semaphore(2);

        List<Thread> threads = new ArrayList<>(3);
        threads.add(new Thread(new Customer(washbasin, "小明")));
        threads.add(new Thread(new Customer(washbasin, "小红")));
        threads.add(new Thread(new Customer(washbasin, "小亮")));
        threads.add(new Thread(new Customer(washbasin, "小兰")));
        for (Thread thread : threads) {
            thread.start();
            Thread.sleep(50);
        }

        for (Thread thread : threads) {
            thread.join();
        }
    }

    static class Customer implements Runnable {
        private Semaphore washbasin;
        private String name;

        public Customer(Semaphore washbasin, String name) {
            this.washbasin = washbasin;
            this.name = name;
        }

        @Override
        public void run() {
            try {
                SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
                Random random = new Random();

                washbasin.acquire();
                System.out.println(
                    sdf.format(new Date()) + " " + name + " 开始洗手...");
                Thread.sleep((long) (random.nextDouble() * 5000) + 2000);
                System.out.println(
                    sdf.format(new Date()) + " " + name + " 洗手完毕!");
                washbasin.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

结果:

16:09:52.322 小明 开始洗手...
16:09:52.339 小红 开始洗手...
16:09:54.742 小明 洗手完毕!
16:09:54.742 小亮 开始洗手...
16:09:57.783 小红 洗手完毕!
16:09:57.783 小兰 开始洗手...
16:10:00.196 小亮 洗手完毕!
16:10:00.397 小兰 洗手完毕!

可以看到同一时刻最多有2个人在洗手。

Semaphore有哪些常用的方法?

public void acquire() throws InterruptedException {  }     //获取一个许可
public void acquire(int permits) throws InterruptedException { }    //获取permits个许可
public void release() { }          //释放一个许可
public void release(int permits) { }    //释放permits个许可

最主要的是acquire方法和release方法。 当调用acquire方法时线程就会被阻塞,直到Semaphore中可以获得到许可证为止。 当调用release方法时将向Semaphore中添加一个许可证,如果有线程因为获取许可证被阻塞时,它将获取到许可证并被释放;

这4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:

public boolean tryAcquire() { };    //尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { };  //尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
public boolean tryAcquire(int permits) { }; //尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; //尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false

Semaphore的内部原理?

Semaphore内部主要通过AQS(AbstractQueuedSynchronizer)实现线程的管理。Semaphore在构造时,需要传入许可证的数量,它最后传递给了AQS的state值。线程在调用acquire方法获取许可证时,如果Semaphore中许可证的数量大于0,许可证的数量就减1,线程继续运行,当线程运行结束调用release方法时释放许可证时,许可证的数量就加1。如果获取许可证时,Semaphore中许可证的数量为0,则获取失败,线程进入AQS的等待队列中,等待被其它释放许可证的线程唤醒。

上面的例子中,这4个人会按照线程启动的顺序洗手嘛?

不一定。

Semaphore类位于java.util.concurrent包下,它提供了2个构造器:

public Semaphore(int permits) {          //参数permits表示许可数目,即同时可以允许多少线程进行访问
    sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {    //这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可
    sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
}

第二个构造器中,它提供了一个fair参数,表示是否公平。默认是false,即NonfairSync(非公平锁),这个类不保证线程获得许可证的顺序,调用acquire方法的线程可能在一直等待的线程之前获得一个许可证。如果fair参数传入true,这样使用的是FairSync(公平锁),可以确保按照各个线程调用acquire方法的顺序获得许可证。

非公平锁与公平锁的区别是?

对于非公平锁,当一个线程A调用acquire方法时,会直接尝试获取许可证,而不管同一时刻阻塞队列中是否有线程也在等待许可证,如果恰好有线程C调用release方法释放许可证,并唤醒阻塞队列中第一个等待的线程B,此时线程A和线程B是共同竞争可用许可证,不公平性就体现在:线程A没任何等待就和线程B一起竞争许可证了。

和非公平策略相比,FairSync中多一个对阻塞队列是否有等待的线程的检查,如果没有,就可以参与许可证的竞争;如果有,线程直接被插入到阻塞队列尾节点并挂起,等待被唤醒。

 

参考:

https://www.cnblogs.com/dolphin0520/p/3920397.html

https://blog.csdn.net/liangyihuai/article/details/83106584

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章