深入并发AQS二


AQS需要解决以下几个问题:
1.锁状态,如何保证并发情况下能够安全的更新?
2.当前线程不能获取锁时,放在哪里? AQS是放在一个队列当中

3.如何提高效率?

AQS的主要职责是当获取不到锁时,将线程放入CLH队列(一种变体)。并且当有线程释放当前拥有的锁时,找出header结点下一个结点(代表一个阻塞线程),并
将它唤醒。


由于AQS放等待阻塞线程的队列用的是CLH变体队列,先大致了解下队列。

AQS的队列有一个header结点,这里AQS只有在第一次有线程尝试获取锁但获取不到时,它才会进行创建。

队列中的每个结点代表了一个等待锁的线程,用一个Node进行封装。

Node中有一个next指针批向下一结点,这个next指针主要用于之后唤醒后面线程结点。

一个prev指向指向上一结点。

Node结点有一个waitStatus状态,有signal,cancelled,CONDITION,PROPAGATE,0(初始状态)。

其中主要关注signal状态,这个状态用于暗示当前设置了waitStatus为signal的结点线程,告诉它在释放锁时记得唤醒它后面的结点。

prev指针的作用则是用于获取前结点,这里包括设置前结点的waitStatus。


注:当第一次有线程获取不到锁时,要入队列时,会创建一个虚拟结点,header会指向这个结点,然后tail指向这个要获取锁的线程结点A。

之后,只有header后一个结点,也就是刚刚入队的等待锁的线程结点A可以尝试获取锁。获取到锁后,将header结点指向这个等待锁结点A,释放之前虚拟结点。

拥有锁的线程A,之后调用unlock释放锁,释放锁会将state减1。当state减到0时,会唤醒header(即A结点)后一个等待锁线程结点。

除了第一次获取锁时,header是指向虚拟结点,之后在线程获取到锁后,header线程指向这个获取到锁的线程结点。


所有可以获取锁的线程结点条件为:header结点的下一个结点。


AQS的设计围绕着在适度的情况尽量不让线程挂起,进行CAS获取锁。

AQS本身有两个核心实现方法acquire及:
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
        unparkSuccessor(h);
        return true;
    }
    return false;
}

AQS获取锁与释放锁调用的是acquire与release方法。

acquire方法的职责是:
 如果当前线程不能获取到锁,则将其封装成一个Node放入队列当中,并决定何时将当前线程真正阻塞。
 否则获取到锁时,直接返回。

release的职责是:
  尝试释放锁(如果当前线程没有拥有对象锁,不能进行后续操作,即只有拥有锁的线程才能对锁进行release),
成功,则从队列中找出header下一个结点(存储了阻塞了的线程),调用LockSupport的unpark方法将它唤醒。

aquire及release中的tryAcquire与tryRelease方法交由子类去完成,子类在获取锁及释放锁时增加一些特性,如进行公平锁与非公平锁,超时等特性。


这里所有的阻塞与唤醒操作使用LockSupport的 park(Object blocker)及unpark(Thread thread)方法。

AQS定义了四个方法供子类个性化实现如果可中断,定时获取锁的形式,如下:

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



ReentrantLock就是用AQS作为基础框架,内部使用Sync,这个类继承了AQS。

ReentrantLock类结构如下:

可以看到ReentrantLock有一个sync实例,然后拥有lock,lockINterruptibly等方法。

事实上lock这些方法是给客户调用的,但最终ReentrantLock会将相应的调用交由sync相应方法去处理。

ReentrantLock使用sync,相当于策略模式一样。ReentrantLock为了支持非公平及公平锁,因此在内部同时创建了两个sync的子类,分别对应于公平锁与非公平锁实现。

Sync继承图:



接下去看下一个线程请求一个ReentrantLock锁时发生的大致流程:





可以看到线程想要获取锁,在操作1时,这里具体的获取操作是:
 查看当前锁state是否为0(说明当前锁未被占有),不是则说明已被其它线程获取,进入can not分支。
 如果当前锁state为0,则尝试用cas操作将锁的state改为1。cas成功后再将exclusiveOwnerThread改为当前线程。

ReentrantLock的Sync类的lock代码如下:
  

final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else    
		acquire(1);
}


这里如果当前线程不能立即获取锁时用调用AQS的 acquire方法。

先看下AQS的acquire方法,代码如下:

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

acquirey方法做了以下几件事情:

   调用tryAcquire(子类ReentrantLock去实现)尝试获取锁,如果获取到则退出acquire方法。

 如果tryAcquire获取不到锁,这时先通过addWaiter方法将当前线程封装成一个CLH的Node放入CLH队列。并且调用acquireQueued方法决定何时将当前线程阻塞。


接下去分别对acquire方法中的tryAcquire,addWaiter及acquireQueued进行分析。

tryAcquire是AQS提供给子类用于实现自己个性化的获取锁机制。

ReentrantLock中分别有公平锁(FairSync)及非公平锁(NonFairSync)实现。

这里看非公平锁NonFairSync的实现,代码如下:

protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

可以看到这里最后调用的是nonFairTryAcquire方法,它的实现在Sync类主体上,代码如下:

/**
         * Performs non-fair tryLock.  tryAcquire is
         * implemented in subclasses, but both need nonfair
         * try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

nonFairTryAcquire的之所以是非公平的,是因为任何线程进入这个方法获取锁时都有机会先获取锁,而不管在CLH队列中阻塞(或在进行CAS操作)的线程。

这里会先判断:

  如果当前锁对象state为0进入if(c==0)分支,这里马上尝试CAS操作试图将state状态改为被锁状态。如果成功继续将锁对象上的exclusiveOwnerThread改为当前线程。如果不能CAS成功这里就返回false,表明不能获取到锁。

  如果当前锁对象state不为0,进入else if分支,这时根据锁对象上的exclusiveOwnerThread来判断是否是当前线程拥有了这个锁对象,如果是则将锁占有次数(放在state状态上)加1,并且返回true。


在nonFairTryAcquire方法执行完后,返回到AQS的acquire方法:

if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();

这里如果tryAcquire(arg)返回的是true则表明当前线程已经获取到锁直接返回,否则进行 &&后面的操作。这里会先执行addWaiter方法,这个方法会先当前线程放入到队列。


代码如下:

/**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    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;
    }

可以看到先将当前线程封装成一个Node对象,然后尝试将新结点放入到队尾,这里会判断队尾是否存在.

如果存在,则尝试将它放入队尾。
不存在调用enque方法,创建一个新的header结点使header指针指向这个header结点。tail指针指向这个当前线程结点。

代码如下:

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

可以看到这个方法最终是创建一个header结点,将header指针指向了header结点,并将header结点的next指针指向当前线程结点。而tail指针指向这个当前线程结点。
如图:

然后返回到acquire方法:
if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();

执行acquireQueued方法,这个方法会观察当前在队列中的结点的上一结点看它的状态waitState是否为signal,如果不是则进行一次CAS尝试获取锁,还是不成功的情况下则调用LookSurport的park方法阻塞该线程。
代码如下:

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

可以看到对于这部分来讲,只有头结点后面的一个结点线程才有机会用CAS获取锁。即执行if (p == head && tryAcquire(arg)) 。
这里如果当前结点线程不是头结点下一结点或者说CAS获取锁还是失败,则进入shouldParkAfterFailedAcquire方法,看是否该返回true表明该阻塞该线程了。
则进入shouldParkAfterFailedAcquire方法代码如下:

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

可以看到这里如果当前线程结点上一结点的waitStatus不为signal或者大于1时(表明CANCELLED),这时会先设置前一结点的waitStatus为signal。
然后返回false表明先不阻塞当前线程,返回acquireQueued方法会再走一次for循环。
第二次走acquireQueued的for循环时,发现还是不能获取锁,这时shouldParkAfterFailedAcquire会返回true,然后回到acquireQueued方法就调用parkAndCheckInterrupt方法真正阻塞线程了。

这样完成了线程获取锁的整个流程解析。接下去需要解析释放锁这部分逻辑,相对于线程获取锁简单一点。


 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值