一、信号量
信号量机制常用于服务限流,限制了同一时间对外提供的服务数量。这有点类似于令牌机制,想要调用服务,则首先需要获取令牌,服务调用完毕后归还令牌。而令牌的数量是有限的,未能获取到令牌的线程则只能等待其他线程归还令牌后再调用服务。这就实现了服务限流,Spring Cloud中的Hystrix就是使用了类似的机制实现了服务熔断。
1.1 信号量基础使用示例
public class Demo325 {
public static void main(String[] args) {
Demo325 demo = new Demo325();
int N = 10; // 客人数量
Semaphore semaphore = new Semaphore(5); // 令牌数量,限制请求数量
for (int i = 0; i < N; i++) {
String vipNo = "vip-0" + i;
new Thread(() -> {
try {
// 获取令牌
semaphore.acquire();
// 业务代码
demo.service(vipNo);
// 释放令牌
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
// 业务代码:限流 控制5个线程 同时访问
public void service(String vipNo) throws InterruptedException {
System.out.println(" -> 楼上出来迎接贵宾一位,贵宾编号" + vipNo + ",...");
Thread.sleep(new Random().nextInt(1000));
System.out.println(" <- 欢送贵宾出门,贵宾编号" + vipNo);
}
}
执行结果:可以看到,最多只有5个线程在调取服务
-> 楼上出来迎接贵宾一位,贵宾编号vip-00,...
-> 楼上出来迎接贵宾一位,贵宾编号vip-01,...
-> 楼上出来迎接贵宾一位,贵宾编号vip-02,...
-> 楼上出来迎接贵宾一位,贵宾编号vip-03,...
-> 楼上出来迎接贵宾一位,贵宾编号vip-04,...
<- 欢送贵宾出门,贵宾编号vip-04
-> 楼上出来迎接贵宾一位,贵宾编号vip-05,...
<- 欢送贵宾出门,贵宾编号vip-03
-> 楼上出来迎接贵宾一位,贵宾编号vip-06,...
<- 欢送贵宾出门,贵宾编号vip-00
-> 楼上出来迎接贵宾一位,贵宾编号vip-07,...
<- 欢送贵宾出门,贵宾编号vip-05
-> 楼上出来迎接贵宾一位,贵宾编号vip-08,...
<- 欢送贵宾出门,贵宾编号vip-06
-> 楼上出来迎接贵宾一位,贵宾编号vip-09,...
<- 欢送贵宾出门,贵宾编号vip-01
<- 欢送贵宾出门,贵宾编号vip-02
<- 欢送贵宾出门,贵宾编号vip-09
<- 欢送贵宾出门,贵宾编号vip-07
<- 欢送贵宾出门,贵宾编号vip-08
1.2 信号量底层探究
通过追踪Semaphore的源码可以看到,其实现与ReentrantLock类似:内置了一个AbstractQueuedSynchronizer的抽象实现类Sync与Sync的2个子类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);
}
与ReentrantLock类似的,Semaphore提供了如下共享模式下的2+2个方法:tryAcquireShared、tryReleaseShared与acquireShared、releaseShared。而tryAcquireShared、tryReleaseShared的具体实现依旧是交由NonfairSync、FairSync实现的。
以acquire()为例,Semaphore的调用流程如下(采用FairSync):
1.3 自定义实现一个简易的信号量
1.3.1 首先自定义一个AQS,实现acquire、release方法,将tryAcquireShared、tryReleaseShared交由具体的子类实现:
public abstract class MyAQS {
public volatile LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();
public volatile AtomicInteger state ;
public MyAQS(int count) {
this.state = new AtomicInteger(count);
}
public int tryAcquireShared(){
throw new UnsupportedOperationException();
}
public boolean tryReleaseShared(){
throw new UnsupportedOperationException();
}
public void acquire(){
boolean addQ = true;
while(tryAcquireShared() < 0) {
if (addQ) {
// 没拿到锁,加入到等待集合
waiters.offer(Thread.currentThread());
addQ = false;
} else {
// 阻塞当前的线程
LockSupport.park(); // 写在while代码块中防止伪唤醒
}
}
waiters.remove(Thread.currentThread()); // 把线程移除
}
public void release(){
if (tryReleaseShared()) {
// 通知等待者
Iterator<Thread> iterator = waiters.iterator();
while (iterator.hasNext()) {
Thread next = iterator.next();
LockSupport.unpark(next); // 唤醒
}
}
}
}
1.3.2 实现tryAcquireShared、tryReleaseShared方法:
public class MySemaphoreDemo {
public static void main(String[] args) {
MyAQS semaphore = new MyAQS(5) {
@Override
public int tryAcquireShared() {
for(;;) {
int count = super.state.get();
int n = count - 1;
if(count <= 0 || n < 0) {
return -1;
}
if( super.state.compareAndSet(count, n)) {
return super.state.get();
}
}
}
@Override
public boolean tryReleaseShared() {
return super.state.incrementAndGet() >= 0;
}
};
int N = 10; // 客人数量
for (int i = 0; i < N; i++) {
String vipNo = "vip-0" + i;
new Thread(() -> {
try {
// 获取令牌
semaphore.acquire();
// 业务代码
service(vipNo);
// 释放令牌
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
// 限流 控制5个线程 同时访问
public static void service(String vipNo) throws InterruptedException {
System.out.println(" -> 楼上出来迎接贵宾一位,贵宾编号" + vipNo + ",...");
Thread.sleep(new Random().nextInt(1000));
System.out.println(" <- 欢送贵宾出门,贵宾编号" + vipNo);
}
}
执行后同样可以看到,还是最多只有5个线程在调取服务。
二、计数器:CountDownLatch
2.1 CountDownLatch概述:
CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。而CountDownLatch实际的作用也是如此,其构造方法需要传入一个int型的数值,相当于是倒计时的时常,CountDownLatch对象提供了await、countdown方法。分别用于线程阻塞与倒计时-1。
假设有如下场景,有3个子线程在执行一些业务逻辑,而主线程需要在这些子线程执行完毕之后,再去做一些额外的收尾工作。这时就可以使用CountDownLatch:主线程可以调用CountDownLatch对象的await方法进入休眠状态,子线程在执行完任务时,执行CountDownLatch对象的coutndown方法(进行倒计时),在所有子线程执行完毕之后,主线程就会被唤醒:
public class MyCountdownLatchDemo {
private static CountDownLatch countDownLatch = new CountDownLatch(3);
public static void main(String[] args) throws InterruptedException {
new Thread(MyCountdownLatchDemo::doSomething).start();
new Thread(MyCountdownLatchDemo::doSomething).start();
new Thread(MyCountdownLatchDemo::doSomething).start();
countDownLatch.await();
System.err.println(Thread.currentThread().getName()+"开始执行收尾工作……");
}
private static void doSomething(){
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println(Thread.currentThread().getName()+"执行完毕");
countDownLatch.countDown();
}
}
执行结果:
Thread-2执行完毕
Thread-1执行完毕
Thread-0执行完毕
main开始执行收尾工作……
2.2 CountDownLatch源码探究
CountDownLatch的源码相对于ReentrantLock、Semaphore可以来说是比较简单的:调用await的时候其实是去tryAcquireShared,当然大部分情况下state是不为0的,则加入阻塞线程的链表。而子线程每次执行countdown()的时候,就将state-1,并尝试tryAcquireShared,而当state为0的时候,就去唤醒阻塞链表中的线程。
public class CountDownLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
……
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
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))
return nextc == 0;
}
}
}
……
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void countDown() {
sync.releaseShared(1);
}
}
三、线程栅栏:CyclicBarrier
3.1 CyclicBarrier概述
CyclicBarrier又称为循环栅栏、线程屏障。可以让一组线程到达屏障时被阻塞,直到最后一个线程到达后,他们一起被执行。CyclicBarrier好比一扇门,默认情况下关闭状态,直到所有线程都就位,门才打开,使其通行,
public class MyCyclicBarrierDemo {
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
@Override
public void run() {
System.out.println("有2个线程已就绪,开始批量处理 ");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
public static void main(String[] args) {
new Thread(MyCyclicBarrierDemo::doSomething).start();
new Thread(MyCyclicBarrierDemo::doSomething).start();
}
private static void doSomething(){
System.err.println(Thread.currentThread().getName()+"准备就绪");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.err.println(Thread.currentThread().getName()+"开始执行业务");
}
}
执行结果:
Thread-0准备就绪
Thread-1准备就绪
有2个线程已就绪,开始批量处理
Thread-1开始执行业务
Thread-0开始执行业务
3.2 CyclicBarrier源码分析
CyclicBarrier倒也没有直接使用AQS,而是直接使用了ReentrantLock。await方法的调用逻辑如下: