详解AbstractQueuedSynchronizer(上)

AQS是jdk的concurrent包中非常重要也设计得非常好的一个类。

AQS是AbstractQueuedSynchronizer的缩写,翻译成中文就是抽象同步队列,之所以这么命名,就是他是实现同步的一个抽象类。所以很多同步工具都是基于他来实现的。ReentrantLock,CountDownLatch,CyclicBarrier,Semaphore这些类也是变量继承AbstractQueuedSynchronizer来实现线程间的同步的。

1.AbstractQueuedSynchronizer(下面统一简称AQS)是如何设计的呢?

//同步队列的头
private transient volatile Node head;
//同步队列的尾
private transient volatile Node tail;
//同步的条件
private volatile int state;

AQS主要包含三个变量,一个head、一个tail、还有一个state。head和tail是Node类型的变量,分别代表同步队列的头和尾,state代表同步的状态。这三个变量都是volatile修饰的,所以它们的修改都是内存可见的,这是AQS能够成为同步队列的所具备的。

2.如何理解这三个变量呢?

我们可以联想到现实生活中的一个场景,银行柜台取号办业务的场景,第一个去的人取了号码,但是前面没人在办业务,柜台显示空闲的状态,所以他可以直接去柜台办理业务,这时柜台显示正在办理。其他后面去的人取完号后只能到后面去排队,等待轮到自己。
state就是柜台显示的状态,也就是资源的状态,这个state由继承的类自己去定义。比如在ReentrantLock,就定义state为0时资源空闲,state大于0时资源被占用(当然,ReentrantLock是可重入锁,state>1代表重入次数)。head就代表着排队的人的第一个(当前面的人办理完成时,第一个就可以从队列中出来获得资源去办理业务),tail就代表着排队的人的最后一个(当有人来办理业务时,就可以插入最后一个人tail的后面去排队)。

3.Node又是如何设计的呢?

 static final Node SHARED = new Node();
       
 static final Node EXCLUSIVE = null;

 static final int CANCELLED =  1;

 static final int SIGNAL    = -1;

 static final int CONDITION = -2;

 static final int PROPAGATE = -3;

 volatile int waitStatus;

 volatile Node prev;

 volatile Node next;

 volatile Thread thread;

 Node nextWaiter;

Node的变量如上图所示,SHARED和EXCLUSIVE分别代表着你要访问的这个资源是可以共享的还是只能独占的。waitStatus对应当前在等待的这个线程对应的状态,就是CANCELLED,SIGNAL,CONDITION和PROPAGATE这四个值。如果一个线程的状态是CANCELLED,则代表这个线程不再去争取资源了,它后面排队的线程检测到他是CANCELLED状态会将他移出队列。如果是SIGNAL,则代表这个线程如果已经释放资源之后要去通知队列中的下一个线程(当然,这个SIGNAL状态有可能是下一个线程设置的)。prev代表着同步队列中的上一个等待线程,next代表这同步队列中的下一个线程,thread就是存储线程对象的。

4.如何排队取号呢?

知道了排队办业务的场景,那么我们是如何取号的呢?首先AQS提供了acquire(获取独占资源),acquireInterruptibly(可中断获取独占资源),acquireShared(获取共享资源),acquireSharedInterruptibly(可中断获取共享资源)四个方法来提供取号的方法。

(1)acquire()是如何实现的

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

首先会去调用tryAcquire这个方法取尝试获取资源,在AQS中值定义了这个方法,并没有具体的去实现,因为每家银行的取号方式都是不同的,能不能取号成功也是不一样的,所以需要继承的子类具体去重写这个方法。如果tryAcquire返回false,就是获取资源失败后,就去执行addWaiter这个方法,这个方法就是将当前的线程排到同步队列的尾部的方法,就是取完号之后开始进入队列中排队。

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

接下来就是执行acquireQueued这个方法

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这个方法中有一个for循环,for循环里面有两个if判断。第一个if判断中,先获取当前线程节点的前一个节点,如果这个是头节点,并且当前线程节点尝试获取资源成功的话,则将当前节点设置为头节点,并将原来的头节点的next设置为空,然后返回。如果不是头节点的话则第二个if判断,shouldParkAfterFailedAcquire主要进行的是判断当前节点的前序节点是不是SIGNAL状态,如果是的话就返回true(这样的话如果他释放了资源就可以通知到当前去争夺资源),如果不是的话判断前序节点是不是已取消(CANCELLED)状态,如果是则将其移除知道当前节点的前序节点没有已取消状态的线程,如果这两种状态都不是的话则去改变他的前序节点状态为SIGNAL状态。这里可以看出,在同步队列中,第一个对象是已经获得锁的对象。

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        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 {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

还是银行排队的场景,上面整个方法的意思是,当前这个新来的人(B)先去看下自己前面那个人(A)是不是队伍的第一个人,如果是的话是不是已经轮到自己(B)去办理业务了,如果已经轮到自己了(B),而A就可以移除了,因为已经轮到自己的,则自己(B)就成为了排队中的第一个人,就不去休息了(对应返回的interrupted就是false)。如果A不是排队第一个人,就去看A能不能办理完业务通知下自己,然后自己先去休息下。A如果不同意就去看A是不是想等没人的时候再来办理,如果A是这种状态的话就将他踢出排队队伍中,如果一个一个去问,直到把所有有这种想法的人踢出为止,最后让自己前面那个人办完业务之后通知下自己。做完这些事后自己就可以去休息了,然后interrupted就可以设置为true了。

总结一下: acquire方法就是尝试去获取资源,如果没能获取到资源的话就到同步队列中去排队,然后将他的前序节点状态改为SIGNAL,最后挂起自己。

(2)acquireInterruptibly是如何实现的呢?

    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

acquireInterruptibly和acquire方法差不多,只是会去判断当前线程是否中断,如果是中断的话则抛出中断异常。

(3)acquireShared

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }

acquireShared首先也会去调用tryAcquireShared方法去判断是否获取到资源了,同样,这个方法也是由继承AQS的子类去重写。当tryAcquireShared返回的结果小于0说明获取失败了,当返回的结果等于0说明获取成功,但是没有可以再获取的资源了, 所以后面的线程不能再获取。当返回结果大于0说明获取成功,后面的线程还可以再获取。

    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

在上面代码中,也会先去将当前节点封装成一个共享类型的节点,然后添加到同步队列中。再执行一个for循环,再第一个if判断中,先查看当前节点的前序节点是不是头节点,如果是头节点的话则去尝试获取获取资源,获取到的话则执行setHeadAndPropagate方法,并将head的下一个节点置为空,这样头节点就会被垃圾收集器回收了,而当前节点就可以成为头节点并且获取到资源了。

    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }
    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    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;
        }
    }
    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)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

在setHeadAndPropagate这个方法中,先判断propagate是否还大于0或者头节点是否为空或者是非取消状态,如果大于0的还则说明还可以进行资源的获取,那么就获取到当前节点的下一个节点,如果下一个节点还没有或者是共享的节点,则可以唤醒后续节点。doReleaseShared这个方法就是唤醒下一个节点方法的具体实现了。

按照银行的场景可以这么理解,一个咨询会可以安排多个人参加,排队中的第一个人进去之后发现会议室的椅子还有,并且上一个人嘱咐他如果还有座位的话喊他下,所以他就喊了下一个人。直到没有位置为止,或者没有人为止。

(4)acquireSharedInterruptibly方法是如何实现的?

acquireSharedInterruptibly方法和acquireShared方法基本上是一致的,只是会在挂起线程会抛出中断异常。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值