后端进阶笔记09: AQS进阶:信号量、计数器与信号栅栏

471 阅读6分钟

一、信号量

信号量机制常用于服务限流,限制了同一时间对外提供的服务数量。这有点类似于令牌机制,想要调用服务,则首先需要获取令牌,服务调用完毕后归还令牌。而令牌的数量是有限的,未能获取到令牌的线程则只能等待其他线程归还令牌后再调用服务。这就实现了服务限流,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个方法:tryAcquireSharedtryReleaseSharedacquireSharedreleaseShared。而tryAcquireSharedtryReleaseShared的具体实现依旧是交由NonfairSyncFairSync实现的。

以acquire()为例,Semaphore的调用流程如下(采用FairSync):

20200318162944

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的源码相对于ReentrantLockSemaphore可以来说是比较简单的:调用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方法的调用逻辑如下:

20200318201436