JUC之深入理解ReentrantLock

1. 简介

ReentrantLock,可重入锁,是一种递归无阻塞的同步机制。它可以等同于 synchronized 的使用,但是 ReentrantLock 提供了比 synchronized 更强大、灵活的锁机制,可以减少死锁发生的概率。

一个可重入的互斥锁定 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁定相同的一些基本行为和语义,但功能更强大。

ReentrantLock 将由最近成功获得锁定,并且还没有释放该锁定的线程所拥有。当锁定没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁定并返回。如果当前线程已经拥有该锁定,此方法将立即返回。可以使用 isHeldByCurrentThread()getHoldCount() 方法来检查此情况是否发生。

ReentrantLock 还提供了公平锁非公平锁的选择,通过构造方法接受一个可选的 fair 参数(默认非公平锁):当设置为 true 时,表示公平锁;否则为非公平锁。

公平锁与非公平锁的区别在于,公平锁的锁获取是有顺序的。但是公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

ReentrantLock 整体结构如下图:

  • ReentrantLock 实现 Lock 接口,基于内部的 Sync 实现。

  • Sync 实现 AQS ,提供了 FairSync 和 NonFairSync 两种实现。

2. Sync 抽象类

Sync 是 ReentrantLock 的内部静态类,实现 AbstractQueuedSynchronizer 抽象类,同步器抽象类。它使用 AQS 的 state 字段,来表示当前锁的持有数量,从而实现可重入的特性。

 /**
     * 该锁同步控制的一个基类.下边有两个子类:非公平机制和公平机制.使用了AbstractQueuedSynchronizer类的
     */
    static abstract class Sync extends AbstractQueuedSynchronizer

2.1 lock

基于CAS尝试将state(锁数量)从0设置为1

A、如果设置成功,设置当前线程为独占锁的线程;

B、如果设置失败,还会再获取一次锁数量,

B1、如果锁数量为0,再基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;

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

/**
 * Performs {@link Lock#lock}. The main reason for subclassing
 * is to allow fast path for nonfair version.
 */
abstract void lock();
  • 执行锁。抽象了该方法的原因是,允许子类实现快速获得非公平锁的逻辑。

2.2 nonfairTryAcquire

nonfairTryAcquire(int acquires) 方法,非公平锁的方式获得锁。代码如下:

final boolean nonfairTryAcquire(int acquires) {
    //当前线程
    final Thread current = Thread.currentThread();
    //获取同步状态
    int c = getState();
    //state == 0,表示没有该锁处于空闲状态
    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;
}
  • 该方法主要逻辑:首先判断同步状态state == 0?

    • 如果是,表示该锁还没有被线程持有,直接通过CAS获取同步状态。

      • 如果成功,返回 true 。

      • 否则,返回 false 。

    • 如果不是,则判断当前线程是否为获取锁的线程?

      • 如果是,则获取锁,成功返回 true 。成功获取锁的线程,再次获取锁,这是增加了同步状态 state 。通过这里的实现,我们可以看到上面提到的 “它使用 AQS 的 state 字段,来表示当前锁的持有数量,从而实现可重入的特性”。

      • 否则,返回 false 。

  • 理论来说,这个方法应该在子类 FairSync 中实现,但是为什么会在这里呢?在下文的 ReentrantLock.tryLock()中,详细解析。

2.3 tryRelease

tryRelease(int releases) 实现方法,释放锁。代码如下:

protected final boolean tryRelease(int releases) {
    // 减掉releases
    int c = getState() - releases;
    // 如果释放的不是持有锁的线程,抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // state == 0 表示已经释放完全了,其他线程可以获取同步状态了
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
  • 通过判断判断是否为获得到锁的线程,保证该方法线程安全。

  • 只有当同步状态彻底释放后,该方法才会返回 true 。state == 0 时,则将锁持有线程设置为 nullfree= true,表示释放成功。

2.4 其他实现方法

// 是否当前线程独占
@Override
protected final boolean isHeldExclusively() {
    // While we must in general read state before owner,
    // we don't need to do so to check if current thread is owner
    return getExclusiveOwnerThread() == Thread.currentThread();
}

// 新生成条件
final ConditionObject newCondition() {
    return new ConditionObject();
}

// Methods relayed from outer class

// 获得占用同步状态的线程
final Thread getOwner() {
    return getState() == 0 ? null : getExclusiveOwnerThread();
}

// 获得当前线程持有锁的数量
final int getHoldCount() {
    return isHeldExclusively() ? getState() : 0;
}

// 是否被锁定
final boolean isLocked() {
    return getState() != 0;
}

/**
 * Reconstitutes the instance from a stream (that is, deserializes it).
 * 自定义反序列化逻辑
 */
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    s.defaultReadObject();
    setState(0); // reset to unlocked state
}
  • 从这些方法中,我们可以看到,ReentrantLock 是独占获取同步状态的模式。

3. Sync 实现类

3.1 NonfairSync

获取锁的步骤

基于CAS尝试将state(锁数量)从0设置为1

A、如果设置成功,设置当前线程为独占锁的线程;

B、如果设置失败,还会再获取一次锁数量,

B1、如果锁数量为0,再基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;

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

NonfairSync 是 ReentrantLock 的内部静态类,实现 Sync 抽象类,非公平锁实现类。

3.1.1 lock

lock() 实现方法,首先基于 AQS state 进行 CAS操作,将 0 设置为1 。若成功,则获取锁成功。若失败,执行 AQS 的正常的同步状态获取逻辑。代码如下:

/**
 * 1)首先基于CAS将state(锁数量)从0设置为1,如果设置成功,设置当前线程为独占锁的线程;-->请求成功-->第一次插队
 * 2)如果设置失败(即当前的锁数量可能已经为1了,即在尝试的过程中,已经被其他线程先一步占有了锁),这个时候当前线程执行acquire(1)方法
 	//获取锁的方法
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();//中断自己
    }

  * 2.1)acquire(1)方法首先调用下边的tryAcquire(1)方法,在该方法中,首先获取锁数量状态,
  * 2.1.1)如果为0(证明该独占锁已被释放,当下没有线程在使用),这个时候我们继续使用CAS将state(锁数量)从0设置为1,如果设置成功,当前线程独占锁;-->请求成功-->第二次插队;当然,如果设置不成功,直接返回false
  * 2.2.2)如果不为0,就去判断当前的线程是不是就是当下独占锁的线程,如果是,就将当前的锁数量状态值+1(这也就是可重入锁的名称的来源)-->请求成功
      * 
      * 下边的流程一句话:请求失败后,将当前线程链入队尾并挂起,之后等待被唤醒。
      * 
  * 2.2.3)如果最后在tryAcquire(1)方法中上述的执行都没成功,即请求没有成功,则返回false,继续执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法
  * 2.2)在上述方法中,首先会使用addWaiter(Node.EXCLUSIVE)将当前线程封装进Node节点node,然后将该节点加入等待队列(先快速入队,如果快速入队不成功,其使用正常入队方法无限循环(即自旋)一直到Node节点入队为止)
  * 2.2.1)快速入队:如果同步等待队列存在尾节点,将使用CAS(compareAndSetTail(pred, node))尝试将尾节点设置为node,并将之前的尾节点插入到node之前(即Node pred = tail;node.prev = pred;)
  * 2.2.2)正常入队:如果同步等待队列不存在尾节点或者上述CAS尝试不成功的话,就执行正常入队(enq(node);该方法是一个无限循环的过程,即直到入队为止)-->第一次阻塞
  * 2.2.2.1)如果尾节点为空(初始化同步等待队列),创建一个dummy节点,并将该节点通过CAS尝试设置到头节点上去,设置成功的话,将尾节点也指向该dummy节点(即头节点和尾节点都指向该dummy节点)
  * 2.2.2.1)如果尾节点不为空,执行与快速入队相同的逻辑,即使用CAS尝试将尾节点设置为node,并将之前的尾节点插入到node之前
 * 最后,如果顺利入队的话,就返回入队的节点node,如果不顺利的话,无限循环去执行2.2)下边的流程,直到入队为止
 * 2.3)node节点入队之后,就去执行acquireQueued(final Node node, int arg)(这又是一个无限循环的过程,这里需要注意的是,无限循环等于阻塞,多个线程可以同时无限循环--每个线程都可以执行自己的循环,这样才能使在后边排队的节点不断前进)
 * 2.3.1)获取node的前驱节点p,如果p是头节点,就继续使用tryAcquire(1)方法去尝试请求成功,-->第三次插队(当然,这次插队不一定不会使其获得执行权,请看下边一条),
 * 2.3.1.1)如果第一次请求就成功,不用中断自己的线程,如果是之后的循环中将线程挂起之后又请求成功了,使用selfInterrupt()中断自己
 * (注意p==head&&tryAcquire(1)成功是唯一跳出循环的方法,在这之前会一直阻塞在这里,直到其他线程在执行的过程中,不断的将p的前边的节点减少,直到p成为了head且node请求成功了--即node被唤醒了,才退出循环)
 * 2.3.1.2)如果p不是头节点,或者tryAcquire(1)请求不成功,就去执行shouldParkAfterFailedAcquire(Node pred, Node node)来检测当前节点是不是可以安全的被挂起,
 * 2.3.1.2.1)如果node的前驱节点pred的等待状态是SIGNAL(即可以唤醒下一个节点的线程的状态),则node节点的线程可以安全挂起,返回true,执行2.3.1.3)
 * 2.3.1.2.2)如果node的前驱节点pred的等待状态是CANCELLED,则pred的线程被取消了,我们会将pred之前的连续几个被取消的前驱节点从队列中剔除,返回false(即不能挂起),如下:
 do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
 
 之后继续执行2.3)中上述的代码的cancelAcquire(node);
 * 2.3.1.2.3)如果node的前驱节点pred的等待状态是除了上述两种的其他状态,则使用CAS尝试将前驱节点的等待状态设为SIGNAL,并返回false(因为CAS可能会失败,这里不管失败与否,都返回false,下一次执行该方法的之后,pred的等待状态就是SIGNAL了),之后继续执行2.3)中上述的代码的cancelAcquire(node);
 * 2.3.1.3)如果可以安全挂起,就执行parkAndCheckInterrupt()挂起当前线程,之后,继续执行2.3)中之前的代码
* 最后,直到该节点的前驱节点p之前的所有节点都执行完毕为止,我们的p成为了头节点,并且tryAcquire(1)请求成功,跳出循环,去执行。
 * (在p变为头节点之前的整个过程中,我们发现这个过程是不会被中断的)
 * 2.3.2)当然在2.3.1)中产生了异常,我们就会执行cancelAcquire(Node node)取消node的获取锁的意图。
*/
        final void lock() {
            if (compareAndSetState(0, 1))//如果CAS尝试成功
                setExclusiveOwnerThread(Thread.currentThread());//设置当前线程为独占锁的线程
            else
                acquire(1);
        }
  • 优先基于 AQS state 进行 CAS 操作,已经能体现出非公平锁的特点。因为,此时有可能有 N + 1 个线程正在获得锁,其中 1 个线程已经获得到锁,释放的瞬间,恰好被新的线程抢夺到,而不是排队的 N 个线程。

3.1.2 tryAcquire

tryAcquire(int acquires) 实现方法,非公平的方式,获得同步状态。代码如下:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
  • 直接调用 nonfairTryAcquire(int acquires) 方法,非公平锁的方式获得锁。

/**
         * 非公平锁中被tryAcquire调用
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//获取当前线程
            int c = getState();//获取锁数量
            if (c == 0) {//如果锁数量为0,证明该独占锁已被释放,当下没有线程在使用
                if (compareAndSetState(0, acquires)) {//继续通过CAS将state由0变为1,注意这里传入的acquires为1
                    setExclusiveOwnerThread(current);//将当前线程设置为独占锁的线程
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//查看当前线程是不是就是独占锁的线程
                int nextc = c + acquires;//如果是,锁状态的数量为当前的锁数量+1
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);//设置当前的锁数量
                return true;
            }
            return false;
        }

3.2 FairSync

FairSync 是 ReentrantLock 的内部静态类,实现 Sync 抽象类,公平锁实现类。

lock的步骤:获取一次锁数量,

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

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

3.2.1 lock

lock() 实现方法,代码如下:

final void lock() {
    acquire(1);
}
  • 直接执行 AQS 的正常的同步状态获取逻辑。

3.2.2 tryAcquire

tryAcquire(int acquires) 实现方法,公平的方式,获得同步状态。代码如下:

 	 /**
         * 获取公平锁的方法
         * 1)获取锁数量c
         * 1.1)如果c==0,如果当前线程是等待队列中的头节点,使用CAS将state(锁数量)从0设置为1,如果设置成功,当前线程独占锁-->请求成功
         * 1.2)如果c!=0,判断当前的线程是不是就是当下独占锁的线程,如果是,就将当前的锁数量状态值+1(这也就是可重入锁的名称的来源)-->请求成功
         * 最后,请求失败后,将当前线程链入队尾并挂起,之后等待被唤醒。
         */
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() && // <1>
                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;
}

比较非公平锁和公平锁获取同步状态的过程,会发现两者唯一的区别就在于,公平锁在获取同步状态时多了一个限制条件 <1> 处的 hasQueuedPredecessors() 方法,是否有前序节点,如果返回true,即表示自己不是首个等待获取同步状态的节点。代码如下:

// AbstractQueuedSynchronizer.java
public final boolean hasQueuedPredecessors() {
    Node t = tail;  //尾节点
    Node h = head;  //头节点
    Node s;

    //头节点 != 尾节点
    //同步队列第一个节点不为null
    //当前线程是同步队列第一个节点
    return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
}
  • 该方法主要做一件事情:主要是判断当前线程是否位于 CLH 同步队列中的第一个。如果是则返回 true ,否则返回 false 。

总结:公平锁与非公平锁对比

  • FairSync:lock()少了插队部分(即少了CAS尝试将state从0设为1,进而获得锁的过程)

  • FairSync:tryAcquire(int acquires)多了需要判断当前线程是否在等待队列首部的逻辑(实际上就是少了再次插队的过程,但是CAS获取还是有的)。

最后说一句,

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

4. Lock 接口

java.util.concurrent.locks.Lock 接口,定义方法如下:

void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();

5. ReentrantLock

java.util.concurrent.locks.ReentrantLock ,实现 Lock 接口,重入锁。

ReentrantLock 的实现方法,基本是对 Sync 的调用。

5.1 构造方法

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
  • 基于 fair 参数,创建 FairSync 还是 NonfairSync 对象。

5.2 lock

/**
     *获取一个锁
     *三种情况:
     *1、如果当下这个锁没有被任何线程(包括当前线程)持有,则立即获取锁,锁数量==1,之后再执行相应的业务逻辑
     *2、如果当前线程正在持有这个锁,那么锁数量+1,之后再执行相应的业务逻辑
     *3、如果当下锁被另一个线程所持有,则当前线程处于休眠状态,直到获得锁之后,当前线程被唤醒,锁数量==1,再执行相应的业务逻辑
     */
public void lock() {
    sync.lock();	//调用NonfairSync(非公平锁)或FairSync(公平锁)的lock()方法
}

//获取公平锁
/**
     *获取一个锁
     *三种情况:
     *1、如果当下这个锁没有被任何线程(包括当前线程)持有,则立即获取锁,锁数量==1,之后被唤醒再执行相应的业务逻辑
     *2、如果当前线程正在持有这个锁,那么锁数量+1,之后被唤醒再执行相应的业务逻辑
     *3、如果当下锁被另一个线程所持有,则当前线程处于休眠状态,直到获得锁之后,当前线程被唤醒,锁数量==1,再执行相应的业务逻辑
     */

5.3 lockInterruptibly

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

 

6.synchronized 和 ReentrantLock 异同?

  • 相同点

    • 都实现了多线程同步和内存可见性语义。

    • 都是可重入锁。

  • 不同点

    • 同步实现机制不同

      • synchronized 通过 Java 对象头锁标记和 Monitor 对象实现同步。

      • ReentrantLock 通过CAS、AQS(AbstractQueuedSynchronizer)和 LockSupport(用于阻塞和解除阻塞)实现同步。

    • 可见性实现机制不同

      • synchronized 依赖 JVM 内存模型保证包含共享变量的多线程内存可见性。

      • ReentrantLock 通过 ASQ 的 volatile state 保证包含共享变量的多线程内存可见性。

    • 使用方式不同

      • synchronized 可以修饰实例方法(锁住实例对象)、静态方法(锁住类对象)、代码块(显示指定锁对象)。

      • ReentrantLock 显示调用 tryLock 和 lock 方法,需要在 finally 块中释放锁。

    • 功能丰富程度不同

      • synchronized 不可设置等待时间、不可被中断(interrupted)。

      • ReentrantLock 提供有限时间等候锁(设置过期时间)、可中断锁(lockInterruptibly)、condition(提供 await、condition(提供 await、signal 等方法)等丰富功能

    • 锁类型不同

      • synchronized 只支持非公平锁。

      • ReentrantLock 提供公平锁和非公平锁实现。当然,在大部分情况下,非公平锁是高效的选择。

      • ReentrantLock 支持中断处理,且性能较 synchronized 会好些。

synchronized 优化以前,它的性能是比 ReenTrantLock 差很多的,但是自从 synchronized 引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用 synchronized

并且,实际代码实战中,可能的优化场景是,通过读写分离,进一步性能的提升,所以使用 ReentrantReadWriteLock 。

 

 

posted @ 2019-03-20 11:43  白晨冬阳  阅读(162)  评论(0编辑  收藏  举报