看了大佬的专栏,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中多一个对阻塞队列是否有等待的线程的检查,如果没有,就可以参与许可证的竞争;如果有,线程直接被插入到阻塞队列尾节点并挂起,等待被唤醒。
参考: