AQS源码——通过Semaphore来看共享锁

前言

我们可以先翻到最后看一下大致的步骤,有了宏观上的把控之后,在阅读源会更好一些!

还有就是这篇文章中哪里有问题一定要指正,防止误人子弟,谢谢大家啦!

用例

//同时只允许两个线程获取资源
Semaphore semaphore = new Semaphore(2);
String[] name = {"A","B","C","D"};
for (int i = 0; i < 4; i++){
    Thread thread = new Thread(() -> {
        try {
            //获取锁
            semaphore.acquire();
            Thread.sleep(2000);
            //释放所
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    thread.setName(name[i]);
    thread.start();
    try {
            thread.join();
    } catch (InterruptedException e) {
            e.printStackTrace();
    }
}

上述代码中创建了四个线程去获取Semaphore,同一时间只能有两个线程去获取锁;

获取锁

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

sync稍作解释:他是Semaphore的静态内部类,也是AQS(AbstractQueuedSynchronizer)的抽象子类,实现了AQS的部分抽象方法,是此处体现了设计模式中的策略模式。

//AQS的方法
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    //如果线程被中断则抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //下面有两个步骤 
    //用于获取入场券 -> step-1
    if (tryAcquireShared(arg) < 0)
        //获取入场券失败会进入阻塞-> step-2
        doAcquireSharedInterruptibly(arg);
}

补充:AQS有一个成员属性

private volatile int state;

我们可将其视作入场券的数量,入场券数量有限,一个入场券可进入一个线程。下面我们来看看(step-1)获取入场券的方法。

/**
  * 该类是Sync的子类实现了Sync尚未实现的AQS的抽象方法
  * 这是非公平锁,就算有其他线程在等待锁,其他新来的线程也可以插队执行,不用排队
  */
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::nonfairTryAcquireShared(acquires):

final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                //这里getState就是获取入场券的数量
                int available = getState();
                //acquires就是我们需要的入场券数量,当前为1
                int remaining = available - acquires;
                //如果我们的入场券数量为0的话,此时remaining为-1,我们直接返回-1
                //如果我们的入场券数量>0的话,我们就应该拿走一张并修改入场券的数量
                //此处我们用cas+外层for循环保证入场券数量正确的情况下会有一个返回结果
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

此时如果我们成功获取到了入场券,我们就可以执行我们自己的逻辑,否则将执行(step-2)进入等待队列的操作:

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //执行加入等待队列操作 -> step-3
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                //获取他的前置节点
                final Node p = node.predecessor();
                //如果前置节点就是队列的头节点的话
                if (p == head) {
                    //我们再次尝试去获取入场券
                    //(为了减少线程挂起的操作,毕竟上下文切换很耗时)
                    int r = tryAcquireShared(arg);
                    //如果获取后入场券的数量大于等于0说明我们拿到了一张
                    if (r >= 0) {
                        //设置头节点,并唤醒后置节点 step-4-3
                        //第一次就可以走到这里 那么step-4-1、step-4-2就不会执行了
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //如果前置节点不是头节点,或者是头节点但再次获取入场券失败时走到这里
                //对前置节点的状态进行操作 step-4-1
                if (shouldParkAfterFailedAcquire(p, node) &&
                    //对当前线程阻塞 step-4-2
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

补充:AQS有两个队列,一个是等待队列,另一个就是条件队列,等待队列是等待被唤醒的线程所处的队列,条件队列是等待某个条件满足时被移动到等待队列,条件队列的应用之一是在BlockingQueue中被应用的。

下面我们来看一下step-3 加入等待队列:

//mode时Node.SHARED表示节点是共享锁的队列节点
private Node addWaiter(Node mode) {
        //我们把我们的线程封装到该节点
        Node node = new Node(Thread.currentThread(), mode);
        //获取我们的尾节点
        Node pred = tail;
        //如果前置节点不为空
        if (pred != null) {
            node.prev = pred;
            //通过CAS去设置尾节点,因为有可能有多个线程同时加入队列
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //刚才没加入成功则进入到这里
        enq(node);
        return node;
}

private Node enq(final Node node) {
        for (;;) {
            //获取尾节点
            Node t = tail;
            //如果尾节点为空,则队列就是空的
            if (t == null) { // Must initialize
                //采用CAS的方式去设置头节点,只有一个线程可以设置成功
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                //采用CAS+for循环的方式保证本节点一定会加入到队列中
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}

接下来我们看一下step-4-1对前置节点的设置:

在此之前我们补充一下节点的状态:

//节点初始状态是0
// 该节点线程被取消了
static final int CANCELLED =  1;
// 该节点的后续节点需要释放
static final int SIGNAL    = -1;
// 在条件队列是节点的状态
static final int CONDITION = -2;
// 该节点在共享模式下线程唤醒行为传播下去
static final int PROPAGATE = -3;
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        //如果前去节点的状态是SIGNAL则返回true
        if (ws == Node.SIGNAL)
            return true;
        //如果大于0说节点状态是取消状态,就向前遍历把前置节点从队列里剔除掉,
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //此时状态为0或者-3 ,那就设置为-1,告诉前置节点:你后面有个节点需要被唤醒
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

我们接下来看step-4-2 阻塞线程:

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

public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        //是用unsafe.park对线程进行阻塞
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
}

当线程被唤醒后获取到所或者第一次在阻塞之前就获取到了锁我们就进入到了step-4-3:
 

private void setHeadAndPropagate(Node node, int propagate) {
        //将h指向旧的头节点
        Node h = head; 
        //将head指针指向自身节点
        setHead(node);
        
        // 唤醒下一个节点的条件(全都是或者的关系)
        // 1、剩余的入场券比0大
        // 2、旧的头节点是空,这个是不成立的(存疑)
        // 3、旧的头节点的状态小于0,
        //情况:在本线程被唤醒后,另一个持有入场券的线程执行完毕要释放锁但在
        //    doReleaseShared方法中拿到的头节点是0然后设置为了-3.(这里看不懂可以先放一下,后
        //    面看了doReleaseShared的逻辑再回来看就好懂一些了,有些绕大家可以把自己的大脑当成    
        //    是内核试着自己调度一下)
        // 4、新的头节点是空的,这个是不成立的(存疑)
        // 5、新的头节点的状态小于0,情况:
        //情况:只要头节点不是尾节点那么头节点是小于0的
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //如果下一个是空的或者下一个节点是共享状态
            if (s == null || s.isShared())
                //唤醒后继节点
                doReleaseShared();
        }
    }

释放锁

//我们的使用示例中执行完自己的逻辑后调用了释放锁的方法
semaphore.release();

//Semaphore的解锁方法
public void release() {
    sync.releaseShared(1);
}

之后调用了sync父类AQS的释放共享锁的方法:

public final boolean releaseShared(int arg) {
        //归还入场券 step-5
        if (tryReleaseShared(arg)) {
            //成功则调用唤醒等待队列节点的逻辑 strp-6
            doReleaseShared();
            return true;
        }
        return false;
    }

我们先来看step-5归还入场券的逻辑:

//该方法在AQS中是抽象方法,我们要看的实现是在Semaphore中
protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                //CAS+死循环的方式去归还券数量,保证一定会归还成功
                if (compareAndSetState(current, next))
                    return true;
            }
        }

当我们归还成功之后,我们就要执行step-6 唤醒等待队列节点的逻辑:

private void doReleaseShared() {
        for (;;) {
            //获取当前的头节点
            Node h = head;
            //判断该节点既不为空也不是尾节点
            if (h != null && h != tail) {
                //获取头节点的状态
                int ws = h.waitStatus;
                //如果头节点的状态为-1
                if (ws == Node.SIGNAL) {
                    //之后一个线程可以将头节点从-1置为0,其他线程就进入下一轮循环
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    //唤醒下一个节点 step-7
                    unparkSuccessor(h);
                }
                //如果在某线程执行到此处时,会有一种情况:
                //刚判断完ws是0,此时队列中除了头节点没有节点存在
                //然后又加入了一个新的节点,
                //此时ws的状态被变成了-1(这一步是在step-4-1中执行的)
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    //有后续节点加入,尝试唤醒该节点。
                    continue;                // loop on failed CAS
            }
            //如果走到这里h记录的还是头节点那么就结束循环,共享锁释放完毕
            if (h == head)                   // loop if head changed
                break;
        }
}

我们最后来看看step-7 唤醒后继节点:

private void unparkSuccessor(Node node) {
        //此处主要用于独占锁释放时的逻辑,已经完成唤醒后继节点的动作
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        //获取该节点的下一个节点
        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)
                    //找到最前方的一个状态小于等于0的节点
                    s = t;
        }
        //有可能找不到
        if (s != null)
            //唤醒该节点
            LockSupport.unpark(s.thread);
    }

总结

获取锁,大致步骤可分为:

step-1:tryAcquireShared(arg)->获取入场券

step-2:doAcquireSharedInterruptibly(arg)->获取入场券失败,准备进行阻塞

step-3:addWaiter(Node.SHARED)->加入等待队列

step-4-1:shouldParkAfterFailedAcquire(p, node)->对前置节点进行操作

step-4-2:parkAndCheckInterrupt()->阻塞当前线程

step-4-3:setHeadAndPropagate(node, r)->(当被唤醒时或临阻塞前的一次尝试获取到了锁)设置头节点并唤醒后继节点

释放锁,大致步骤:

step-5:tryReleaseShared(arg)->归还入场券

step-6:doReleaseShared()->唤醒等待队列的节点

step-7:unparkSuccessor(Node node)->唤醒head的后继节点(step-6的内部逻辑)

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值