1 CountDownLatch
-
CountDownLatch是一个计数器闭锁,通过它可以完成类似于阻塞当前线程的功能,即:一个线程或多个线程一直等待,直到其他线程执行的操作完成。
-
CountDownLatch用一个给定的计数器来初始化,该计数器的操作是原子操作,即同时只能有一个线程去操作该计数器。调用该类await()方法的线程会一直处于阻塞状态,直到其他线程调用countDown()方法使当前计数器的值逐渐减少,到0为止,每次调用countDown计数器的值减1。
-
当计数器值减至零时,所有因调用await()方法而处于等待状态的线程就会继续往下执行。这种现象只会出现一次,因为计数器不能被重置,如果业务上需要一个可以重置计数次数的版本,可以考虑使用CycliBarrier。
-
CountDownLatch实现的是AQS的共享锁机制。
-
CountDownLatch出现以前,类似功能我们使用线程的join()方法实现。
1.1 重要方法
1.1.1 构造器
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
Sync(int count) {
setState(count);
}
其实CountDownLatch的构造器很简单,count入参表示计数器的次数,CountDownLatch将其赋给了state字段,使用AQS的状态值来表示计数器值。
1.1.2 await()
当前线程调用了CountDownLatch对象的await方法后,当前线程会被阻塞,直到下面的情况之一才会返回:
- 当所有线程都调用了CountDownLatch对象的countDown方法后,也就是说计时器值为 0 的时候。
- 其他线程调用了当前线程的interrupt()方法中断了当前线程,当前线程会抛出InterruptedException异常后返回。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);//这个1,其实在CountDownLatch里面没有用
}
//AQS的获取共享资源时候可被中断的方法
public final void acquireSharedInterruptibly(int arg)throws InterruptedException {
//如果线程被中断则抛异常
if (Thread.interrupted())
throw new InterruptedException();
//尝试看当前是否计数值为0,为0则直接返回,否则进入AQS的队列等待
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);//该方法前文已经论述,详见2.4.2.2 AQS.doAcquireSharedInterruptibly()
}
//CountDownLatch.sync类实现的AQS的接口
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
tryAcquireShared方法是CountDownLatch.sync类实现AQS的接口,只判断了getState()是否等于0,这是计数器有别于其他传统共享锁的核心
1.1.3 await(long timeout, TimeUnit unit)
当线程调用了 CountDownLatch 对象的await(long timeout, TimeUnit unit)方法后,当前线程会被阻塞,直到下面的情况之一发生才会返回:
- 当所有线程都调用了 CountDownLatch 对象的 countDown 方法后,也就是计时器值为 0 的时候,这时候返回 true
- 设置的 timeout 时间到了,因为超时而返回 false;
- 其它线程调用了当前线程的 interrupt()方法中断了当前线程,当前线程会抛出 InterruptedException 异常后返回。
也就是相比于await,引入了一个timeout的概念
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
1.1.4 countDown()
当前线程调用了该方法后,会递减计数器的值,递减后如果计数器为0则会唤醒所有调用await方法而被阻塞的线程,否则什么都不做,接下来看一下countDown()方法内部是如何调用AQS的方法的,源码如下:
public void countDown() {
sync.releaseShared(1);//委托sync调用AQS的方法
}
//AQS的方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();//AQS的释放资源方法,会唤醒head的后继
//后继争锁成功再唤醒后继,使得所有挂起的线程都被唤醒。详见2.4.4.3 AQS.doReleaseShared()
return true;
}
return false;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
//循环进行cas,直到当前线程成功完成cas使计数值(状态值state)减一并更新到state
for (;;) {
int c = getState();
if (c == 0)//如果当前状态值为0则直接返回返回false,
return false;//返回false就不用调用doReleaseShared()了
//这里的if (c == 0)貌似是多余的,其实不然,之所以添加if (c == 0)是为了防止计数器值为 0 后,其他线程又调用了countDown方法,如果没有这里,状态值就会变成负数。
int nextc = c-1;//否则,state-1
if (compareAndSetState(c, nextc))
return nextc == 0;//这里如果返回true,说明当前线程是最后一个调用countDown()方法的线程
//那么该线程除了让计数器减一外,还需要唤醒调用CountDownLatch的await方法而被阻塞的线程。
//所以它返回true,tryReleaseShared中则会调用doReleaseShared,唤醒其他节点。
}
}
1.2 使用demo
package com.lscherish;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class CountDownLatchTest {
private static AtomicInteger id = new AtomicInteger();
// 创建一个CountDownLatch实例,管理计数为ThreadNum
private static volatile CountDownLatch countDownLatch = new CountDownLatch(3);
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("【玩家" + id.getAndIncrement() + "】已入场");
countDownLatch.countDown();
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("【玩家" + id.getAndIncrement() + "】已入场");
countDownLatch.countDown();
}
});
Thread threadThree = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("【玩家" + id.getAndIncrement() + "】已入场");
countDownLatch.countDown();
}
});
// 启动子线程
threadOne.start();
threadTwo.start();
threadThree.start();
System.out.println("等待斗地主玩家进场");
// 等待子线程执行完毕,返回
countDownLatch.await();
System.out.println("斗地主玩家已经满人,开始发牌.....");
}
}
运行结果
等待斗地主玩家进场
【玩家0】已入场
【玩家1】已入场
【玩家2】已入场
斗地主玩家已经满人,开始发牌.....
1.3 与join()相比
CountDownLatch 与 join 方法的区别,一个是调用一个子线程的 join()方法后,该线程会一直被阻塞直到该线程运行完毕,而 CountDownLatch 则使用计数器允许子线程运行完毕或者运行中时候递减计数,也就是 CountDownLatch 可以在子线程运行任何时候让 await 方法返回而不一定必须等到线程结束;另外使用线程池来管理线程时候一般都是直接添加 Runable 到线程池这时候就没有办法在调用线程的 join 方法了,countDownLatch 相比 Join 方法让我们对线程同步有更灵活的控制。
2 Semaphore
Semaphore也叫信号量,在JDK1.5被引入,用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。还可以用来实现某种资源池,或者对容器施加边界。
打个比喻,Semaphore就像一道阀门,可以控制同时进入某一逻辑的线程数量(构造方法中指定),我们使用acquire方法来争取通行票,使用release方法来归还通行票。通行票只是一个比喻,一般我们称之为许可。
- Semaphore内部维护了一组虚拟的许可,许可的数量可以通过构造函数的参数指定。
public Semaphore(int permits) { sync = new NonfairSync(permits); }
- 访问特定资源前,必须使用acquire方法获得许可,如果许可数量为0,该线程则一直阻塞,直到有可用许可。
- 访问资源后,使用release释放许可。
- Semaphore和ReentrantLock类似,获取许可有公平策略和非公平许可策略,默认情况下使用非公平策略。
public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); }
- 当初始值为1时,可以用作互斥锁,并具备不可重入的加锁语义。
- Semaphore将AQS的同步状态(status字段)用于保存当前可用许可的数量。
我们调用Semaphore方法时,其实是在间接调用其内部类或AQS方法执行的。Semaphore类结构与ReetrantLock类相似,内部类Sync继承自AQS,然后其子类FairSync和NoFairSync分别实现公平锁和非公平锁的获取锁方法tryAcquireShared(int arg),而释放锁的tryReleaseShared(int arg)方法则有Sync类实现,因为非公平或公平锁的释放过程都是相同的。
2.1 重要方法
Semaphore在JAVA并发之AQS详解2.4节中有过描述,不论是其公平锁实现还是非公平锁实现,故本文不再赘述,欲了解源码可以阅读JAVA并发之AQS详解2.4节
2.2 使用demo
场景:老师需要4个学生到讲台上填写一张表,但是老师只有2支笔,因此同一时刻只能保证2个学生拿到笔进行填写,没有拿到笔的学生只能等前面的学生填写完毕,再去拿笔进行填写。
package com.lscherish;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
// 2支笔
private static Semaphore semaphore = new Semaphore(2, true);
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(5);
// 5个学生
for (int i = 1; i <=5; i++) {
service.execute(() -> {
try {
System.out.println("同学"+Thread.currentThread().getId() + "想要拿到笔===");
semaphore.acquire();
System.out.println("同学"+Thread.currentThread().getId() + "拿到笔---");
System.out.println("同学"+Thread.currentThread().getId() + "填写中...");
TimeUnit.SECONDS.sleep(2);
System.out.println("同学"+Thread.currentThread().getId() + "填写完毕,马上归还笔。。。");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
});
}
service.shutdown();
}
}
得到结果
同学10想要拿到笔===
同学10拿到笔---
同学10填写中...
同学11想要拿到笔===
同学11拿到笔---
同学11填写中...
同学12想要拿到笔===
同学13想要拿到笔===
同学14想要拿到笔===
同学10填写完毕,马上归还笔。。。
同学11填写完毕,马上归还笔。。。
同学13拿到笔---
同学13填写中...
同学12拿到笔---
同学12填写中...
同学13填写完毕,马上归还笔。。。
同学14拿到笔---
同学14填写中...
同学12填写完毕,马上归还笔。。。
同学14填写完毕,马上归还笔。。。