三种同步器的功能
本文主要介绍三个线程同步器,它们的功能分别是:
- CountDownLatch:确保所有子线程执行完成以后再执行汇总, 内部有一个计数器,一个子线程执行完就倒数一下,倒数完后返回;
- CyclicBarrier:让一组线程全部达到一个状态以后再全部同时执行, 当所有线程执行完毕以后,重置CyclicBarrier的状态之后还可以被重用;
- Semaphore:大家都很熟悉的信号量,内部也有一个计数器但是是递增的,一开始不知道需要同步的线程个数,而是在需要同步的地方调用acquire方法指定需要同步的线程个数。
CountDownLatch
确保所有子线程执行完成以后再执行汇总, 内部有一个计数器,一个子线程执行完就倒数一下,倒数完后返回。首先看一个简单的使用方法:
//创建一个CountDownLatch实例
private static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String args[]) throws InterruptedException{
ExecutorService executorService = Executors.newFixedThreadPool(2);
//将线程A添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(1000);
System.out.println("child threadOne over!");
}catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}finally {
countDownLatch.countDown();
}
}
});
//将线程B添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(1000);
System.out.println("child threadTwo over!");
}catch(InterruptedException e) {
e.printStackTrace();
}finally {
countDownLatch.countDown();
}
}
});
System.out.println("wait all child thread over!");
//等待子线程执行完毕,返回
countDownLatch.await();
System.out.println("all child thread over!");
executorService.shutdown();
}
这里使用了一个线程池,先后创建了两个线程放进池中。然后在主线程中调用CountDownLatch的await()方法,等待两个子线程执行完毕。这个代码的输出大概是这样的:
也可能是这样的:
也就是说,threadOne和threadTwo的顺序可能会交换,因为这取决于线程池是怎么执行它们的。但是wait语句和all over语句一定是一个在开头一个在结尾。因为all over语句是在await()返回以后才输出的,相当于一个最后的汇总方法。
下面我们来看一下CountDownLatch内部的构造。它里面有一个私有类Sync,继承了AbstractQueuedSynchronizer(AQS)抽象类。实际上,它把计数器的值赋给了AQS的状态变量state,用它来表示计数器值。
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
Sync(int count) {
setState(count);
}
它的await()方法,也是委托sync调用了AQS的acquireSharedInterruptibly方法:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
这个方法的代码如下:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 如果线程被中断则抛出异常
if (Thread.interrupted())
throw new InterruptedException();
// 查看当前计数器值是否为0,为0则返回,不为0则进入AQS的队列等待
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
方法很简单,就是查看一下计数器,是0就直接返回,不是0就放到AQS的等待队列里面。后续如果计数器变成0了,CountDownLatch会调用AQS的释放资源方法,在那个方法里面唤醒等待队列里的await()线程并让其返回。
以上这个逻辑的实现在countDown()方法中:
public void countDown() {
sync.releaseShared(1);
}
啊,什么都没有,原来又是调用的sync里的releseShared方法。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 让计数器-1
doReleaseShared(); // AQS的释放资源方法,唤醒await()线程使其返回
return true;
}
return false;
}
再来看一下让计数器-1的关键方法tryReleaseShared():
protected boolean tryReleaseShared(int releases) {
// 循环进行CAS,直至成功完成CAS,使计数器值-1并更新到state
for (;;) {
// 获取当前state值
int c = getState();
// 如果当前state就是0就返回false,因为计数器值已经为0,没办法-1
if (c == 0)
return false;
// CAS执行
int nextc = c - 1;
if (compareAndSetState(c, nextc))
// CAS执行成功,是0的话就返回true,否则返回false
return nextc == 0;
}
}
之所以需要判断当前state是否为0,是为了防止当计数器值已经为0时其他线程又调用了countDown()方法,使得计数器值变为负数。
相比起使用join()方法来实现线程间同步,CountDownLatch更具有灵活性和方便性。在初始化时设置计数器值,线程调用countDown()方法递减计数器值,当计数器值变为0时,激活由于调用await()方法而被阻塞的线程。
CyclicBarrier
功能:让一组线程全部达到一个状态以后再全部同时执行,当所有线程执行完毕以后,重置CyclicBarrier的状态之后还可以被重用。同样看一个使用方法:
//创建一个CyclicBarrier实例,并添加一个所有子线程全部到达屏障后执行的任务
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread() + "task1 merge result");
}
});
public static void main(String args[]) throws InterruptedException{
ExecutorService executorService = Executors.newFixedThreadPool(2);
//将线程A添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
System.out.println(Thread.currentThread() + "task1-1");
System.out.println(Thread.currentThread() + "enter in barrier");
cyclicBarrier.await();
System.out.println(Thread.currentThread() + "enter out barrier");
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
});
//将线程B添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
System.out.println(Thread.currentThread() + "task1-2");
System.out.println(Thread.currentThread() + "enter in barrier");
cyclicBarrier.await();
System.out.println(Thread.currentThread() + "enter out barrier");
}catch(Exception e) {
e.printStackTrace();
}
}
});
executorService.shutdown();
}
也是创建了一个容量为2的线程池并放入两个线程,执行完后的结果是这样的:
可以看出,线程1先执行,到了await那一步后,线程1就“停下来”了,“等待”着线程2也执行到了await那一步,然后执行cyclicBarrier中的那个当所有子线程到达屏障后执行的任务(输出了merge result),然后两个线程才退出屏障。
那么它的可复用性又是怎么一回事呢?再看一个例子:
//创建一个CyclicBarrier实例,并添加一个所有子线程全部到达屏障后执行的任务
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread() + "task2 merge result");
}
});
public static void main(String args[]) throws InterruptedException{
ExecutorService executorService = Executors.newFixedThreadPool(2);
//将线程A添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
System.out.println(Thread.currentThread() + "step1");
cyclicBarrier.await();
System.out.println(Thread.currentThread() + "step2");
cyclicBarrier.await();
System.out.println(Thread.currentThread() + "step3");
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
});
//将线程B添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
System.out.println(Thread.currentThread() + "step1");
cyclicBarrier.await();
System.out.println(Thread.currentThread() + "step2");
cyclicBarrier.await();
System.out.println(Thread.currentThread() + "step3");
}catch(Exception e) {
e.printStackTrace();
}
}
});
executorService.shutdown();
}
以上,跟第一个例子比起来,其他的代码都没变,就是把两个线程try代码块中执行的任务换成了3步。这里我们要求,两个线程都完成了step1才可以开始step2,两个线程都完成了step2才可以开始step3。这就可以通过CyclicBarrier,像上面那样去完成。输出结果如下:
可以看到任务是按顺序执行的,并且cyclicBarrier输出了两次merge。这也告诉我们CyclicBarrier里面的Runnable中的run方法是只要cyclicBarrier到了屏障点就会被触动的。 而且在上面的代码中,没有看到我们显式地去重置cyclicBarrier的状态,所以它的状态应该是到了屏障点就会被触发重置。
有了以上感性的认识以后,我们再来看它的源码。
CyclicBarrier类并没有继承什么类,也没有实现什么接口。它里面内置了一个私有类叫做Generation,也同样没有什么继承类和接口。它有以下几个变量:
// 基于独占锁实现,也就是说底层还是基于AQS
private final ReentrantLock lock = new ReentrantLock();
// 条件变量
private final Condition trip = lock.newCondition();
// parties这个变量用来记录线程个数,也就是有parties个线程调用await()方法后,线程们才会冲破屏障继续往下执行
private final int parties;
// 就是上面构造方法里的Runnable,在线程们冲破屏障时触发其run方法
private final Runnable barrierCommand;
// 初始化generation
private Generation generation = new Generation();
// 记录当前时刻实际上调用了await()方法的线程数,因为parties不能递减(还要被用来复用)
private int count;
构造方法,没什么稀奇的,这里不多说。来看await()方法:
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
好像就是调用了一个dowait()方法嘛。那么接着看dowait()方法:
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
// 拿到锁并且加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 得到当前的generation
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// 重点:count-1,如果此时count为0则证明到屏障点的线程数够了,该重置状态和触发barrierCommand的run方法了
int index = --count;
if (index == 0) { // tripped
// 这个ranAction是用来记录是否调用了barrierCommand的run方法
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
// 激活其他因调用await()而被阻塞的线程,并重置CyclicBarrier
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// 如果count没到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();
}
}
这么长的代码,这就是CyclicBarrier的核心功能代码了。从代码里我们可以看出,generation这个变量实际上是表示了CyclicBarrier过了几“代”,也就是经历了几个“调用await()的线程数从0到parties”的轮回。首先把count-1,如果count到了0就执行barrierCommand的run方法并调用nextGeneration开启下一个轮回(也就是唤醒条件队列里因await()而阻塞的所有线程并重置CyclicBarrier的状态),如果count没到0就调用trip.await()方法把当前线程塞进条件队列。
看看nextGeneration的代码是不是如我们上面所说的那样:
private void nextGeneration() {
// 唤醒条件队列里面等待的所有线程
trip.signalAll();
// 重置状态
count = parties;
generation = new Generation();
}
确实如此。以上就是CyclicBarrier类的核心代码。
Semaphore
Semaphore比其他两个同步器优越的地方就在于它的计数器是递增的,只在调用acquire方法时才通过参数确定需要同步的线程到底有多少个,所以更加灵活。
用Semaphore实现第一个CountDownLatch的例子,开启两个子线程让他们执行,等到他俩执行完后主线程继续向下运行:
//创建一个Semaphore
private static Semaphore semaphore = new Semaphore(0);
public static void main(String args[]) throws InterruptedException{
ExecutorService executorService = Executors.newFixedThreadPool(2);
//将线程A添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
System.out.println(Thread.currentThread() + "over");
semaphore.release();
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
});
//将线程B添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
System.out.println(Thread.currentThread() + "over");
semaphore.release();
}catch(Exception e) {
e.printStackTrace();
}
}
});
semaphore.acquire(2);
System.out.println("all child thread over");
executorService.shutdown();
}
其中,release()方法相当于让信号量的计数器值递增1,acquire(2)方法说明调用了该方法的线程会一直阻塞,直到信号量的计数变为2才会返回。
现在我们来看Semaphore的源码。它和之前的CountDownLatch差不多,也是内置了一个Sync类,继承了AQS抽象类。不同的是,这个Sync也是一个抽象类,它有两个实现:NonfairSync和FairSync。从字面意义我们能看出,这两种实现分别代表着非公平策略和公平策略。
这里再解释一下,非公平策略和公平策略的区别。如果一个线程被放入了阻塞队列,又有一个线程到来了,此时锁被释放,两个线程争夺锁,公平策略就是保证那个先来的线程先获取锁,而非公平策略则没有这个保证。Semaphore默认的实现是非公平的,看构造函数:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
如果要指定公平/非公平策略的话有:
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
参数是true就是fair,反之则为Nonfair。
接下来看几个重点方法的实现。
acquire方法:当前线程调用该方法的目的是希望获取一个信号量资源。如果当前信号量个数大于0,则当前信号量的计数会减1,然后该方法直接返回。否则如果当前信号量个数等于0,则当前线程会被放入AQS的阻塞队列。
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1); // 传递参数为1说明获取1个信号量资源
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 如果线程被中断就抛异常
if (Thread.interrupted())
throw new InterruptedException();
// 否则调用Sync子类方法尝试获取一个资源
if (tryAcquireShared(arg) < 0)
// 如果获取失败则放入阻塞队列
doAcquireSharedInterruptibly(arg);
}
上面代码中的tryAcquireShared方法是Sync的两个子类实现的。非公平策略是这样的:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
// 主要是调用的这个方法
return nonfairTryAcquireShared(acquires);
}
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
// 获取当前信号量值
int available = getState();
// 计算当前剩余值
int remaining = available - acquires;
// 如果当前剩余值小于0或信号量设置成功则返回
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
这个代码大概就是说,获取就完事了,不管什么先来先得的。而公平性的FairSync就需要额外做一些处理来保证公平的实现:
protected int tryAcquireShared(int acquires) {
for (;;) {
// 如果阻塞队列的前面有已经在等待的线程,就直接返回-1
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
也就是看当前线程节点的前驱节点是否也在等待获取该资源,如果是则自己放弃获取的权限,不跟他争。
最后看看release()方法,它的逻辑其实很简单,就是把信号量加1,再看看阻塞队列里面有没有线程,有的话就选一个信号量个数能被满足的线程把它激活。
public void release() {
// 无参数的话默认释放一个资源
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
// 尝试释放资源,如果释放成功则
if (tryReleaseShared(arg)) {
// 则调用park方法唤醒AQS队列里最先挂起的线程
doReleaseShared();
return true;
}
return false;
}
protected final boolean tryReleaseShared(int releases) {
for (;;) {
// 拿到当前的信号量
int current = getState();
int next = current + releases;
// releases<0,抛异常
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
// 设置信号量
if (compareAndSetState(current, next))
return true;
}
}
以上就是JUC中三个线程同步器的用法与一些源码的解析。下一次大概会写一些关于AQS和它的阻塞队列的笔记,毕竟所有的锁几乎都是基于AQS实现的。