AQS源码分析--独占锁和独占锁的特性

AQS的源码:
1.独占锁的获取(acquire)与释放(release())
2.独占锁的特性:
响应中断	lockInterruptibly()-acquireInterruptibly()
超时获取锁tryLock(long time,TimeUnit unit)-doAcquireNanos(arg,nanosTimeout)

不公平锁:新来的线程和同步队列中的第一个线程竞争,刚新来的线程更容易获取锁,因为它处于运行状态。(synchronized)
公平锁:新来的线程进入同步队列进行排队,同步队列中的第一个线程获取锁。

一、ReetrantLock

1.synchronized与ReentrantLock的关系与区别

共性:都属于独占锁的实现,任意一个时刻只有一个线程能够获取到锁。都支持可重入。

区别:a.synchronized属于JVM层面的管程实现,ReentrantLock属于Java语言层面实现的管程。

b.ReentrantLock有synchronized不具备的特性:响应中断、支持超时、支持非阻塞式地获取锁,公平锁(在构造方法中传参true),支持多个等待队列。

2.lock()

调用lock()是获取独占锁,获取失败就将当前线程加入同步队列,成功则线程执行。

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

lock()先将当前线程的状态由0(初始状态)设置1(将当前线程节点从同步队列中取消)。如果设置成功,则将同步状态持有线程设置为当前线程;否则调用AQS提供的acquire()方法。

下面是acquire()的实现逻辑:

AQS源码分析--独占锁和独占锁的特性_第1张图片

tryAcquire(int arg)://再次尝试获取同步状态。成功,方法直接退出,失败调用addWaier(Node.EXCLUSIVE)。
addWaiter(Node.EXCLUSIVE,arg):将获取同步状态失败的当前线程以指定的模式(独占式/共享式)封装为Node节点后置入同步队列。
同步队列为空或者尾插失败执行:enq(Node node):当前队列为空或者CAS尾插失败调用此方法来初始化队列或者不断尾插。尾插失败:V不等于O,多个线程同时进行尾插。
节点入队后排队获取同步状态:acquireQueued(addWaiter(Node.EXCLUSIVE), arg)):将当前线程以指定模式(独占式、共享式)封装为Node节点后置入同步队列。
节点获取到同步状态的前置条件:当前节点的前驱节点为头节点并且调用tryAcquire获取到了同步状态->前驱头节点出队,当前节点作为头节点。
acquiredQueued()获取锁成功条件:当前节点前驱为头节点并且获取同步状态成功。
acquireQueued():
1.如果当前节点的前驱节点为头节点并且能够成功获取同步状态,当前线程获得锁成功,方法返回。
2.如果获取锁失败,先不断自旋将前驱节点状态置为SIGNAL,而后调用LockSupport.park()方法将当前线程阻塞。
shouldParkAfterFailedAcquire(Node pred, Node node):不断自旋设置前驱节点状态为SIGNAL,表示当前节点需要阻塞。
节点在同步队列中获取锁失败后调用shouldParkAfterFailedAcquire(Node prev,Node node)。
此方法主要逻辑是使用CAS将前驱结点状态置为SIGNAL,表示需要将当前节点阻塞。
如果CAS失败,不断自旋直到将前驱节点状态设置为SIGNAL(-1)为止。
parkAndCheckInterrupt():将当前节点调用LockSupport.park()阻塞。
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

acquire():该方法先再次尝试获取锁,获取成功,则直接返回;否则继续调用acquireQueued()方法,因此下一个调用的方法是addWaiter(Node.EXCLUSIVE)。

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);//首先将当前线程以指定模式包装为node节点。mode:共享或独占模式。
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;//获取当前队列的尾节点
        if (pred != null) {
            node.prev = pred;
            //如果尾节点不为空,使用CAS将包装了当前线程的节点尾插入队列中。
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;//CAS尾插成功,返回包装了当前线程的节点。
            }
        }
        //当前尾节点为空或者CAS尾插失败,调用enq()。
        enq(node);
        return node;
    }

addWaiter()首先将当前线程以指定模式(独占或共享模式)包装为Node节点,然后获取当前队列的尾节点。如果尾节点不为空,则使用CAS将包装了线程的Node节点尾插入队列中,如果尾插成功,则返回包装了当前线程的节点;否则(如果尾节点为空或者尾插失败),则调用enq(),最后返回包装了当前线程的节点。

    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {
        //不断自旋直到将当前节点尾插入同步队列成功为止
        for (;;) {
            Node t = tail;
            //如果当前队列的尾节点为空,则初始化队列。
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            //如果尾节点不为空,则不断CAS将当前节点尾插到同步队列中。
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;//返回队列的尾节点
                }
            }
        }
    }

enq()当同步队列的尾节点为空或者CAS尾插失败时调用该方法该方法主要实现了如果队列为空,则初始化队列。如果尾节点不为空,则不断CAS直到成功将包装了当前线程的Node节点尾插到同步队列中。

在执行了addWaiter()后,就该执行acquireQueued(addWaiter(Node.EXCLUSIVE),arg)了。下面是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);
        }
    }

acquireQueued(addWaiter(Node.EXCLUSIVE),arg):在当前线程包装的节点入队后调用。先设置标志位failed为true。不断自旋,先获取当前节点的前驱节点,如果前驱节点为队列的头节点并且再次获取同步状态成功,则设置当前节点为头节点,并删除原来的头节点,修改failed为true,直接退出。否则(如果当前节点不是头节点或者获取同步状态失败),那么调用shouldParkAfterFailedAcquire(p,node)方法。

//检查失败获取锁的状态   pred:当前线程的前驱节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //获取前驱节点的节点状态  0-初始状态  1-取消  -1(SIGNAL)-当前节点的后继节点处于等待状态  -2-处于等待队列
        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;//当前驱节点处于等待状态时,调用parkAndCheckInterrupt()。
        //前驱节点已经被取消
        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.
             */
	//前驱节点状态不是取消状态时,将前驱节点的状态置为-1,表示后继节点应该处于等待状态。
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

shouldParkAfterFailedAcquire(Node pred,Node node):在队列中获取锁失败时调用。主要逻辑:使用CAS将前驱结点状态置为SIGNAL,表示需要将当前节点阻塞。如果CAS失败,不断自旋直到将前驱节点状态设置为SIGNAL(-1)为止。该方法首先获取当前节点的状态。如果当前节点处于等待状态,直接返回true。如果当前节点的前驱结点已经被取消,则不断重试找前驱节点直到找到一个前驱结点状态不为取消状态的节点为止,返回false。如果不是这两种情况(即前驱节点是初始状态或等待状态),则调用compareAndSetWaitStutas方法将前驱节点的状态置为-1(表示处于等待状态),返回false,将当前线程节点从队列中移出。

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//阻塞当前线程
        return Thread.interrupted();//返回当前线程的状态是否被中断
    }

parkAndCheckInterrupt()在当前节点处于等待状态时调用。作用是阻塞当前线程,并返回中断判断。如果被中断,则将acquireQueued()方法中的interrupted标志位设置为true。

2.unlock()

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

unlock()实际调用AQS提供的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;
    }

release():再次尝试释放状态成功后,获取当前队列的头节点。如果队列头节点不为空并且头节点的状态不是初始状态,则唤醒后继节点,同时返回true,表示释放成功。否则(如果未释放成功或者释放成功但是(队列的头节点为空或者头节点是初始状态),则返回false,表示未释放成功。

private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
// 头结点的后继节点
    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){
            // 后继节点不为null时唤醒
                LockSupport.unpark(s.thread);
            }
        }
    }
}

unparkSuccessor(Node node):首先获取头节点的后继节点,当后继节点不为空时,会调用LockSupport.unpark()方法唤醒后继节点包装的线程。因此,每一次锁释放后就会唤醒队列中该节点的后继节点所包装的线程。

独占锁获取与释放总结:
1.线程获取锁失败,将线程调用addWaiter(),封装成Node,进行入队操作。
addWaiter()中方法enq()完成对同步队列的头节点初始化以及CAS尾插失败后的重试处理。
2.入队之后,排队获取锁的核心方法acquireQueued(),节点排队获取锁是一个自旋过程。
(*****)当且仅当当前节点的前驱节点为头节点并且成功获取同步状态时,节点出队并且该节点引用的线程获取到锁。
否则,不满足条件时会不断自旋将前驱节点的状态设置为SIGNAL,而后调用LockSupport.park()将当前线程阻塞。
3.释放锁时会唤醒后继节点(后继节点不为空)。

二、独占锁的特性

1.可中断式获取锁

ReentranLock类中的lockInterruptibly方法:

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

最终会调用AQS的acquireInterruptibly(int arg)模板方法:

//获取锁时响应中断
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
	//增加了对中断状态的判断 如果检测线程中断状态改变,抛出中断异常后方法直接退出。
        //判断当前线程状态,如果为true,表明被中断;反之,未被中断。
        if (Thread.interrupted())
            throw new InterruptedException();
       //获取时未中断,排队时中断。
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
 }
    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);//将当前线程封装为Node节点,并尾插入队。
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
	//线程被阻塞时,若检测到中断,抛出中断异常后退出。
                    throw new InterruptedException();//区别
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

总结:可中断式获取锁和独占式锁的区别:1)先判断中断状态是否发生改变,如果改变,直接抛出中断异常后退出;2)在doAcquireInterruptibly()中,当parkAndCheckInterrupt()返回true时,表示线程阻塞时被中断,抛出中断异常后线程退出。

2.超时等待获取锁

在中断获取锁的基础上,增加超时功能。该方法本质调用AQS的模板方法tryAcquireNanos()。

public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {//根据unit传入的时间单位来确定超时时间。
        return sync.tryAcquireNanos(1,time);
}
    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
	//先检测中断状态,如果中断,则退出。
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);//再次尝试获取锁,如果成功,直接退出;如果失败,调用doAcquireNanos方法。
    }
 private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
//传入时间(超时等待时间)小于0,表示方法直接退出,线程获取锁失败。
        if (nanosTimeout <= 0L)
            return false; 
//1.根据超时时间和当前时间计算出截止时间   System.nanoTime():当前时间
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
//2.当前线程获取锁出队列
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
	//3.1重新计算超时时间=截止时间-当前时间值
                nanosTimeout = deadline - System.nanoTime();
	//3.2已经超时,线程直接退出,返回false
                if (nanosTimeout <= 0L)
                    return false;
	//3.3线程阻塞等待
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);//如果线程在超时时间内还没有被唤醒,则线程直接退出。
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

流程图:

AQS源码分析--独占锁和独占锁的特性_第2张图片

该方法在三种情况下会返回
1)在超时时间内,当前线程成功获取到锁。
2)当前线程在超时时间内,被中断。
3)超时时间结束,仍未获取到锁,线程退出,返回false。

超时获取锁逻辑与可中断获取锁基本一致,唯一区别在于获取锁失败后,增加了一个时间处理,如果当前时间超过截止时间,线程不再等待,直接退出,返回false。否则将线程阻塞置为等待状态排队获取锁。

你可能感兴趣的:(Java)