为了账号安全,请及时绑定邮箱和手机立即绑定

Java并发(5)- ReentrantLock与AQS

标签:
Java

引言

synchronized未优化之前我们在编码中使用最多的同步工具类应该是ReentrantLockReentrantLock拥有优化后synchronized关键字的性能又提供了更多的灵活性。相比synchronized他在功能上更加强大具有等待可中断公平锁以及绑定多个条件等synchronized不具备的功能是我们开发过程中必须要重点掌握的一个关键并发类。

ReentrantLock在JDK并发包中举足轻重不仅是因为他本身的使用频度同时他也为大量JDK并发包中的并发类提供底层支持包括CopyOnWriteArrayLitCyclicBarrierLinkedBlockingDeque等等。既然ReentrantLock如此重要那么了解他的底层实现原理对我们在不同场景下灵活使用ReentrantLock以及查找各种并发问题就很关键。这篇文章就带领大家一步步剖析ReentrantLock底层的实现逻辑了解实现逻辑之后又应该怎么更好的使用ReentrantLock

ReentrantLock与AbstractQueuedSynchronizer的关系

在使用ReentrantLock类时第一步就是对他进行实例化也就是使用new ReentrantLock()来看看他的实例化的源码

public ReentrantLock() {
    sync = new NonfairSync();
}public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

在代码中可以看到ReentrantLock提供了2个实例化方法未带参数的实例化方法默认用NonfairSync()初始化了sync字段带参数的实例化方法通过参数区用NonfairSync()FairSync()初始化sync字段。

通过名字看出也就是我们常用的非公平锁与公平锁的实现公平锁需要通过排队FIFO的方式来获取锁非公平锁也就是说可以插队默认情况下ReentrantLock会使用非公平锁的实现。那么是sync字段的实现逻辑是什么呢看下sync的代码

private final Sync sync;abstract static class Sync extends AbstractQueuedSynchronizer {......}static final class NonfairSync extends Sync {......}static final class FairSync extends Sync {......}

到这里就发现了AbstractQueuedSynchronizer类公平锁和非公平锁其实都是在AbstractQueuedSynchronizer的基础上实现的也就是AQS。AQS提供了ReentrantLock实现的基础。

ReentrantLock的lock()方法

分析了ReentrantLock的实例化之后来看看他是怎么实现锁这个功能的

//ReentrantLock的lock方法public void lock() {
    sync.lock();
}//调用了Sync中的lock抽象方法abstract static class Sync extends AbstractQueuedSynchronizer {
    ......    /**
        * Performs {@link Lock#lock}. The main reason for subclassing
        * is to allow fast path for nonfair version.
        */
    abstract void lock();
    ......
}

调用了synclock()方法Sync类的lock()方法是一个抽象方法NonfairSync()FairSync()分别对lock()方法进行了实现。

//非公平锁的lock实现static final class NonfairSync extends Sync {
    ......    /**
        * Performs lock.  Try immediate barge, backing up to normal
        * acquire on failure.
        */
    final void lock() {        if (compareAndSetState(0, 1)) //插队操作首先尝试CAS获取锁0为锁空闲
            setExclusiveOwnerThread(Thread.currentThread()); //获取锁成功后设置当前线程为占有锁线程
        else
            acquire(1);
    }
    ......
}//公平锁的lock实现static final class FairSync extends Sync {
    ......    final void lock() {
        acquire(1);
    }
    ......
}

注意看他们的区别NonfairSync()会先进行一个CAS操作将一个state状态从0设置到1这个也就是上面所说的非公平锁的“插队”操作前面讲过CAS操作默认是原子性的这样就保证了设置的线程安全性。这是非公平锁和公平锁的第一点区别。

那么这个state状态是做什么用的呢从0设置到1又代表了什么呢再来看看跟state有关的源码

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

首先state变量是一个volatile修饰的int类型变量这样就保证了这个变量在多线程环境下的可见性。从变量的注释“The synchronization state”可以看出state代表了一个同步状态。再回到上面的lock()方法在设置成功之后调用了setExclusiveOwnerThread方法将当前线程设置给了一个私有的变量这个变量代表了当前获取锁的线程放到了AQS的父类AbstractOwnableSynchronizer类中实现。

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ......    /**
     * The current owner of exclusive mode synchronization.
     */
    private transient Thread exclusiveOwnerThread;    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }    protected final Thread getExclusiveOwnerThread() {        return exclusiveOwnerThread;
    }
}

如果设置state成功lock()方法执行完毕代表获取了锁。可以看出state状态就是用来管理是否获取到锁的一个同步状态0代表锁空闲1代表获取到了锁。那么如果设置state状态不成功呢接下来会调用acquire(1)方法公平锁则直接调用acquire(1)方法不会用CAS操作进行插队。acquire(1)方法是实现在AQS中的一个方法看下他的源码

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

这个方法很重要也很简单理解有几步操作首先调用tryAcquire尝试获取锁如果成功则执行完毕如果获取失败则调用addWaiter方法添加当前线程到等待队列同时添加后执行acquireQueued方法挂起线程。如果挂起等待中需要中断则执行selfInterrupt将线程中断。下面来具体看看这个流程执行的细节首先看看tryAcquire方法

protected boolean tryAcquire(int arg) {    throw new UnsupportedOperationException();
}protected final boolean tryAcquire(int acquires) {    return nonfairTryAcquire(acquires);
}//NonfairSyncfinal boolean nonfairTryAcquire(int acquires) {    final Thread current = Thread.currentThread();    int c = getState();    if (c == 0) { //锁空闲
        if (compareAndSetState(0, acquires)) { //再次cas操作获取锁
            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;
}//FairSyncprotected final boolean tryAcquire(int acquires) {    final Thread current = Thread.currentThread();    int c = getState();    if (c == 0) {        if (!hasQueuedPredecessors() && //判断队列中是否已经存在等待线程如果存在则获取锁失败需要排队
            compareAndSetState(0, acquires)) { //不存在等待线程再次cas操作获取锁
            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;
}//AQS中实现判断队列中是否已经存在等待线程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());
}

AQS没有提供具体的实现ReentrantLock中公平锁和非公平锁分别有自己的实现。非公平锁在锁空闲的状态下再次CAS操作尝试获取锁保证线程安全。如果当前锁非空闲也就是state状态不为0则判断是否是重入锁也就是同一个线程多次获取锁是重入锁则将state状态+1这也是ReentrantLock`支持锁重入的逻辑。

公平锁和非公平锁在这上面有第二点区别公平锁在锁空闲时首先会调用hasQueuedPredecessors方法判断锁等待队列中是否存在等待线程如果存在则不会去尝试获取锁而是走接下来的排队流程。至此非公平锁和公平锁的区别大家应该清楚了。如果面试时问道公平锁和非公平锁的区别相信大家可以很容易答出来了。

通过tryAcquire获取锁失败之后会调用acquireQueued(addWaiter)先来看看addWaiter方法

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);   //用EXCLUSIVE模式初始化一个Node节点代表是一个独占锁节点
    // 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)) { //cas设置尾节点为当前节点将当前线程加入到队列末尾避免多线程设置导致数据丢失
            pred.next = node;            return node;
        }
    }
    enq(node); //如果队列中无等待线程或者设置尾节点不成功则循环设置尾节点
    return node;
}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)) { //重复addWaiter中的设置尾节点也是cas的经典操作--自旋避免使用Synchronized关键字导致的线程挂起
                t.next = node;                return t;
            }
        }
    }
}static final class Node {    /** Marker to indicate a node is waiting in shared mode */
    static final Node SHARED = new Node(); //共享模式
    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;  //独占模式

    ......
}

addWaiter方法首先初始化了一个EXCLUSIVE模式的Node节点。Node节点大家应该很熟悉我写的集合系列文章里面介绍了很多链式结构都是通过这种方式来实现的。AQS中的Node也不例外他的队列结构也是通过实现一个Node内部类来实现的这里实现的是一个双向队列。Node节点分两种模式一种SHARED共享锁模式一种EXCLUSIVE独占锁模式ReentrantLock使用的是EXCLUSIVE独占锁模式所用用EXCLUSIVE来初始化。共享锁模式后面的文章我们再详细讲解。

初始化Node节点之后就是将节点加入到队列之中这里有一点要注意的是多线程环境下如果CAS设置尾节点不成功需要自旋进行CAS操作来设置尾节点这样即保证了线程安全又保证了设置成功这是一种乐观的锁模式当然你可以通过synchronized关键字锁住这个方法但这样效率就会下降是一种悲观锁模式。

设置节点的过程我通过下面几张图来描述下让大家有更形象的理解
https://img1.sycdn.imooc.com//5b875cfe000108b503670204.jpg

https://img1.sycdn.imooc.com//5b875d06000159c605860214.jpg

https://img1.sycdn.imooc.com//5b875d0d0001989808120214.jpg

将当前线程加入等待队列之后需要调用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)) { //如果前置节点是头节点说明当前节点是第一个挂起的线程节点再次cas尝试获取锁
                setHead(node); //获取锁成功设置当前节点为头节点当前节点占有锁
                p.next = null; // help GC
                failed = false;                return interrupted;
            }            if (shouldParkAfterFailedAcquire(p, node) && //非头节点或者获取锁失败检查节点状态查看是否需要挂起线程
                parkAndCheckInterrupt())  //挂起线程当前线程阻塞在这里
                interrupted = true;
        }
    } finally {        if (failed)
            cancelAcquire(node);
    }
}

可以看到这个方法是一个自旋的过程首先获取当前节点的前置节点如果前置节点为头结点则再次尝试获取锁失败则挂起阻塞阻塞被取消后自旋这一过程。是否可以阻塞通过shouldParkAfterFailedAcquire方法来判断阻塞通过parkAndCheckInterrupt方法来执行。

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 { //非可挂起状态或退出状态则尝试设置为Node.SIGNAL状态
        /*
            * 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;
}private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);//挂起当前线程
    return Thread.interrupted();
}

只有当节点处于SIGNAL状态时才可以挂起线程Node的waitStatus有4个状态分别是

/** waitStatus value to indicate thread has cancelled */static final int CANCELLED =  1;/** waitStatus value to indicate successor's thread needs unparking */static final int SIGNAL    = -1;/** waitStatus value to indicate thread is waiting on condition */static final int CONDITION = -2;/**
    * waitStatus value to indicate the next acquireShared should
    * unconditionally propagate
    */static final int PROPAGATE = -3;

注释写的很清楚这里就不详细解释着四种状态了。到这里整个Lock的过程我们就全部说完了公平锁和非公平锁的区别从Lock的过程中我们也很容易发现非公平锁一样要进行排队只不过在排队之前会CAS尝试直接获取锁。说完了获取锁下面来看下释放锁的过程。

ReentrantLock的unLock()方法

unLock()方法比较好理解因为他不需要考虑多线程的问题如果unLock()的不是之前lock的线程直接退出就可以了。看看unLock()的源码

public class ReentrantLock implements Lock, java.io.Serializable {
    ......    public void unlock() {
        sync.release(1);
    }
    ......
}public abstract class AbstractQueuedSynchronizer {
    ......    public final boolean release(int arg) {        if (tryRelease(arg)) { //尝试释放锁
            Node h = head;            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h); //释放锁成功后启动后继线程
            return true;
        }        return false;
    }
    ......
}abstract static class Sync extends AbstractQueuedSynchronizer {
    ......    protected final boolean tryRelease(int releases) {        int c = getState() - releases;        if (Thread.currentThread() != getExclusiveOwnerThread()) //释放锁必须要是获取锁的线程否则退出保证了这个方法只能单线程访问
            throw new IllegalMonitorStateException();        boolean free = false;        if (c == 0) { //独占锁为0后代表锁释放否则为重入锁不释放
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);        return free;
    }
    ......
}abstract static class Sync extends AbstractQueuedSynchronizer {
    ......    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)
            LockSupport.unpark(s.thread); //挂起当前线程
    }
    ......
}

lock()方法一样会调用AQS的release方法首先调用tryRelease尝试释放首先必须要是当前获取锁的线程之后判断是否为重入锁非重入锁则释放当前线程的锁。锁释放之后调用unparkSuccessor方法启动后继线程。

总结

ReentrantLock的获取锁和释放锁到这里就讲完了总的来说还是比较清晰的一个流程通过AQS的state状态来控制锁获取和释放状态AQS内部用一个双向链表来维护挂起的线程。在AQS和ReentrantLock之间通过状态和行为来分离AQS用管理各种状态并内部通过链表管理线程队列ReentrantLock则对外提供锁获取和释放的功能具体实现则在AQS中。下面我通过两张流程图总结了公平锁和非公平锁的流程。

非公平锁
https://img1.sycdn.imooc.com//5b875d190001ecff16372114.jpg
公平锁
https://img1.sycdn.imooc.com//5b875d2800012b1110422036.jpg

原文出处https://www.cnblogs.com/konck/p/9466496.html

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消