AQS共享锁应用之Semaphore/CountDownLatch/CyclicBarrier
一、信号量Semaphore
控制多个线程拥有许可数,用于处理并发限流。acquire请求许可,release释放许可
1、简单示例
public class SemaphoreTest {
private int[] info = new int[6];// 下标为桌号,置为使用状态
public static void main(String[] args) {
SemaphoreTest sht = new SemaphoreTest();
Semaphore semaphore = new Semaphore(5);// 火锅店桌子数5个
for (int i = 1; i < 11; i++) {// 10波人来就餐
int id = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();//颁发许可,限流
sht.service(id);
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
private int getNo() {
for (int i = 1; i < info.length; i++) {
if (info[i] != 1) {
info[i] = 1;
return i;
}
}
return 0;// 加桌哈哈
}
private void service(int id) {
int no = getNo();// 获取桌牌号
System.out.println(no + "号桌来客..." + id);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
info[no] = 0;// 打扫桌子
System.out.println(no + "号桌用餐结束,有请下一位...");
}
}
输出结果:
2号桌来客…2
5号桌来客…5
3号桌来客…3
4号桌来客…4
1号桌来客…1
2号桌用餐结束,有请下一位…
3号桌用餐结束,有请下一位…
1号桌来客…6
4号桌用餐结束,有请下一位…
1号桌用餐结束,有请下一位…
5号桌用餐结束,有请下一位…
4号桌来客…9
3号桌来客…8
2号桌来客…7
5号桌来客…10
4号桌用餐结束,有请下一位…
1号桌用餐结束,有请下一位…
2号桌用餐结束,有请下一位…
5号桌用餐结束,有请下一位…
3号桌用餐结束,有请下一位…
2、实现原理
跟踪信号量acquire方法我们发现,Semaphore中底层还是使用了AQS的API来实现限流共享的。
//Semaphore的acquire方法
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
//调用sync父类-AQS的的获取资源方法
sync.acquireSharedInterruptibly(permits);
}
//AQS的acquire方法
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())//可中断
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)//这里获取锁资源就要看可用资源数,没有资源则进入阻塞等待
doAcquireSharedInterruptibly(arg);
}
//tryAcquireShared方法是在使用者类中实现的
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();//查看可用资源数,这里就用到了AQS的state属性
int remaining = available - acquires;//预减,方式CAS设置为负值
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;//返回结果成功则>=0,失败则<0
}
3、自己实现信号量及AQS共享锁
- 补充自己AQS共享锁操作
public class MyAQS {
public volatile AtomicReference<Thread> flag = new AtomicReference<Thread>();// 锁标志位
volatile LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<Thread>();// 存放等待线程
public volatile AtomicInteger state = new AtomicInteger();
public boolean tryAcquire() {// 需要使用者自己实现
throw new UnsupportedOperationException();
}
public void acquire() {
boolean in = true;// 放置多次入队
while (!tryAcquire()) {
if (in) {// 没拿到锁则进入队列等待
waiters.offer(Thread.currentThread());
in = false;
LockSupport.park();// 当前线程挂起,阻塞执行
}
}
waiters.remove(Thread.currentThread());// 拿到锁则从等待队列中移除
}
public boolean tryRelease() {
throw new UnsupportedOperationException();
}
public void release() {// 释放锁资源操作交给子类,这里定义了释放资源后的操作
if (tryRelease()) {
Iterator<Thread> it = waiters.iterator();
while (it.hasNext()) {
LockSupport.unpark(it.next());
}
}
}
public int tryAcquireShared() {// 需要使用者自己实现
throw new UnsupportedOperationException();
}
public void acquireShared() {
boolean in = true;// 放置多次入队
while (tryAcquireShared() < 0) {// 当前资源是否占用完
if (in) {// 没拿到锁则进入队列等待
waiters.offer(Thread.currentThread());
in = false;
LockSupport.park();// 当前线程挂起,阻塞执行
}
}
waiters.remove(Thread.currentThread());// 拿到锁则从等待队列中移除
}
public boolean tryReleaseShared() {
throw new UnsupportedOperationException();
}
public void releaseShared() {
if (tryReleaseShared()) {
Iterator<Thread> it = waiters.iterator();
while (it.hasNext()) {
LockSupport.unpark(it.next());
}
}
}
public AtomicInteger getState() {
return state;
}
public void setState(AtomicInteger state) {
this.state = state;
}
}
- 自己实现信号量
public class MySemaphore {
MyAQS aqs = new MyAQS() {
@Override
public int tryAcquireShared() {// 资源-1
for (;;) {
int val = getState().get();
int n = val - 1;// 预减,看资源是否可用
if (n < 0) {
return -1;
}
if (getState().compareAndSet(val, n)) {
return 1;
}
return -1;
}
}
@Override
public boolean tryReleaseShared() {// 资源+1
return getState().incrementAndGet() >= 0;
}
};
public MySemaphore(int args) {
aqs.getState().set(args);// 设置资源数量
}
public void acquire() {
aqs.acquireShared();
}
public void release() {
aqs.releaseShared();
}
}
- 测试
public class SemaphoreTest {
private int[] info = new int[6];// 下标为桌号,置为使用状态
public static void main(String[] args) {
SemaphoreTest sht = new SemaphoreTest();
// Semaphore semaphore = new Semaphore(5);// 火锅店桌子数5个
MySemaphore semaphore = new MySemaphore(5);
for (int i = 1; i < 11; i++) {// 10波人来就餐
int id = i;
new Thread(new Runnable() {
@Override
public void run() {
semaphore.acquire();
sht.service(id);
semaphore.release();
}
}).start();
}
}
private int getNo() {
for (int i = 1; i < info.length; i++) {
if (info[i] != 1) {
info[i] = 1;
return i;
}
}
return 0;// 加桌哈哈
}
private void service(int id) {
int no = getNo();// 获取桌牌号
System.out.println(no + "号桌来客..." + id);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
info[no] = 0;// 打扫桌子
System.out.println(no + "号桌用餐结束,有请下一位...");
}
}
输出:
1号桌来客…1
4号桌来客…4
3号桌来客…3
2号桌来客…2
5号桌来客…5
4号桌用餐结束,有请下一位…
1号桌用餐结束,有请下一位…
1号桌来客…8
3号桌用餐结束,有请下一位…
5号桌用餐结束,有请下一位…
3号桌来客…6
4号桌来客…9
2号桌用餐结束,有请下一位…
5号桌来客…7
2号桌来客…10
1号桌用餐结束,有请下一位…
3号桌用餐结束,有请下一位…
5号桌用餐结束,有请下一位…
2号桌用餐结束,有请下一位…
4号桌用餐结束,有请下一位…
二、CountDownLatch
jdk1.5引入的工具类,称作“倒计数器”。await方法会阻塞等待计数器值变为0,countdown方法将计数器值-1直到为0。常用于多线程之间交互,等待其他线程执行到某一节点,在继续执行当前线程代码。
1、简单示例
模拟业务场景:并行执行不同接口,整合数据
public class CountDownLatchTest {
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(10);// 参与计数的线程数,包括最后一个执行的线程
for (int i = 0; i < 10; i++) {
int n = i;
new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("调用接口" + n + "完毕!");
latch.countDown();// 调用完成,计数减一
}).start();
}
try {
latch.await();// 主线程等待其他线程执行完毕
System.out.println("业务最终入库...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果:
调用接口6完毕!
调用接口4完毕!
调用接口8完毕!
调用接口7完毕!
调用接口5完毕!
调用接口0完毕!
调用接口2完毕!
调用接口3完毕!
调用接口1完毕!
调用接口9完毕!
业务最终入库…
2、实现原理
- await
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);//AQS的方法
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
//CountDownLatch实现的tryAcquireShared逻辑
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;//大于0为获取到锁,否则阻塞
}
- countDown
public void countDown() {
sync.releaseShared(1);//AQS的方法
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
//CountDownLatch实现的tryReleaseShared逻辑
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))//释放为0时对于最后一个执行的线程才算真正释放成功
return nextc == 0;
}
}
3、自己实现CountDownLatch
这里我们不需要再去修改自己的AQS,只需按模板方法实现获取和释放资源的逻辑。
public class MyCountDownLatch {
MyAQS aqs = new MyAQS() {
@Override
public int tryAcquireShared() {
return aqs.getState().get() == 0 ? 1 : -1;// 当前不等于0则继续等待
}
@Override
public boolean tryReleaseShared() {
return aqs.getState().decrementAndGet() == 0;// 为0的时候才去唤醒后续线程
}
};
public MyCountDownLatch(int count) {
aqs.getState().set(count);// 设置到AQS state中
}
public void countDown() {
aqs.releaseShared();
}
public void await() {
aqs.acquireShared();
}
}
test:
public class CountDownLatchTest {
public static void main(String[] args) {
// CountDownLatch latch = new CountDownLatch(10);// 参与计数的线程数
MyCountDownLatch latch = new MyCountDownLatch(10);
for (int i = 0; i < 10; i++) {
int n = i;
new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("调用接口" + n + "完毕!");
latch.countDown();// 调用完成,计数减一
}).start();
}
latch.await();// 主线程等待其他线程执行完毕
System.out.println("业务最终入库...");
}
}
输出结果:
调用接口4完毕!
调用接口1完毕!
调用接口7完毕!
调用接口6完毕!
调用接口5完毕!
调用接口0完毕!
调用接口3完毕!
调用接口2完毕!
调用接口8完毕!
调用接口9完毕!
业务最终入库…
3、CyclicBarrier
CyclicBarrier也是JDK1.5引入的一个并发工具类,见名知意,循环屏障 或循环栅栏,意思是可重复利用,这点与CountDownLatch有区别。用于多个线程间相互等待场景,设置一个节点,等到所有线程都到达这个节点,所有线程再开始后续工作。
1、简单示例
模拟几个朋友相约去餐馆吃饭,最后一个人到达后大家才开始吃饭(第一栅栏),等到最后一个热吃完饭大家才散会(第二栅栏)
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier cb=new CyclicBarrier(10,()->{
System.out.println("我是最后一个完事儿的人,大家可以继续了.");//最后一个到达的任务
});
for (int i = 0; i < 10; i++) {
int n=i;
new Thread(()->{
System.out.println(n+"号在路上。。。");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(n+"已到达。。。");
try {
cb.await();//等待所有人到达
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("开始吃饭...");
System.out.println(n+"号吃完了...");
try {
cb.await();//等待所有人吃完
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
输出结果:
0号在路上。。。
5号在路上。。。
3号在路上。。。
2号在路上。。。
7号在路上。。。
4号在路上。。。
9号在路上。。。
1号在路上。。。
8号在路上。。。
6号在路上。。。
1已到达。。。
4已到达。。。
8已到达。。。
6已到达。。。
5已到达。。。
3已到达。。。
7已到达。。。
0已到达。。。
9已到达。。。
2已到达。。。
我是最后一个完事儿的人,大家可以继续了.
开始吃饭…
2号吃完了…
开始吃饭…
开始吃饭…
8号吃完了…
开始吃饭…
开始吃饭…
9号吃完了…
开始吃饭…
0号吃完了…
开始吃饭…
7号吃完了…
开始吃饭…
3号吃完了…
开始吃饭…
5号吃完了…
开始吃饭…
6号吃完了…
1号吃完了…
4号吃完了…
我是最后一个完事儿的人,大家可以继续了.
注意与CountDownLatch使用的区别:
CountDownLatch是在多个线程都执行的countDown后才触发事件,唤醒await在latch上的线程,而执行countDown的线程,在执行完countDown后继续自己线程的工作;
CyclicBarrier是一个栅栏,用于同步所有调用await方法的线程,并且等所有线程都到了await方法的时候,它们一起返回继续各自的工作。
2、实现原理
- 成员
//静态内部类Generation
private static class Generation {
boolean broken = false;
}
//同步操作锁
private final ReentrantLock lock = new ReentrantLock();
//线程拦截器
private final Condition trip = lock.newCondition();
//每次拦截的线程数
private final int parties;
//当前最后到达执行任务
private final Runnable barrierCommand;
//表示当前阶段
private Generation generation = new Generation();
//计数器
private int count;
- 两个构造方法
public int getParties() {
return parties;
}
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;//维护内部计数器
this.barrierCommand = barrierAction;
}
先理一遍:构造时我们传入参与线程数和最后执行任务,CyclicBarrier内部会维护一个计数器,用于每一层栅栏上拦截的次数,每次await都会-1,调用的线程进入条件队列trip阻塞。当count为0时就会执行最后一个线程操作,并且唤醒所有等待线程,然后重置内部计数器等数据进入下一个阶段,因此可以重复设置栅栏。
- 两个await方法
最后都会调用dowait方法,根据参数实现定时非定时
//一直等待
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
//定时等待
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
- dowait
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;//当前执行阶段
if (g.broken)//判断是否被破坏
throw new BrokenBarrierException();
if (Thread.interrupted()) {//判断是否有线程中断
breakBarrier();//一旦有线程中断,则破坏栅栏
throw new InterruptedException();
}
int index = --count;//计数器-1看是否所有都到达当前栅栏
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();//执行最终任务
ranAction = true;
nextGeneration();//进入下一阶段
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
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();
}
}
- 切换下一阶段
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();//唤醒所有线程
// set up next generation
count = parties;//重置计数器
generation = new Generation();
}
- 打破栅栏
private void breakBarrier() {
generation.broken = true;
count = parties;//重新计数
trip.signalAll();//唤醒换在等待的线程
}