ReentrantLock使用和原理(AQS简述)

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/peytonzhao/article/details/80379661

在开发过程中,synchronized是最简单的同步控制方法,在通常情况下是够用的,但synchronized时不响应中断,而且有时候,我们需要灵活的来控制加解锁。这时候可以使用ReentrantLock。
在以前的版本中,synchronized效率是远远低于ReentrantLock,后来经过优化,两者性能差距不大了。但ReentrantLock有一些新特性,是synchronized所不具备的。
1、接口

//Lock.java
/* 加锁 */
void lock();
//可响应中断
void lockInterruptibly() throws InterruptedException;
//尝试加锁,失败马上返回
boolean tryLock();
//尝试加锁,包含最大等待时间,最大等待时间范围内未加锁,返回。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//解锁,注意不要忘,如果有异常,解锁应放在finally中
void unlock();
//获取锁的condition对象,然后做await,signal来实现等待和通知
Condition newCondition();

另外ReentrantLock可以选择是否使用公平锁,公平锁需要维持一个有序队列,按照请求锁的先后顺序来获得锁,因而效率较低,但好处在于不会产生饥饿现象,即一个线程,只要等待,必然能获取到锁。而非公平锁则不是这样,非公平锁除了随机获得锁以外,还有一个隐藏属性,即一个线程会倾向于再次获得已经持有的锁,这样的锁的分配方式比较高效。
再来说说Condition。

//Condition.java
//等待,可响应中断
void await() throws InterruptedException;
//等待,不响应中断
void awaitUninterruptibly();
//按照时间来等待,响应中断
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
//通知
void signal();
void signalAll();

2、原理
ReentrantLock使用内部类Sync来实现加解锁,Sync是AbstractQueuedSynchronizer(下面简称AQS)的子类。
AQS,人如其名,抽象队列同步器,定义了一套多线程访问共享资源的同步器框架,是模板模式的典型应用,不光用在ReentrantLock中,也用在Semaphor/CountDownLatch等上。
AQS核心是一个共享资源(volatile int state;)和一个等待队列。共享资源相关的方法有3个。

//AbstractQueuedSynchronizer.java
protected final int getState() {
    return state;
}
protected final void setState(int newState) {
    state = newState;
}`
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

队列是一个FIFO的双向队列,队列相关结构如下。

//注释

     * <pre>
     *      +------+  prev +-----+       +-----+
     * head |      | <---- |     | <---- |     |  tail
     *      +------+       +-----+       +-----+
     * </pre>
//code
private transient volatile Node head;
private transient volatile Node tail;

AQS定义了2种资源共享的方式,EXCLUSIVE(独占,即只有一个线程能获取到该资源,比如ReentrantLock),SHARED(共享,多个线程可以共享该资源,Semephore/CountDownLatch),当然也可以兼而有之,比如ReentrantReadWriteLock。
等待队列的每一项为Node,核心结构如下

//AbstractQueueSynchronizer.java
//前一个Node和下一个Node
volatile Node prev;
volatile Node next;
//本Node持有的线程
volatile Thread thread;
//指定本Node的模式,标识共享/独占
Node nextWaiter;
//标识本Node的各种状态,比如被中断,下个节点需要通知,等等等等
volatile int waitStatus;

下面通过具体过程来阐述加锁的细节。
2-1、加锁
通常使用的都是非公平锁,我们以这个为例来说明

//ReentrantLock.java
private final Sync sync;
public ReentrantLock() {
        sync = new NonfairSync();//默认是非公平锁
}
public void lock() {
        sync.lock();
}
//NonfairSync内部类,非公平锁加锁执行本方法
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

其中,compareAndSetState(0, 1),使用CAS来设置State为1,成功返回true,并执行

//AbstractOwnableSynchronizer.java
//设置当前执行的线程
protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

此时表示加锁成功。如果state当时已经为1,即有其他线程已拿到锁,则compareAndSetState(0, 1)马上返回false表示失败。并执行如下代码。

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

首先,继续尝试拿锁。

protected final boolean tryAcquire(int acquires) {
  return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //state为9表示可以加锁,马上CAS操作设置State为1
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果本线程已经拿到锁,state也不为0,此时,state+=1
    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;
}

上一步加锁成功,直接返回,失败则将本线程进行加队列操作。

private Node addWaiter(Node mode) {
        //创建当前线程的Node,独占模式
        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;
    }
   private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

先使用快速方式增加节点,一次成功可以马上返回,失败则执行enq方法。

private Node enq(final Node node) {
       //经典的CAS自旋volatile变量
        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;
                }
            }
        }
    }

加队列成功后,可以以等待状态休息了,直到其他线程释放资源后唤醒本线程,下面来看源码

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
        //记录是否被中断
            boolean interrupted = false;
            //自旋拿锁,即拿state
            for (;;) {
                //队列前一个节点
                final Node p = node.predecessor();
                //如果前一个节点是head节点,则可以尝试进行拿锁,即CAS设置state,快速返回成功或失败。
                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);
        }
    }

如果本线程的Node作为队列中第二个节点拿到锁成功,否则会执行shouldParkAfterFailedAcquire,该函数其实是将Node往前插队,前面的Node可能因为各种原因已经死去(中断等等),直到找到确实在等待拿锁的,然后通过park进入waiting状态。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//前驱的状态
        if (ws == Node.SIGNAL)
            //如果已经告诉前驱拿到锁通知自己了,直接返回,可以马上休息
            return true;
        if (ws > 0) {
            //其他情况,循环遍历队列,如果前驱放弃了,就继续往前找,直到找到正常的节点,并排在他后面。那些被放弃的节点,由于引用消失,后续会被GC掉。
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //前驱状态正常,就设置前驱状态为SIGNAL,表示,前驱拿到锁后通知自己
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

上述函数如果返回成功,则执行park,直到被unpark或中断唤醒。这里特意注意下,如果park时收到中断,并不会抛异常,而是通过Thread.interrupted()获得中断标记,并清除掉中断标记。

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

如果是中断,会在acquire中执行中断,整个拿锁流程如下图所示

1、入队-》2、是否二号&拿锁-》3、找正常前前节点-》4、park等待-》2
2-》成功拿锁/中断

公平锁的区别只有下面的获取锁的方法有区别

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //state为0可以加锁。hasQueuedPredecessors表示前面是否有Node,具体代码见下
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

其中hasQueuedPredecessors表示是否前面有节点

public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

另外还有一些tryLock(只使用乐观加锁尝试一次,直接返回)。 lockInteruptbly(基本流程相同,把最终的Thread.interupt换为throw InteruptException)
2-2、解锁
unlock流程如下,执行unlock表示已经拿到lock,因而不需要考虑线程安全的问题,直接将state-1即可,唯一需要注意的是多次lock需要多次unlock,这里要判断是否存在未完全释放资源的情况。

public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
    Node h = head;//找到头结点
    if (h != null && h.waitStatus != 0)
        unparkSuccessor(h);//通知下一个Node
    return true;
}
return false;
}

首先执行tryRelease,每次都把state-=1;

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

再来看unparkSuccessor

private void unparkSuccessor(Node node) {
        //清零当前node(头结点)的waitState,允许失败
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;//下一个节点
        if (s == null || s.waitStatus > 0) {//为空或取消
            s = null;
            //从队列尾部往前找,waitStatus <0即为有效的节点
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);//对该节点进行unpark唤醒
    }

unlock的流程主要在于对下一个等待线程的通知上。
2-3 Condition.await
首先是lock.newCondition()

//ReentrantLock.java
final ConditionObject newCondition() {
   return new ConditionObject();
}

ConditionObject是AQS的内部类,主要结构如下

//双向等待队列的首节点
/** First node of condition queue. */
private transient Node firstWaiter;
//尾节点
/** Last node of condition queue. */
private transient Node lastWaiter;

关于await,基本猜想就是往上面的队列中加,然后阻塞等,基本逻辑跟lock换汤不换药。下面来验证我们的猜想。下面来看await的代码

//AbastractQueuedSynchronizer.java
public final void await() throws InterruptedException {
    if (Thread.interrupted())//先判断中断
        throw new InterruptedException();
    //加入等待队列,就是上面ConditionObject中的那个。
    Node node = addConditionWaiter();
    //释放锁将lock等待队列的下一个节点进行unpark通知
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //循环判断是否在AQS的等待队列中,不在就进行park动作。之后不论是被unpark唤醒,还是中断,均会跳出次循环。
    while (!isOnSyncQueue(node)) {
        //这里的park是conditionObject的this,即只有signal或中断的会唤醒本线程
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

先看addConditionWaiter的代码,代码的意义在于加入等待队列。

private Node addConditionWaiter() {
    Node t = lastWaiter;
    //如果尾节点取消了等待,清除队列上无效的节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //本节点添加到队列
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

看unlinkCancelledWaiters,其实就是遍历conditionObject的整个队列,将非等待状态的节点摘除。

private void unlinkCancelledWaiters() {
    Node t = firstWaiter;//头结点
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        //不是等待节点,就将该节点摘链
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;
            if (trail == null)
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            //到尾部了
            if (next == null)
                lastWaiter = trail;
        }
        else
            trail = t;
        //往后遍历
        t = next;
    }
}

再看fullyRelease,其实就是unlock操作,并找出lock队列(AQS的队列)中下一个节点(下一个状态不正确, 会从头结点开始寻找)进行unpark通知

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
    Node h = head;
    if (h != null && h.waitStatus != 0)
        unparkSuccessor(h);//参考unlock流程的unpark,这里不展开
    return true;
}
return false;
}

其中tryRelease取决于何种子类,对于ReentrantLock来说,就是state-1。

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

再回过头看下一个流程,判断是否在等待lock的等待队列中,即AQS的队列

final boolean isOnSyncQueue(Node node) {
    //正常等待condition节点可以进行unpark
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    //其他时候需要下一个节点在AQS的等待队列中,即等待拿锁lock中,这里判断不在lock中,执行下一步动作
    if (node.next != null) // If has successor, it must be on queue
        return true;
    //从尾部往前找,找到本节点返回true
    return findNodeFromTail(node);
}
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
           if (t == node)
               return true;
           if (t == null)
               return false;
           t = t.prev;
    }
}

回过头来梳理这个流程,简单来理解,就是入ConditionObject的队列进行park等待,直到被唤醒或中断,这两种都会跳出while循环。下面来看后面的流程。
这里刚从await的park中出来,要么被signal唤醒,要么被中断唤醒。下面会重新拿锁,并返回。

if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
    interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
    unlinkCancelledWaiters();
if (interruptMode != 0)
    reportInterruptAfterWait(interruptMode);

await的核心其实就是释放锁,并通过park等待signal。后面被唤醒时,再拿锁并返回。

2-4 Condition.signal
signal的核心是将线程从await的等待状态(park)中唤醒。

public final void signal() {
    //当前非本线程执行,抛异常
   if (!isHeldExclusively())
       throw new IllegalMonitorStateException();
   Node first = firstWaiter;
   if (first != null)
       doSignal(first);
}
protected final boolean isHeldExclusively() {
   return getExclusiveOwnerThread() == Thread.currentThread();
}

来看核心流程,通知Condition队列的头结点。

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
    //当前Condition状态置为0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    //节点入AQS锁的等待队列
    Node p = enq(node);
    int ws = p.waitStatus;
    //设置为Sinal状态,并进行unpark
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

signal的效果是将头结点加入Lock的等待队列,并通知那个线程启动。那个线程在执行await()会由于while (!isOnSyncQueue(node)) 而跳出循环。
signalAll会把ConditionObject队列中的所有节点都移入AQS的等待队列并唤醒他们。

展开阅读全文

没有更多推荐了,返回首页