Java并发编程——ReentrantLock实现原理

一、前言

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。

CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现

二、AbstractQueuedSynchronizer

AbstractQueuedSynchronizer (AQS)类如其名,抽象的队列式同步容器,AQS定义类一套多线程访问共享资源的同步器,是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。

AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。

AQS以模板方法模式在内部定义了获取和释放同步状态的模板方法,并留下钩子函数供子类继承时进行扩展,由子类决定在获取和释放同步状态时的细节,从而实现满足自身功能特性的需求。除此之外,AQS通过内部的同步队列管理获取同步状态失败的线程,向实现者屏蔽了线程阻塞和唤醒的细节。

2.1 AQS队列节点数据结构

  • prev:指向队列中的上一个节点;
  • waitStatus:节点的等待状态,初始化为0,表示正常同步等待:
    • CANCELLED=1:节点因超时或者被中断而取消时设置为取消状态;
    • SIGNAL=-1:指示当前节点被释放后,需要调用unpark通知后面节点,如果后面节点发生竞争导致获取锁失败,也会将当前节点设置为SIGNAL;
    • CONDITION=-2:指示该线程正在进行条件等待,条件队列中会用到;
    • PROPAGATE=-3:共享模式下释放节点时设置的状态,表示无限传播下去。
  • thread:当前节点操作的线程;
  • nextWaiter:该字段在Condition条件等待中会用到,指向条件队列的下一个节点。或者链接到SHARED常量,表示节点正在以共享模式等待;
  • next:指向队列中的下一个节点。
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    /**
     * 同步等待队列(双向链表)中的节点
     */ 
    static final class Node {
    
        /** 用于指示节点正在共享模式下等待的标记 */
        static final Node SHARED = new Node();
        
        /** 用于指示节点正在独占模式下等待的标记 */
        static final Node EXCLUSIVE = null;

        /** 线程被取消了 */
        static final int CANCELLED =  1;
        
        /** 
         * 如果前驱节点的等待状态是SIGNAL,表示当前节点将来可以被唤醒,那么当前节点就可以安全的挂起了 
         * 否则,当前节点不能挂起 
         */
        static final int SIGNAL    = -1;
        
        /** 线程正在等待条件 */
        static final int CONDITION = -2;
        
        /**
         * 指示下一个acquireShared应无条件传播
         */
        static final int PROPAGATE = -3;

        //值就是前四个int值和0(CANCELLED/SIGNAL/CONDITION/PROPAGATE/0)
        volatile int waitStatus;

        /**
         * 前驱节点
         */
        volatile Node prev;

        /**
         * 后继节点
         */
        volatile Node next;

        /**
         * 节点中的线程
         */
        volatile Thread thread;


        Node nextWaiter;

        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        /**
         * 返回该节点前一个节点
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // 用于addWaiter中
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }
}

2.2 AQS同步器数据结构

AQS中同步等待队列的实现是一个带头尾指针(这里用指针表示引用是为了后面讲解源码时可以更直观形象,况且引用本身是一种受限的指针)且不带哨兵结点(后文中的头结点表示队列首元素结点,不是指哨兵结点)的双向链表。

如上图,AQS中:

  • state:所有线程通过通过CAS尝试给state设值,当state>0时表示被线程占用;同一个线程多次获取state,会叠加state的值,从而实现了可重入;
  • exclusiveOwnerThread:在独占模式下该属性会用到,当线程尝试以独占模式成功给state设值,该线程会把自己设置到exclusiveOwnerThread变量中,表明当前的state被当前线程独占了;
  • 等待队列(同步队列):等待队列中存放了所有争夺state失败的线程,是一个双向链表结构。state被某一个线程占用之后,其他线程会进入等待队列;一旦state被释放(state=0),则释放state的线程会唤醒等待队列中的线程继续尝试cas设值state;
  • head:指向等待队列的头节点,延迟初始化,除了初始化之外,只能通过setHead方法进行修改;
  • tail:指向等待队列的队尾,延迟初始化,只能通过enq方法修改tail,该方法主要是往队列后面添加等待节点。
/**
 * Head of the wait queue, lazily initialized.  Except for
 * initialization, it is modified only via method setHead.  Note:
 * If head exists, its waitStatus is guaranteed not to be
 * CANCELLED.
 */
private transient volatile Node head;//指向队列首元素的头指针

/**
 * Tail of the wait queue, lazily initialized.  Modified only via
 * method enq to add new wait node.
 */
private transient volatile Node tail;//指向队列尾元素的尾指针

为了接下来能够更好的理解加锁和解锁过程的源码,对该同步队列的特性进行简单的讲解:

  • 1、同步队列是个先进先出(FIFO)队列,获取锁失败的线程将构造结点并加入队列的尾部,并阻塞自己。如何才能线程安全的实现入队是后面讲解的重点,毕竟我们在讲锁的实现,这部分代码肯定是不能用锁的。
  • 2、队列首结点可以用来表示当前正获取锁的线程。
  • 3、当前线程释放锁后将尝试唤醒后续处结点中处于阻塞状态的线程。

同步状态变量

private volatile int state; //同步状态变量

这是一个带volatile前缀的int值,是一个类似计数器的东西。在不同的同步组件中有不同的含义。以ReentrantLock为例,state可以用来表示该锁被线程重入的次数。当state为0表示该锁不被任何线程持有;当state为1表示线程恰好持有该锁1次(未重入);当state大于1则表示锁被线程重入state次。因为这是一个会被并发访问的量,为了防止出现可见性问题要用volatile进行修饰。

持有同步状态的线程标志

/**
 * The current owner of exclusive mode synchronization.
 */
 //持有同步状态的线程标志
private transient Thread exclusiveOwnerThread;

如注释所言,这是在独占同步模式下标记持有同步状态线程的。ReentrantLock就是典型的独占同步模式,该变量用来标识锁被哪个线程持有。

AbstractQueuedSynchronizer自定义同步器实现

自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了,自定义同步容器实现时主要实现以下几种方法:

  • 1、isHeldExclusively():该线程是否正在独占资源,只有用到condition才需要取实现它。
  • 2、tryAcquire(int):独占方式,尝试获取资源,成功则返回true,失败则返回false。
  • 3、tryRelease(int):独占方式,尝试释放资源,成功则返回true,失败则返回false。
  • 4、tryAcquireShared(int):共享方式,尝试获取资源,附属表示获取失败,0表示获取成功,但是没有剩余资源可用,其他线程不能在获取,正数表示获取成功,并且有剩余资源,也就是自己可以重复获取(可重入)。
  • 5、tryReleaseShare(int):共享方式。尝试释放资源,成功返回true,失败返回false。
  • 6、以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()的时候,会调用tryAcquire()独占该锁并将state+1。此后其他线程在tryAcquire()独占该锁并将state+1。此后表示其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(几锁释放)为止,其他线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

了解AQS的主要结构后,就可以开始进行ReentrantLock的源码解读了。

三、ReentrantLock

ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:

  • 非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取。

  • 公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

  • 可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。

  • 可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。

公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。

3.1 Reentrantlock类结构

public class ReentrantLock implements Lock, java.io.Serializable {

}

可以看出Reentrantlock 实现类lock接口,首先看下这个lock接口。

public interface Lock {

    //普通的获取锁的方法,lock会阻塞直到成功
    void lock();

    //与lock不同的时,它可以相应中断,
    void lockInterruptibly() throws InterruptedException;
     
    //尝试获取锁,立即返回,不阻塞,成功返回true,失败返回false
    boolean tryLock();

    //先尝试获取锁,如果成功则返回true,失败则阻塞等待,等待时间由参数决定,
    //在等待的同时相应中断,抛出中断异常,如果在等待的过程中获得类锁则返回true,
    //否则直到时间超时,则返回false。
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    //普通释放锁的方法    
    void unlock();

    //新建一个条件,lock可以关联多个条件,关于条件在以后篇幅中记录
    Condition newCondition();
}

可以看出,显示锁,与synchronied相比,支持非阻塞的方式获取锁,可以响应中断,可以限时,这使得它非常的灵活。

现在回到Reentrantlock类,可以看到其内部基本上定义类三个内部类:

public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;
    
    abstract static class Sync extends AbstractQueuedSynchronizer {
        //......
    }
    
    //非公平锁
    static final class NonfairSync extends Sync {
        //......
    }
    
    //公平锁
    static final class FairSync extends Sync {
        //......
    }
}

很显然从上面的类结构,我们可以得出jdk利用CAS算法和AQS实现类ReentrantLock。

3.2 构造函数分析

  • ReentrantLock出于性能考虑,默认采用非公平锁。
public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;
    
    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

四、NonfairSync非公平锁

NonfairSync非公平锁加锁流程(简化):

  • 1、基于CAS尝试将state(锁数量)从0设置为1。
    • A、如果设置成功,设置当前线程为独占锁的线程;
    • B、如果设置失败,还会再获取一次锁数量,
      • B1、如果锁数量为0,再基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;

      • B2、如果锁数量不为0或者上边的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒。

4.1 非公平模式下的加锁

public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;
    
    static final class NonfairSync extends Sync {

        final void lock() {
            //直接通过CAS,通过改变共享资源 state值,成功则设置当前线程为持有锁的线程(独占),
            //失败则调用顶层AQS的acquire方法,再次尝试,失败则将线程放到阻塞队列
            if (compareAndSetState(0, 1))
                //将当前线程标记为已持有锁
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
    }
}

ReentrantLock 中使用 state 用于表示是否被锁和持有的数量,如果当前未被锁定,则立即获得锁,否则调用acquire(1);方法来获得锁,其中acquire为AQS中的方法,其实现为:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    /**
     * 该方法主要做了三件事情:
     * 1. 调用 tryAcquire 尝试获取锁,该方法需由 AQS 的继承类实现,获取成功直接返回
     * 2. 若 tryAcquire 返回 false,则调用 addWaiter 方法,将当前线程封装成节点,
     *    并将节点放入同步队列尾部
     * 3. 调用 acquireQueued 方法让同步队列中的节点循环尝试获取锁
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
}

4.1.1、tryAcquire(arg)尝试去获取锁。如果尝试获取锁成功,方法直接返回。

  • 这段代码调用 tryAcquire 方法来尝试获取锁,该方法需要被子类重写,在 NonFairSync 中的实现为:
public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;
    
    static final class NonfairSync extends Sync {

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    
    abstract static class Sync extends AbstractQueuedSynchronizer { 
        final boolean nonfairTryAcquire(int acquires) {
            //获取当前线程
            final Thread current = Thread.currentThread();
            //获取state变量值
            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");
                    
                // 更新state值为新的重入次数  
                setState(nextc);
                return true;
            }
            //获取锁失败
            return false;
        }
    }
}

非公平锁tryAcquire的流程是:检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回false。

tryAcquire()尝试获取锁

不同的锁可以有不同的tryAcquire()实现,所以,你可以看到ReentrantLock锁里面会有非公平锁和公平锁的实现方式。
ReentrantLock公平锁的实现代码在获取锁之前多了一个判断:!hasQueuedPredecessors(),这个是判断如果当前线程节点之前没有其他节点了,那么我们才可以尝试获取锁,这就是公平锁的体现。

4.1.2、进入等待队列。由于上文中提到锁已经被占用了,所以执行tryAcquire失败,并且入等待队列。如果锁一直不释放,那么后续线程就会被挂起。

  • 先看下入队的过程。先看addWaiter(Node.EXCLUSIVE)
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    /**
     * 将新节点和当前线程关联并且入队列
     * @param mode 独占/共享
     * @return 新节点
     */
    private Node addWaiter(Node mode) {
        //初始化节点,设置关联线程和模式(独占 or 共享)
        Node node = new Node(Thread.currentThread(), mode);
        // 获取尾节点引用
        Node pred = tail;
        // 尾节点不为空,说明队列已经初始化过
        if (pred != null) {
            node.prev = pred;
            // 设置新节点为尾节点
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 尾节点为空,说明队列还未初始化,需要初始化head节点并入队新节点
        enq(node);
        return node;
    }
}
  • 假如多个线程同时尝试入队列,由于队列尚未初始化,tail==null,故至少会有一个线程会走到enq(node)。我们假设同时走到了enq(node)里。
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    /**
     * 初始化队列并且入队新节点
     */
    private Node enq(final Node node) {
        //开始自旋
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                // 如果tail为空,则新建一个head节点,并且tail指向head
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                // tail不为空,将新节点入队
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
}

这里体现了经典的自旋+CAS组合来实现非阻塞的原子操作。由于compareAndSetHead的实现使用了unsafe类提供的CAS操作,所以只有一个线程会创建head节点成功。假设线程B成功,之后B、C开始第二轮循环,此时tail已经不为空,两个线程都走到else里面。假设B线程compareAndSetTail成功,那么B就可以返回了,C由于入队失败还需要第三轮循环。最终所有线程都可以成功入队。

当线程进入等待队列后,此时AQS队列如下:


addWaiter(Node mode)进入等待队列

4.1.3、挂起。B和C相继执行acquireQueued(final Node node, int arg)。这个方法让已经入队的线程尝试获取锁,若失败则会被挂起。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    /**
     * 已经入队的线程尝试获取锁
     */ 
    final boolean acquireQueued(final Node node, int arg) {
        //标记是否成功获取锁
        boolean failed = true;
        try {
            //标记线程是否被中断过
            boolean interrupted = false;
            for (;;) {
                //获取前驱节点
                final Node p = node.predecessor();
                //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
                if (p == head && tryAcquire(arg)) {
                    // 获取成功,将当前节点设置为head节点
                    setHead(node);
                    // 原head节点出队,在某个时间点被GC回收
                    p.next = null; // help GC
                    //获取成功
                    failed = false;
                    //返回是否被中断过
                    return interrupted;
                }
                // 判断获取失败后是否可以挂起,若可以则挂起
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 线程若被中断,设置interrupted为true
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
}

code里的注释已经很清晰的说明了acquireQueued的执行流程。假设B和C在竞争锁的过程中A一直持有锁,那么它们的tryAcquire操作都会失败,因此会走到第2个if语句中。

我们再看下shouldParkAfterFailedAcquire和parkAndCheckInterrupt都做了哪些事吧。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    /**
     * 判断当前线程获取锁失败之后是否需要挂起.
     */ 
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //前驱节点的状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            // 前驱节点状态为signal,返回true
            return true;
            
        // 前驱节点状态为CANCELLED 
        if (ws > 0) {
            // 从队尾向前寻找第一个状态不为CANCELLED的节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 将前驱节点的状态设置为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    
    /**
     * 挂起当前线程,返回线程中断状态并重置
     */ 
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }   
}

线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是否>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为SIGNAL。

整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去尝试竞争锁。

注意:
是否需要 unpark 是由当前节点的前驱节点的 waitStatus == Node.SIGNAL 来决定,而不是本节点的waitStatus 决定。

如果当前节点的prev不是head,或者争抢失败,则会将前面节点的状态设置为SIGNAL。

如果前面的节点状态大于0,表示节点被取消,这个时候会把该节点从队列中移除掉。

下图为尝试CAS争抢锁,但失败了,然后把head节点状态设置为SIGNAL


acquireQueued(final Node node, int arg)

然后再会循环一次尝试获取锁,如果获取失败了,就调用LockSupport.park(this)挂起线程。

那么时候才会触发唤起线程呢?这个时候我们得先看看释放锁是怎么做的了。

五、FairSync公平锁

FairSync公平锁加锁流程(简化):

  • 1、如果锁数量为0,如果当前线程是等待队列中的头节点,基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;

  • 2、如果锁数量不为0或者当前线程不是等待队列中的头节点或者上边的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒。

5.1 公平模式下的加锁

public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;

    static final class FairSync extends Sync {
        final void lock() {
            acquire(1);
        }
    }
}

调用acquire(1);方法来获得锁,其中acquire为AQS中的方法,其实现为:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    /**
     * 该方法主要做了三件事情:
     * 1. 调用 tryAcquire 尝试获取锁,该方法需由 AQS 的继承类实现,获取成功直接返回
     * 2. 若 tryAcquire 返回 false,则调用 addWaiter 方法,将当前线程封装成节点,
     *    并将节点放入同步队列尾部
     * 3. 调用 acquireQueued 方法让同步队列中的节点循环尝试获取锁
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
}

5.2 FairSync:tryAcquire(int acquires)

public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;

    static final class FairSync extends Sync {
        protected final boolean tryAcquire(int acquires) {
            //获取当前线程
            final Thread current = Thread.currentThread();
            //获取state变量值
            int c = getState();
            if (c == 0) {
                //如果没有线程持有锁
                //调用hasQueuedPredecessors函数判断队列中是否有优先于此线程的排队线程
                //如果没有才去尝试竞争锁
                
                // hasQueuedPredecessors()和非公平锁相比,这里多了一个判断:是否有线程在等待
                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");
                    
                // 更新state值为新的重入次数      
                setState(nextc);
                return true;
            }
            //获取锁失败
            return false;
        }
    }
}

可以看到这里的逻辑和NonfairSync唯一的区别就是FairSync多了hasQueuedPredecessors函数的调用,在竞争锁之前需要先看队列中是否有优先于此线程的排队线程,如果有的话,那么FairSync不会去竞争锁,而是直接进入后面入队阻塞的逻辑。那么这里强调的优先于此线程的排队线程是什么意思呢?因为FairSync是公平锁的实现,获取锁总要有个先来后到。

5.3 hasQueuedPredecessors

  • 判断队列中是否有优先于此线程的排队线程
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    public final boolean hasQueuedPredecessors() {
        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());
    }
}

步骤:

  • 1、如果h==t成立,h和t均为null或是同一个具体的节点,无后继节点,返回false。
  • 2、如果h!=t成立,head.next是否为null,如果为null,返回true。
    • 什么情况下h!=t的同时h.next==null?,有其他线程第一次正在入队时,可能会出现。见AQS的enq方法,compareAndSetHead(node)完成,还没执行tail=head语句时,此时tail=null,head=newNode,head.next=null。
  • 3、如果h!=t成立,head.next != null,则判断head.next是否是当前线程,如果是返回false,否则返回true(head节点是获取到锁的节点,但是任意时刻head节点可能占用着锁,也可能释放了锁(unlock()),未被阻塞的head.next节点对应的线程在任意时刻都是有必要去尝试获取锁)

六、释放锁

NonfairSync非公平锁和FairSync公平锁的释放锁代码一样。

6.1 unlock()

步骤:

  • 1、获取当前的锁数量,然后用这个锁数量减去解锁的数量(这里为1),最后得出结果c。

  • 2、判断当前线程是不是独占锁的线程,如果不是,抛出异常。

  • 3、如果c==0,说明锁被成功释放,将当前的独占线程置为null,锁数量置为0,返回true。

  • 4、如果c!=0,说明释放锁失败,锁数量置为c,返回false。

  • 5、如果锁被释放成功的话,唤醒距离头节点最近的一个非取消的节点。

6.2 ReentrantLock:unlock()

public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;
    
    /**
     * 释放这个锁
     *1)如果当前线程持有这个锁,则锁数量被递减
     *2)如果递减之后锁数量为0,则锁被释放。
     *如果当前线程不持久有这个锁,抛出异常
     */
    public void unlock() {
        sync.release(1);
    }
}

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    public final boolean release(int arg) {
        //如果成功释放锁
        if (tryRelease(arg)) {
            //获取头节点:(注意:这里的头节点就是当前正在释放锁的节点)
            Node h = head;
            //头结点存在且等待状态不是取消
            if (h != null && h.waitStatus != 0)
                //唤醒距离头节点最近的一个非取消的节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
}

6.3 Sync:tryRelease(int releases)

最后我们再看下tryRelease的执行过程

public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;
    
    /**
     * 释放当前线程占用的锁
     * @param releases
     * @return 是否释放成功
     */
    abstract static class Sync extends AbstractQueuedSynchronizer { 
        protected final boolean tryRelease(int releases) {
            // 计算释放后state值
            int c = getState() - releases;

            if (Thread.currentThread() != getExclusiveOwnerThread())
                // 如果不是当前线程占用锁,那么抛出异常
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                // 锁被重入次数为0,表示释放成功
                free = true;
                // 清空独占线程
                setExclusiveOwnerThread(null);
            }
            // 更新state值
            setState(c);
            return free;
        }
    }
}

6.4 AbstractQueuedSynchronizer:unparkSuccessor(Node node)

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    /**
     * 唤醒离头节点node最近的一个非取消的节点
     * @param node 头节点
     */ 
    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)
            //将ws设为0状态(即什么状态都不是)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * 获取头节点的下一个等待状态不是cancel的节点
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            /*
             * 注意:从后往前遍历找到离头节点最近的一个非取消的节点,
             * 从后往前遍历据说是在入队(enq())的时候,可能nodeX.next==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);
    }
}

tryRelease()具体由子类实现。一般处理流程是让state减1。

如果释放锁成功,并且头节点waitStatus!=0,那么会调用unparkSuccessor()通知唤醒后续的线程节点进行处理。

注意:在遍历队列查找唤醒下一个节点的过程中,如果发现下一个节点状态是CANCELLED那么就会忽略这个节点,然后从队列尾部向前遍历,找到与头结点最近的没有被取消的节点进行唤醒操作。

唤醒之后,节点对应的线程2又从acquireQueued()方法的阻塞处醒来继续参与争抢锁。并且争抢成功了,那么会把head节点的下一个节点设置为null,让自己所处的节点变为head节点:

这样一个AQS独占式、非中断的抢占锁的流程就结束了。

七、完整流程

最后我们再以另一个维度的流程来演示下这个过程。

首先有4个线程争抢锁,线程1,成功了,其他三个失败了,分别依次入等待队列:

线程2、线程3依次入队列:


现在突然发生了点事情,假设线程3用的是带有超时时间的tryLock,超过了等待时间,线程3状态变为取消状态了,这个时候,线程4追加到等待队列中后,发现前一个节点的状态是1取消状态,那么会执行操作把线程3节点从队列中移除掉:

最后,线程1释放了锁,然后把head节点ws设置为0,并且找到了离head最靠近的一个waitStatus<=0的线程并唤醒,然后参与竞争获取锁:

最终,线程2获取到了锁,然后把自己变为了Head节点,并取代了原来的Head节点:

接着就一直这样循环,我就不再画图了,聪明的你应该对此了如指掌了。

八、可打断原理

8.1 不可打断模式

在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    private final boolean parkAndCheckInterrupt() {
        // 如果打断标记已经是 true, 则 park 会失效
        LockSupport.park(this);
        // interrupted 会清除打断标记
        return Thread.interrupted();
    }   
    
    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())
                    // 如果是因为 interrupt 被唤醒, 返回打断状态为 true
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }   
    
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 如果打断状态为 true
            selfInterrupt();
    }   
    
    static void selfInterrupt() {
        // 重新产生一次中断
        Thread.currentThread().interrupt();
    }   
}

8.2 可打断模式

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
            
        // 如果没有获得到锁, 进入 ㈠   
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }   
    
    // ㈠ 可打断的获取锁流程
    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        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())
                    // 在 park 过程中如果被 interrupt 会进入此
                    // 这时候抛出异常, 而不会再次进入 for (;;)    
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
}

九、总结:

9.1 公平锁与非公平锁对比

  • FairSync:lock()少了插队部分(即少了CAS尝试将state从0设为1,进而获得锁的过程)
  • FairSync:tryAcquire(int acquires)多了需要判断当前线程是否在等待队列首部的逻辑(实际上就是少了再次插队的过程,但是CAS获取还是有的)。

ReentrantLock是基于AbstractQueuedSynchronizer实现的,AbstractQueuedSynchronizer可以实现独占锁也可以实现共享锁,ReentrantLock只是使用了其中的独占锁模式。

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

参考:
https://blog.csdn.net/fuyuwei2015/article/details/83719444

https://blog.51cto.com/u_3265857/2369641

https://segmentfault.com/a/1190000014769953

https://www.jb51.net/article/105762.htm

https://www.cnblogs.com/java-zhao/p/5133402.html

https://www.cnblogs.com/java-zhao/p/5131544.html

https://www.itzhai.com/articles/aqs-and-lock-implementation-in-concurrent-packages.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容