java并发:AQS独占锁源码详解

版权声明:本文为博主原创文章,未经博主允许不得转载。你想转载请附加连接哦 https://blog.csdn.net/dmw412724/article/details/83154019

说明:

AQS是并发包的基石。它有两种模式:独占模式和共享模式。本篇只说独占模式。

什么是独占模式?就相当于lock的锁只有一把,一条线程占用,其他线程就得处于BLOCK或者WAIT状态。

在AQS里,获取的方法就是:

Acquire()

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

这个方法比较恶心,拆解成下面的可视性比较强的。

boolean acquire;//是否获取成功
boolean putQueue;//放入队列等行为是否成功
acquire = tryAcquire(arg);//1.尝试去获取
if(!acquire ){//如果没有获取到
    Node node = addWaiter(Node.EXCLUSIVE);//2.新建个节点。
    putQueue= acquireQueued(node,arg);//3.把节点放入队列,并且阻塞队列。
    if (putQueue){
        selfInterrupt();//4.中断线程。
    }
}

依次查看方法:

tryAcquire()

 protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

这个方法什么都没有干,就抛了个异常。按照其他博主的说明是,AQS是一个抽象类,尝试去获取的方法应该由子类自己去实现,按理说本方法应该写成抽象的。但是由于AQS有独占模式和共享模式,那么独占模式的tryAcquire的写成抽象方法,共享模式的tryAcquireShared也得写成抽象的。但是对于继承者来说只需要其中一样功能即可。所以作者写成了可继承的类似于适配器的模式。

addWaiter();

这个方法传来了一个Node.EXCLUSIVE = null的一个参数。

得说下Node,就是线程的节点,是AQS的内部类,他们之间的结构就相当于链表结构。

AQS本身有两个属性,分别是Node head和Node tail。也就是节点头和节点尾的意思。

而Node自身也有两个属性,即Node pre和Node net,也就是上个节点和下个节点的意思。

这样的话,这样一串有头有尾的节点就形成了。

Node也有个Thread字段,作为参数传入了构造方法。这样一连串的Node就相当于一连串的有序线程。

Node 还有个属性是status,在独占模式下它有几个有用的值。

        
        当Node 当status分别为下列值时,则该Node的线程代表着下列意思。
       1. 默认值 int = 0;//说明本线程节点根本未处理,还在初始化的状态
        //线程已超时或取消。
       2. static final int CANCELLED =  1;
        //代表着该节点的下一个节点是阻塞状态.可以想象认为整个正常线程节点串都是SIGNAL状态
       3. static final int SIGNAL    = -1;

具体还是看看addWaiter代码怎么实现的。

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

这里最后还有个enq方法。看下

//在上个代码片里可以看到,该方法的执行是有前置条件的。
//条件是:节点串没有最后一个,也就是节点串是空的。

/*那么依据前置条件该方法的意思是:
第一次循环:
如果节点串是空的,就创建一个空节点(没有放线程进去)然后放到首位。
第二次循环:
把当前节点追加到空节点后面*/
private Node enq(final Node node) {
        for (;;) {
            /
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

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);
                    //把原来的头部的后置节点设置为Null,方便GC。相当于把原来的头节点抛弃了。
                    p.next = null; // help GC
                    failed = false;
                    //返回 中断 = false。
                    return interrupted;
                }

                

                //如果当前节点排队排很多,前置节点不是头。执行下面的方法
                
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

又有个shouldParkAfterFailedAcquire方法,来看下。这里用到了Node.status。上面里有节点状态说明

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
       //获取前置节点的状态
         int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
           //如果前置节点的状态是SIGNAL,那么就这里返回true。目的是进入下一个方法.
            return true;
        if (ws > 0) {
           //如果前置节点的状态被取消了。为什么会被取消?可能是任务中途不执行了,比如IO读一半嫌时间太长,那么这个节点不能影响后续节点啊。
            //这里做的就是一直for循环往前遍历节点,直到找到一个没有取消的,然后追加到后面。
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //初始状态是0,会进入这个方法.将设置为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

我们可以看到因为新建的线程节点初始状态都是0 那么会将状态设置为SIGNAL,然后返回false.

这个时候再回到acquireQueued方法里这个地方:

for(;;){
    ....
       if (shouldParkAfterFailedAcquire(p, node) &&
          parkAndCheckInterrupt())
          interrupted = true;
       ...
}

因为shouldParkAfterFailedAcquire返回了false,所以由于&&具有短路效应,不会执行接下来的方法,而是跳入循环,又来到了shouldParkAfterFailedAcquire里面,但这个时候状态已经是SIGNAL了.该方法返回了true.然后到了parkAndCheckInterrupt这个方法里.

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

LockSupport.park(this)的意思就是阻塞当前线程,.该线程就停留在了第二行,不会执行第三行的代码.

这样说来,AQS的节点串,每过来一个线程都会追加到节点串后面,然后修改状态为SIGNAL,然后变成阻塞的状态.(当然头部的那个比较特殊,根本没有阻塞,而是直接获取了.)非常完美.

关于阻塞后面的代码先不说.因为线程已经阻塞了.我们先看看释放的代码吧.

release()

public final boolean release(int arg) {
        if (tryRelease(arg)) {//这个还是抛异常,如果你连续看这篇文章,你会明白什么意思的
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

上面这个方法取到节点头,然后进入了unparkSuccessor方法

private void unparkSuccessor(Node node) {
        //拿到头节点的状态
        int ws = node.waitStatus;
        //如果头节点正常,那么让头节点的状态初始化.
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        //获取第二个节点.
        Node s = node.next;
        //如果第二个节点不存在或已超时.那么从尾节点开始往前撸,
        //一直撸到距离头节点最近的那个有效的节点t.
        //把第二个节点设置为节点t
        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);
    }

当第二个节点线程不阻塞后.再来看下acquireQueued这个方法.

它再次经历了for循环.来到了

 for (;;) {
                //获取当前节点的前置节点。
                final Node p = node.predecessor();
                //判断前置节点是头吗?且还要执行一遍获取资源的操作。
                if (p == head && tryAcquire(arg)) {
                   //把头部设置成当前节点。
                    setHead(node);
                    //把原来的头部的后置节点设置为Null,方便GC。相当于把原来的头节点抛弃了。
                    p.next = null; // help GC
                    failed = false;
                    //返回 中断标志。
                    return interrupted;
                }

这个时候就跳出了for循环了.

(但也有可能非第二节点遭到外部力量唤醒,那就去下面再阻塞一次)

至于interrupted这个东西以及acquire()里的selfInterrupt();作用是这样的:

Thread.interrupted()方法在返回中断标记的同时会清除中断标记,

也就是说当由于中断醒来然后获取锁成功,那么整个acquireQueued方法就会返回true表示是因为中断醒来,但如果中断醒来以后没有获取到锁,继续挂起,由于这次的中断已经被清除了,下次如果是被正常唤醒,那么acquireQueued方法就会返回false,表示没有中断。

猜你喜欢

转载自blog.csdn.net/dmw412724/article/details/83154019