基于 AQS 分析 Semaphore

135 阅读4分钟

基于 AQS 分析 Semaphore

书痴者文必工,艺痴者技必良 —— 蒲松龄 「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战

代码案例

public class SemaphoreDemo {
    // 停车场有三个车位,设置三个信号量
    private static Semaphore semaphore = new Semaphore(3);

    public static void main(String[] args) {
        //模拟5辆车进入停车场
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {
                        System.out.println("......" + Thread.currentThread().getName() + "来到停车场......");
                        // 查看可用的许可证的数量
                        if (semaphore.availablePermits() == 0) {
                            System.out.println("车位不足,请耐心等待");
                        }
                        semaphore.acquire();//获取令牌尝试进入停车场
                        System.out.println("进入 :"+Thread.currentThread().getName() + "成功进入停车场......");
                        Thread.sleep(new Random().nextInt(10000));//模拟车辆在停车场停留的时间
                        System.out.println("离开 :" + Thread.currentThread().getName() + "驶出停车场......");
                        semaphore.release();//释放令牌,腾出停车场车位
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, i + "号车");
            thread.start();
        }
    }
}

代码说明

模拟停车场的生活场景,如果有车位则进行停车,如果没有车位则进行等待,一个车位相当于一个信号牌子

源码分析

先看一下构造函数

private static Semaphore semaphore = new Semaphore(3);

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

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);
    }
}

Sync(int permits) {
    setState(permits);
}

protected final void setState(int newState) {
    state = newState;
}

此时发现还是通过Sync来修改AQS中的state的值,此时这个state的值是3

再看一下availablePermits()方法

获取当前可利用的许可证的数量

public int availablePermits() {
    return sync.getPermits();
}
final int getPermits() {
    return getState();
}
protected final int getState() {
    return state;
}

其实就是获取一下当前的state的值没有什么过多的复杂逻辑,此时该值是3,再看看 acquire() 获取许可证的方法

再探获取许可证的方法 acquire

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

看看 sync.acquireSharedInterruptibly(1) 这个方法,这里面的参数是1

public final void acquireSharedInterruptibly(int arg)
    throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

看起来和CountDountLatch的await方法挺像的,先看主要逻辑 tryAcquireShared(arg)

protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

看一下非公平获取共享锁的逻辑

final int nonfairTryAcquireShared(int acquires) { // 传入进来的值是1
    for (;;) {
        // 获取可利用的许可证数量此时为3
        int available = getState();
        // 计算还有多少可以用的 3 - 1 = 2
        int remaining = available - acquires;
        // cas设置成功后返回剩余的可利用的许可证数量
        if (remaining < 0 || compareAndSetState(available, remaining)) 
            return remaining;
    }
}

其实当还有许可证的数量的时候,线程调用 acquire 方法,就相当于减少一次state的数量,相当于共享锁减少1,如果当可利用许可证数量没有的时候怎么办,我们此时当state=0,也就是来了三辆车将车位都占满了,再来一辆车的场景,那么此时 remaining 返回的是 0-1 = -1,那么走到这个方法中 doAcquireSharedInterruptibly(arg)

private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { // 传进来的参数是1
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}

方法有点长我们慢慢分析一下,先看这个addWaiter方法,里面传了一个Node.SHARED,其实就是添加了一个Node进入AQS的等待队列

private Node addWaiter(Node mode) {
    // 创建一个Node 节点
    Node node = new Node(Thread.currentThread(), mode);
    // 此时的pred 和 tail指针目前都是空值
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 所以走到了这个入队的方法
    enq(node);
    return node;
}

第一步先创建一个Node 节点,第二步进行了入队的方法,因为此时的pred 和 tail指针目前都是空值,此时的if条件不成立,这个enq的方法真的是看了好多遍了,再巩固一下

private Node enq(final Node node) {
    for (;;) {
        // 此时t指向的是空的,因为tail也是空的,所以需要进行初始化队列
        Node t = tail;
        if (t == null) { // Must initialize
            // 初来乍到,创建一个空的Node节点,之后将空head指针指向Node节点,之后tail尾部指针指向了head指向的头节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

初来乍到,此时t指向的是空的,因为tail也是空的,所以需要进行初始化队列,因为t==null,创建一个空的Node节点,之后将空head指针指向Node节点,之后tail尾部指针指向了head指向的头节点
image.png
此时进行下一次循环,t指向了tail指针,所以此时t不等于null了所以走到了else逻辑,新Node节点将他的前驱指针指向了当前队列的head节点,并且tail指针指向了当前新入队的node节点,之后t【此时的t就相当于head节点】的next指向了当前新入队的node节点,并且返回了头节点,此时队列中的节点样式
image.png
此时该走到下面的for循环里面的逻辑了

for (;;) {
    // 获取当前节点的前驱节点,此时前驱节点就是头节点
    final Node p = node.predecessor();
    // 条件成立
    if (p == head) {
        int r = tryAcquireShared(arg);
        if (r >= 0) {
            setHeadAndPropagate(node, r);
            p.next = null; // help GC
            failed = false;
            return;
        }
    }
    if (shouldParkAfterFailedAcquire(p, node) &&
        parkAndCheckInterrupt())
        throw new InterruptedException();
}

再一次尝试走tryAcquireShared方法获取锁

final int nonfairTryAcquireShared(int acquires) { // 传入进来的值是1
    for (;;) {
        // 获取可利用的许可证数量此时为0
        int available = getState();
        // 计算还有多少可以用的 0 - 1 = -1
        int remaining = available - acquires;
        // -1 < 0
        if (remaining < 0 || compareAndSetState(available, remaining)) 
            return remaining;
    }
}

不过还是失败了,此时r=-1 <0,所以走到这个方法中 shouldParkAfterFailedAcquire(p, node),走到下面挂起的逻辑了,这个逻辑我们也看了好多次了

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 第一次进来的时候此时的头节点的waitStatus还是0或者null呢
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 所以第一次就走到这里了将头节点的waitStatus设置成了-1
          compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

第一次进来的时候此时的头节点的waitStatus还是0或者null呢,所以第一次就走到这里了将头节点的waitStatus设置成了-1,此时队列中的样子变成了下面的样子
image.png
第二次又一次获取锁又失败了之后再次进到shouldParkAfterFailedAcquire这个方法的时候头节点的waitsStatus就是-1了此时第一个if逻辑就成功了,并且返回了true,之后走到下一个方法里面parkAndCheckInterrupt()

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

此时在这个方法中将线程挂起了,其实这个线程调用了await方法的时候就是将自己加入到AQS的等待队列中去了

看看释放许可证的方法

public void release() {
    sync.releaseShared(1);
}

这个是核心方法 sync.releaseShared(1),看看里面的逻辑没准咱们还是看过的

public final boolean releaseShared(int arg) { // 传进来的参数是1
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

看一下这个方法 tryReleaseShared(arg)

protected final boolean tryReleaseShared(int releases) {  // 传进来的参数是1
    for (;;) {
        // 获取当前的state的值,此时为0
        int current = getState();
        // 进行state值变更 1 + 0 =1
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        // cas设置将state值进行加1返回true
        if (compareAndSetState(current, next))
            return true;
    }
}

其实释放许可证的核心逻辑就是state变量进行+1,返回true 走doReleaseShared()方法,其实我感觉这个方法就是唤醒挂起的线程哈哈,果真和CountDownLatch一样

private void doReleaseShared() {
    for (;;) {
        // 定义了一个head指针指向了AQS队列的head节点
        Node h = head;
        // 此时h不是空并且也不是最后一个节点,所以条件成立
        if (h != null && h != tail) {
            // 此时头节点的 waitStatus 是 -1
            int ws = h.waitStatus;
            // 此 if 逻辑成立
            if (ws == Node.SIGNAL) {
                // 将头节点的 waitStatus 从-1 变成了 0
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 唤醒后续的节点来获取锁
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

此方法我们也分析好多次了,看上面代码的注释,以及下面的图
image.png
看看如何唤醒的AQS中等待队列的线程

private void unparkSuccessor(Node node) {// 传进来了一个头节点
    // 此时头节点的waitStatus已经被改成过0了
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 定义一个s指针指向了当前头节点的下一个节点
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 此时s不是空值所以,进行了线程唤醒
    if (s != null)
        LockSupport.unpark(s.thread);
}

看看目前AQS队列中的样子
image.png

分析唤醒之后的逻辑

线程在哪里进行挂起来的,我记得是获取锁的时候被挂起来的我们往前看看
image.png
再尝试重新走获取锁的方法将,state的值减一,也就是将许可证数量减一