【Java 多线程并发】 ReentrantLock

Metadata

title: 【Java 多线程并发】 ReentrantLock
date: 2023-07-04 17:10
tags:
  - 行动阶段/完成
  - 主题场景/程序
  - 笔记空间/KnowladgeSpace/ProgramSpace/BasicsSpace
  - 细化主题/Java
categories:
  - Java
keywords:
  - Java
description: ReentrantLock是一个可重入且互斥的锁,它具有与使用synchronized监视器锁相同的基本行为和语义,但它是继承了Lock接口,与synchronized关键字相比,它更灵活、更强大,增加了轮询、超时、中断等高级功能。

概述

ReentrantLock就是一个互斥锁,默认是非公平锁。并且是一个可中断的锁。

特点

  • 可重入性
  • 公平性
  • 条件变量
  • 可中断

使用流程

  1. 创建一个ReentrantLock对象。
  2. 在需要保护共享资源的代码块中,使用lock()方法获取锁。
  3. 在使用完共享资源后,使用unlock()方法释放锁。
  4. 相比于synchronized关键字,ReentrantLock提供了更多的灵活性和功能,但也需要手动管理锁的获取和释放。

原理
ReentrantLock是基于AQS来实现的
AQS中还维护了一个条件锁内部类ConditionObject,这个内部类中也维护了一个条件队列

数据结构

三个内部类:Sync、NonfairSync、FairSync。

  1. 抽象类Sync继承自AbstractQueuedSynchronizer,实现了AQS的部分方法;
  2. NonfairSync继承自Sync,实现了Sync中的方法,主要用于非公平锁的获取;
  3. FairSync继承自Sync,实现了Sync中的方法,主要用于公平锁的获取。

【Java 多线程并发】 ReentrantLock

ReentrantLock是一个可重入且互斥的锁,它具有与使用synchronized监视器锁相同的基本行为和语义,但它是继承了Lock接口,与synchronized关键字相比,它更灵活、更强大,增加了轮询、超时、中断等高级功能。

Reentrant = Re + entrant,Re是重复、又、再的意思,entrant是enter的名词或者形容词形式,翻译为进入者或者可进入的,所以Reentrant翻译为可重复进入的、可再次进入的,因此ReentrantLock翻译为可重入锁或者再入锁。

顾名思义,重入锁是指一个线程获取锁之后再尝试获取锁时会自动获取锁,是一种递归无阻塞的同步机制,synchronized也是重入锁。ReentrantLock除了支持可重入锁之外,还支持获取锁时的公平和非公平选择。

ReentrantLock的类图如下:

我们可以看到ReentrantLock只是实现了Lock接口,但是其本身并没有继承AQS抽象类,而是通过ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。如果在绝对时间上,先对锁进行获取的请求你一定先被满足,那么这个锁是公平的,反之,是不公平的。公平锁的获取,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。ReentrantLock的公平与否,可以通过它的构造函数来决定。ReentrantLock默认是非公平锁,之所以这样是因为非公平锁普遍情况下比公平锁效率要高。

那么公平锁为什么普遍没有非公平锁的效率高呢?这是因为非公平模式会在一开始就尝试两次获取锁,如果当时正好state的值为0,它就会成功获取到锁,少了排队导致的阻塞/唤醒过程,并且减少了线程频繁的切换带来的性能损耗。但是,并不是任何场景都是以TPS作为唯一指标,非公平锁有可能会导致一开始排队的线程一直获取不到锁,导致线程饿死。而公平锁能够减少“饥饿”发生的概率,等待越久的请求越能够得到优先满足。

总结起来,ReentrantLock就是一个互斥锁,默认是非公平锁。并且是一个可中断的锁。

ReentrantLock的特点

可重入性

与synchronized关键字一样,ReentrantLock允许线程重复获取同一个锁。当一个线程已经获取了锁,并且再次请求获取该锁时,它将会成功获取锁,而不会被阻塞。这种机制可以避免死锁情况的发生。

公平性

ReentrantLock可以选择公平锁和非公平锁。公平锁会按照线程的请求顺序来获取锁,而非公平锁允许插队,可能导致某些线程始终无法获取到锁。默认情况下,ReentrantLock是非公平锁。

条件变量

ReentrantLock提供了条件变量(Condition),可以通过条件变量实现更灵活的线程间通信。通过调用ReentrantLock的newCondition()方法来创建一个条件变量,然后可以使用await()、signal()和signalAll()等方法进行线程的等待和唤醒。

可中断

ReentrantLock支持线程的中断。当一个线程等待获取锁时,可以通过调用interrupt()方法中断该线程,使其放弃对锁的请求。

使用流程

使用ReentrantLock的基本流程如下:

  1. 创建一个ReentrantLock对象。
  2. 在需要保护共享资源的代码块中,使用lock()方法获取锁。
  3. 在使用完共享资源后,使用unlock()方法释放锁。
  4. 相比于synchronized关键字,ReentrantLock提供了更多的灵活性和功能,但也需要手动管理锁的获取和释放。在一些复杂的并发场景下,使用ReentrantLock可以更好地控制线程的访问顺序和等待机制。
Lock lock = new ReentrantLock();
lock.lock();
 
try {
    // do something...
} finally {
    lock.unlock();
}

在使用Lock接口下的锁时,要注意以下几点:

  • 要在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。
  • 尽量不要将获取锁的过程写在try块中,因为如果在获取锁(可以是自定义的锁)的过程中发生了异常,在异常抛出的同时,也会导致锁的无故释放。

ReentrantLock的数据结构

主要内部类

ReentrantLock中主要定义了三个内部类:Sync、NonfairSync、FairSync。

abstract static class Sync extends AbstractQueuedSynchronizer {}
 
static final class NonfairSync extends Sync {}
 
static final class FairSync extends Sync {}
  1. 抽象类Sync继承自AbstractQueuedSynchronizer,实现了AQS的部分方法;
  2. NonfairSync继承自Sync,实现了Sync中的方法,主要用于非公平锁的获取;
  3. FairSync继承自Sync,实现了Sync中的方法,主要用于公平锁的获取。

由此可以看出,AQS是ReentrantLock类的基石。

Sync类源码

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;
    
    abstract void lock();
   
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        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");
            setState(nextc);
            return true;
        }
        return false;
    }
 
    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;
    }
 
    protected final boolean isHeldExclusively() {
        return getExclusiveOwnerThread() == Thread.currentThread();
    }
 
    final ConditionObject newCondition() {
        return new ConditionObject();
    }
    final Thread getOwner() {
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }
    final int getHoldCount() {
        return isHeldExclusively() ? getState() : 0;
    }
    final boolean isLocked() {
        return getState() != 0;
    }
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }
}

NonfairSync类源码

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
 
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
 
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

FairSync类源码

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
    final void lock() {
        acquire(1);
    }
 
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            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;
    }
}

公平锁和非公平锁的实现类中只实现了获取锁的方法,也就是说只有获取锁时非公平锁和公平锁有区别,在释放锁的时候就都是用AQS提供的释放锁的模板方法了,公平锁和非公平锁的释放锁的方法也就没有区别了。

主要属性

private final Sync sync;

主要属性就一个sync,它在构造方法中初始化,决定使用公平锁还是非公平锁的方式获取锁。

主要构造方法

// 默认构造方法,ReentrantLock默认是非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}
 
// 自己可选择使用公平锁还是非公平锁,传入true是公平锁,传入false是非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
  1. 默认构造方法使用的是非公平锁;
  2. 第二个构造方法可以自己决定使用公平锁还是非公平锁;

ReentrantLock数据结构总结

我们先从ReentrantLock锁的使用角度来分析,我们在使用该锁的时候就是将需要保证线程安全的代码块用ReentrantLock.lock()和ReentrantLock.unlock()包裹起来,这样就能保证这个代码块内部的资源在同一时间段内只会被同一个线程所使用,就避免了线程风险。通过ReentrantLock的使用流程,就能更好的理解为什么它要被称为“锁”。其实ReentrantLock就相当于一个带着锁的栅栏,这个带锁的栅栏把想要保证线程安全的,也就是想要“锁”起来代码块前后“包”起来,只有持有这个ReentrantLock的线程才可以打开栅栏门进入到这个栅栏内部,其他不持有ReentrantLock的线程,是没有办法打开栅栏门进入到栅栏内部的。而这个栅栏门前,会有一条专门的“通道”,所有想要进到栅栏里面的线程必须都在这里排队等待,等到持有锁的线程从栅栏里面出来后,就会把锁的钥匙留在门口,然后在外面排队的线程中,排在第一位的线程就能拿到钥匙,成功持有了这个锁,它就可以打开栅栏门进入到栅栏内部了,这个用来排队的“通道”就是同步等待队列。这个就是锁的基本运作流程,通过这个流程我们就能很好的了解到ReentrantLock的主要数据结构。

ReentrantLock有一个继承了AQS的内部类,通过这个内部类就实现了锁的功能,这个内部类会根据用户的指定来创建公平锁实现或非公平锁实现,并且将实现对象作为ReentrantLock的一个成员属性来此存储,这个成员属性sync就是基于AQS来实现的。AQS中维护了一个同步队列(也可称为等待队列),节点类型是Node,是一个双向链表,并且AQS还继承了AbstractOwnableSynchronizer这个抽象类,这个抽象类中有一个成员属性用来记录持有当前锁,通过继承这个抽象类,AQS也就能记录当前持有锁的线程了。所以ReentrantLock就通过继承了AQS的内部类,维护了一个记录持有当前锁的线程的属性还有一个同步等待队列,进而实现锁的功能。

ReentrantLock中还有一个条件锁,这个也是通过AQS来实现的,AQS中有一个ConditionObject内部类,ConditionObject这个类实现了条件锁的功能,所以ReentrantLock就通过它的继承AQS的内部类实现了条件锁的功能。ConditionObject类中也维护了一个队列,节点类型也是Node,是一个单向链表,这个队列被称为条件队列,这个队列中就存储的是所有正在等待该条件的线程节点。一个ConditionObject类对象就可以看作是一种条件,在这个对象中维护的条件队列中存储的就全部都是等待该条件的线程,这些线程获取ReentrantLock锁之后,假如用户使用了ReentrantLock的条件锁,如果此时还没有满足条件锁ConditionObject的条件,该线程就会被阻塞等待,放到条件队列中去。所有等待该条件的线程都会被放到队列中去。当满足条件之后,就会从队列中取出第一个节点来将其唤醒继续向下执行。这就是条件锁的实现流程。

综上所述,我们就能整体的理解ReentrantLock的数据结构了,ReentrantLock是基于AQS来实现的,AQS中维护了持有当前锁的线程,还有等待该锁的同步等待队列,所有等待该锁的线程都会进入到该队列排队,依次获取锁,保证并发安全。AQS中还维护了一个条件锁内部类ConditionObject,这个内部类中也维护了一个条件队列,一个ConditionObject就代表着一种条件,所有持有该锁的线程如果不满足条件就都会进入到这个条件队列中等待,直到满足条件之后就会在条件队列中依次被唤醒。如果ReentrantLock需要条件锁的功能,就可以创建一个条件锁对象来根据用户的需要来实现相关逻辑。

所以ReentrantLock中核心的数据结构就是一个同步等待队列,一个条件锁对象中的条件队列,还有一个记录持有当前锁线程的属性,通过这几个结构实现了ReentrantLock锁的功能。

ReentrantLock的方法

获取锁(均继承自Lock接口):

void lock()

获取锁,调用该方法当前线程将会获取锁,当锁获取后,该方法将返回。

该方法优先考虑获取锁,待获取锁成功后,才响应中断,在获取到锁之前,不会响应中断。

执行过程:

调用lock()方法,在获取锁的过程中但是还没获取到锁时(尝试获取锁或正在队列中阻塞等待),如果检测到Thread.isInterrupted,线程会忽略中断,只是将记录是否中断的一个变量设置为true,然后继续尝试获取锁,直到成功获取锁之后,将记录是否终端的变量返回给上一层,在上一层进行中断处理,调用Thread.currentThread().interrupt()方法响应中断。在这个整个过程中lock()都不会抛出异常。

void lockInterruptibly() throws InterruptedException

可中断获取锁,与 lock()方法不同之处在于该方法优先考虑响应中断,线程会立刻响应中断,即在线程获取锁的过程中如果执行线程中断命令,会直接抛出异常。

执行过程:

调用lockInterruptibly()方法,在获取锁的过程中但是还没获取到锁时(尝试获取锁或正在队列中阻塞等待),如果检测到Thread.isInterrupted,线程会立刻抛出中断异常,但是在这期间并没有处理中断状态,也就是没有调用Thread.currentThread().interrupt()相关方法。异常会一层一层向外抛出,直到调用lockInterruptibly()的位置,由调用者自己处理中断。

boolean tryLock()

尝试非阻塞的获取锁,调用该方法立即返回,是一个有返回值的方法,试图申请一个锁,在成功获得锁后返回true,否则,立即返回false,并不会阻塞等待锁释放,线程可以立即离开去做其他的事情。

boolean tryLock(long time,TimeUnit unit) throws InterruptedException

超时获取锁,是一个具有超时参数的尝试获取锁的方法,阻塞时间不会超过给定的值;如果成功则返回true。以下情况会返回:

  1. 时间内获取到了锁,返回true
  2. 时间内被中断,返回false
  3. 时间到了没有获取到锁,返回false

释放锁(继承自Lock接口)

void unlock()

释放锁

条件锁(继承自Lock接口):

Condition newCondition()

创建一个条件锁

统计

int getHoldCount()

查询当前线程保持此锁定的个数,即调用lock()方法的次数。

    public class ReentrantLockMethodTest1 {
     
        private ReentrantLock reentrantLock = new ReentrantLock();
     
        public void test1(){
            try{
                reentrantLock.lock();
                System.out.println("Lock count---" + reentrantLock.getHoldCount());
                test2();
            } finally {
                reentrantLock.unlock();
            }
        }
     
        public void test2(){
     
            try{
                reentrantLock.lock();
                System.out.println("Lock count---" + reentrantLock.getHoldCount());
            }finally {
                reentrantLock.unlock();
            }
        }
     
        public static void main(String[] args){
     
            ReentrantLockMethodTest1 reentrantLockMethodTest1 = new ReentrantLockMethodTest1();
            reentrantLockMethodTest1.test1();
        }
}

运行结果:

Collection<Thread> getQueuedThreads()

返回正在等待获取此锁的线程的集合。

boolean isHeldByCurrentThread()

判断当前线程是否获取到了该锁

ReentrantLock原理总结

自旋、LockSupport、CAS、队列

自旋

通过自旋,让线程等待资源被释放。

LockSupport

但是如果一直自旋,就会一直抢占CPU,自旋就是一个死循环,什么也不干,这样反而会白白浪费CPU资源,所以就还引入了LockSupport,来对线程进行休眠阻塞,这样就能把CPU资源释放出来。

CAS

CAS保证了互斥性,可以在不影响太多效率的情况下,让资源不会被多个线程同时进行抢占

队列

被阻塞的线程就会被添加到队列中,线程就在队列中排队等待获取资源。

源码分析

首先我们来看获取锁的这几个方法源码实现。主要有:lock()、lockInterruptibly()、tryLock()、tryLock(long time,TimeUnit unit)

lock()方法

获取锁,调用该方法当前线程将会获取锁,当锁获取后,该方法将返回。该方法优先考虑获取锁,待获取锁成功后,才响应中断,在获取到锁之前,不会响应中断。

lock()方法涉及到非公平锁和公平锁两种不同的实现方法,这里我们就从公平锁和非公平锁两个角度来讲解源码的实现。

后面的其他获取锁的方法有一些也会因为RenntractLock属性sync的实现类不同,而导致实现源码不同的情况。也就是会因公平锁和非公平锁的不同而采用不同的实现方法,但是依旧是使用不同的sycn实现类方法而已。

公平锁

这里我们假设ReentrantLock的实例是通过以下方式获得的:

ReentrantLock reentrantLock = new ReentrantLock(true);
 
public ReentrantLock(booleanfair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

下面的是加锁的主要逻辑:

FairSync.lock()
// ReentrantLock.lock()
public void lock() {
    // 调用的sync属性的lock()方法
    // 我们这一节讲公平锁,所以这里的sync是FairSync的实例
    sync.lock();
}
 
// ReentrantLock.FairSync.lock()
final void lock() {
    // 调用AQS的acquire()方法获取锁
    // 注意,这里传的值为1
    acquire(1);
}
 
// AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) { // AQS提供的可以直接调用的模板方法
    // 尝试获取锁
    if (!tryAcquire(arg) && // 锁竞争逻辑
        /**
         * 如果上面获取锁失败了,就入队等待
         * 1、addWaiter():为当前线程创建一个Node,添加到阻塞等待队列中,队列就是一个双向链表。在这个方法中的线程还是在运行当中的
         *    注意addWaiter()这里传入的节点模式为独占模式。addWaiter()入参是用来标识当前锁是共享锁还是互斥锁的标志,ReentrantLock是一个互斥锁,所以这里传入的是Node.EXCLUSIVE互斥标志
         * 2、acquireQueued():对新添加到队列中的等待线程进行阻塞,线程是在这个方法中被阻塞的。线程被阻塞后再被唤醒,会继续在这个方法中自旋去调用tryAcquire(arg)尝试获取锁,最后成功获取锁并返回
         */
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 这一步操作就是调用中断方法来改变线程的中断状态,这样做就可以把中断信号往外层转递,程序员自己设计的代码也就可以识别到中断信号来自定义相关响应操作。执行这个方法是当前线程肯定是没有获取到锁
        selfInterrupt();
}
 
// ReentrantLock.FairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) { // tryAcquire方法(尝试非阻塞获取锁)是由AQS进行声明,但是需要使用者自行实现的方法。所以这个方法是由ReentrantLock实现的
    // 当前线程
    final Thread current = Thread.currentThread();
    // 查看当前状态变量的值
    int c = getState();
    // 如果状态变量的值为0,说明暂时还没有人占有锁,可以去尝试获取锁
    if (c == 0) {
        // !hasQueuedPredecessors():如果没有其它线程在排队,那么当前线程尝试更新state的值为acquires
        // 如果更新成功,则说明当前线程获取了锁
        if (!hasQueuedPredecessors() && // 判断当前是否已经有其他线程正在等待。 通过这个方法就保证了如果等待队列中第一个线程节点被release()唤醒来抢占锁,它是100%能够成功获取锁的,这就是公平锁的特性。如果是非公平锁,当队列头部节点被release()唤醒后就不一定能抢占到锁了,有可能被别的线程抢先一步抢占
            compareAndSetState(0, acquires)) { // 相当于一个比较交换的原子操作,由unsafe提供的native本地方法
            // 当前线程获取了锁,把自己设置到exclusiveOwnerThread变量中
            // exclusiveOwnerThread是AQS的父类AbstractOwnableSynchronizer中提供的变量
            setExclusiveOwnerThread(current);
            // 返回true说明成功获取了锁
            return true;
        }
    }
    // 如果当前线程本身就占有着锁,现在又尝试获取锁,那么,直接让它获取锁并返回true。这里我们就可以看出来ReentrantLock实现了可重入性
    else if (current == getExclusiveOwnerThread()) {
        // 状态变量state的值加acquires
        int nextc = c + acquires;
        // 如果溢出了,则报错
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 将累加的新状态量设置到state变量中
        // 这里不需要CAS更新state
        // 因为当前线程占有着锁,其它线程只会CAS把state从0更新成1,是不会成功的
        // 所以不存在竞争,自然不需要使用CAS来更新
        setState(nextc);
        // 当线程获取锁成功
        return true;
    }
    // 当前线程尝试获取锁失败
    return false;
}
 
// AbstractQueuedSynchronizer.hasQueuedPredecessors()
public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    // head指针节点
    Node h = head;
    Node s;
    // 这个方法就是查看队头有没有线程节点,如果有就看一下这个队头线程节点是不是当前线程节点,如果队头存在节点并且不是当前线程节点,就说明此时队列中有别的线程正在等待,返回true
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
 
// AbstractQueuedSynchronizer.addWaiter()
// 调用这个方法,说明上面tryAcquire()尝试获取锁失败了。addWaiter方法传入的参数是Node的属性:共享属性(Node.SHARED),独占(互斥)属性(Node.EXCLUSIVE)
// 线程入队,将Node节点加入同步对联,而Node节点是与Thread绑定的,也就相当于将线程入队。addWaiter这个操作将线程入队之后,并没有将线程阻塞,对线程的阻塞操作在acquireQueued方法中
创建节点Node,Node中的重要属性有这些: pre,next,waitestate,thread
    waitestate节点的生命状态:信号量  一个节点的waitestate属性值表示的是这个节点下一个节点的生命状态:
        SIGNAL = -1 //可被唤醒   指这个节点的下一个节点可被唤醒
        CANCELLED = 1 //代表出现异常,中断引起的,需要废弃结束
        CONDITION = -2 // 条件等待,这个是在条件锁中使用的Node状态
        PROPAGATE = -3 // 传播,可以用来广播的状态
        0 // 初始状态Init状态。一个对象被创建出来,int类型的属性默认值都是0
 
private Node addWaiter(Node mode) {
    // 为当前线程创建一个Node节点。Node是AbstractQueuedSynchronizer中的一个静态内部类,主要用来存储线程信息
    Node node = new Node(Thread.currentThread(), mode);
    /**
     * 这里先尝试把新节点加到尾节点后面(尾插法)
     * 如果成功了就返回新节点
     * 如果没成功再调用enq()方法不断尝试添加
     */
    // 获取尾节点
    Node pred = tail;
    // 如果尾节点不为空,则将新建节点尾插到CLH等待队列中
    if (pred != null) {
        // 设置新节点的前驱节点为现在的尾节点
        node.prev = pred;
        // CAS更新尾节点为新节点
        if (compareAndSetTail(pred, node)) {
            // 如果成功了,把旧尾节点的下一个节点指向新节点
            pred.next = node;
            // 并返回新节点
            return node;
        }
    }
    // 如果上面尝试入队新节点没成功(有两个原因:1、被别的线程抢先插入了节点。 2、当前CLH队列还未初始化),调用enq()处理
    enq(node);
    return node;
}
 
// AbstractQueuedSynchronizer.enq()
// 将线程节点添加到等待队列
private Node enq(final Node node) {
    // 自旋,不断尝试将节点添加到CLH等待队列
    for (;;) {
        // 本轮循环获取现在最新的队列尾节点
        Node t = tail;
        /**
         * 如果尾节点为空,说明队列还未初始化
         * 这个分支是为了给CLH队列初始化,在AQS中规定,如果head=tail=null,就说明这个队列还没有初始化。
         * 创建一个空的Node节点,然后要head和tail都先指向这个空的节点,这样就完成CLH队列初始化。
         * 其实就相当于队列头指向的是一个空节点,然后空节点后面的才是真正存储信息的Node节点。
         * 之所以这样设计,是因为每个节点的waitStatus = SIGNAL时,表示的是下一个节点的生存状态,所以队列中第一个存有线程的节点,必须也要有一个前驱结点来标识它的生存状态
         */
        if (t == null) {
            // 初始化head头指针节点和tail尾指针节点
            if (compareAndSetHead(new Node())) // 创建一个空的Node节点,并且用CAS将其赋值给head属性
                // 将尾节点指向当前初始化的头节点,完成队列初始化。然后在下一轮循环中就可以尝试将Node节点插入队列了
                tail = head;
        } else {
            // 如果尾节点不为空,说明当前队列已经完成了初始化,可以尝试向队列中插入Node节点
            // 设置新节点的前一个节点为现在的尾节点
            node.prev = t;
            // 因为入队也存在竞争,所以通过一个比较交换的原子操作,先看一下当前队列的尾节点是不是我们预想的尾节点t,如果是,说明队列还没有被修改过,插入不存在并发风险,直接插入。
            // 如果不是t,说明队列已经被修改,需要重新继续进行自旋,获取最新的tail,来准备入队。
            if (compareAndSetTail(t, node)) { // compareAndSetTail方法在判断可以将node尾插到尾部后,就会将node的pre节点指向现在的tail,然后再将tail指向刚刚插入的node节点,这两步是用原子操作完成的
                // 成功了,则设置旧尾节点的下一个节点为新节点
                t.next = node;
                // 并返回旧尾节点,结束自旋
                return t;
            }
        }
        // 开始新一轮循环,更新最新的tail节点
    }
}
 
// AbstractQueuedSynchronizer.acquireQueued()
// 调用上面的addWaiter()方法使得新节点已经成功入队了,下面这个方法就是将当前节点阻塞。当线程被唤醒之后,也会在这个方法中继续自旋调用tryAcquire(arg)尝试获取锁,直到成功获取锁后返回
//   该方法的返回值是中断标记,如果该方法是正常返回的,那么说明当前线程一定已经成功获取锁了,这个返回值只是用来判断该线程的中断标志的
//   如果是true,说明线程是被中断唤醒的,需要响应中断信号,如果是false,则说明线程没有被中断
/**
    如果当前节点是队列中的第一个线程节点,节点阻塞之前还得再尝试一次获取锁:
    1、能够获取到,节点出队,并且把head指针往后挪一个节点,也就是指向了当前节点,然后将当前节点的内部属性都清空,当前节点就变成了head指针节点,也就完成了当前节点的出队
    2、不能获取到,将节点阻塞,然后等待被唤醒
        1.首先第1轮循环,修改当前节点的前驱节点的的waitStatus生命状态,修改成sinal=-1,表示当前节点可以被唤醒,只有保证当前节点可以被唤醒之后,才可以放心的将其阻塞
        2.第2轮循环,阻塞线程,并且需要判断线程是否是由中断信号唤醒的,进而设置中断信号,在该线程成功获取到锁之后去响应中断
 */
final boolean acquireQueued(final Node node, int arg) {
    // 阻塞线程是否失败
    boolean failed = true;
    try {
        // 中断标记,是否要将当前线程中断
        boolean interrupted = false;
        // 自旋
        for (;;) {
            // 获取当前节点的前一个节点
            final Node p = node.predecessor();
            // 判断当前节点的前一个节点是否为head指针节点,如果是,说明当前节点是队列中的第一个线程节点,则不对其进行阻塞,来让这个线程再去尝试抢一次资源。
            // 因为阻塞和唤醒操作涉及到用户态和内核态的上下文切换,非常耗时,尽量避免阻塞和唤醒操作。不管是公平锁还是非公平锁acquireQueued()的操作都是一样的
            if (p == head && tryAcquire(arg)) { // 调用ReentrantLock.FairSync.tryAcquire()方法再次尝试获取锁,如果是公平锁,头部节点被唤醒一定能成功获取到锁,但是非公平锁就不一定了
                // 尝试获取锁成功,下面就需要将当前节点的node出队
                // 将等待队列队中第一个线程节点node出队并不会涉及到并发异常的问题,因为仅仅只是将head指针向后移动一位,不涉及到其他的更改,所以不需要用CAS更新
                // 将head指针节点指向node,并且将node节点的前驱节点和代表的线程属性都设置为null
                setHead(node);
                
                /**
                 * p是原head指针节点,也就是原来node节点的前驱结点,将原head指针节点从队列中删除
                 * 将p节点,也就是head指针节点的next设置为空。
                 * 此时head和tail都指向空的node节点,这里虽然没有操作tail的指向,但是在进入到acquireQueued之前,tail就指向了node节点(将node尾插入队列,tail就会指向node节点),
                 * 在调用setHead方法的时候,又让head重新指向了node,所以此时head和tail又指向了同一个空节点,恢复到了初始状态
                 */
                p.next = null; // 这个p.next指向的是原来head指向的节点,但是现在head向后移动了一个位置,所以p节点,也就是原head节点也没什么用了,将其从队列中删除,将它的next指向设置为空,p节点就成了不可达对象,会被GC回收
                // 将阻塞标志设置为false,说明没有阻塞节点线程
                failed = false;
                /**
                 * 通过这个源码我们就发现,如果使用lock()方法获取锁,在尝试获取锁的过程中这个方法时忽略了中断信号的。
                 * 但是在尝试获取锁的过程中,当前线程的中断状态被一直记录在了该方法中的局部变量interrupted中
                 * 当该线程成功获取到锁的时候,才会响应中断信号,将记录在interrupted局部变量中的中断信号向上一层返回,在上一层的selfInterrupt()调用Thread.currentThread().interrupt()方法响应中断
                 * 所以lock()方法优先考虑获取锁,待获取锁成功后,才响应中断,在获取到锁之前,不会响应中断。
                 */
                // 返回中断标记,告诉上一层调用位置,当前线程要不要对其进行中断
                return interrupted;
            }
            /**
             * 如果上面没有成功获取到资源,就需要对当前的线程进行阻塞。
             * 1、首先判断node节点能否被阻塞
             *      shouldParkAfterFailedAcquire()方法用来判断node节点能都被阻塞。
             *      这里就是用到了每个Node节点的watStatus属性,只有当一个节点的前驱节点的watStatus属性是-1(可被唤醒)的时候,该节点才可以被从队列中唤醒。
             *      这个方法就是用来判断当前node节点是不是可唤醒的状态,只有这个node节点是可唤醒的状态,我们才可以对其进行阻塞。
             *      因为如果这个节点不能被唤醒的状态我们就对他阻塞,这个节点将永远不能被唤醒,就会留在线程栈空间中,造成空间浪费堆积。
             * 
             * 2、然后对node节点进行阻塞
             *      parkAndCheckInterrupt()方法用来对node节点进行阻塞
             *      阻塞的线程会停留在parkAndCheckInterrupt方法中,直到被唤醒。当被阻塞节点在队列中排到第一个节点时,调用release()就会将其唤醒
             *      被唤醒之后就会继续进行循环自旋,在下一轮的循环中执行到if (p == head && tryAcquire(arg))分支时,被唤醒的队列头线程节点(队列头线程节点指的是head指针节点的下一个节点)就能够成功获取到资源了。
             *      注意除了队列头线程节点被唤醒之外,其他后面的节点还是处在阻塞状态,不在自旋,这也就是我们前面所说的节约CPU资源。  
             */
            // 判断是否可以将node节点阻塞,如果可以则将其阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&  // 判断node节点能否被唤醒
                // 真正对node节点进行阻塞的方法,阻塞之后这个线程就会停在这个方法内
                parkAndCheckInterrupt())
                // 如果parkAndCheckInterrupt()执行了阻塞并且返回当前线程的中断信号是true,则将这个方法中设置的中断信号变量设置为true,用来向上一层返回当前线程的中断标志
                interrupted = true;
        }
    } finally {
        /**
         * failed标识当前线程的阻塞有没有失败
         * acquireQueued()最初始把failed设置为true,只有进入到成功获取到锁的分支中,才会将failed设置为false。
         * 所以除了成功获取锁return导致try{}执行结束以外,其他任何情况导致try{}执行结束都会使failed仍然等于true,也就是没有成功将线程阻塞过
         * 
         * waitStatus = CANCELLED =  1,表示线程已被取消(等待超时或者被中断),需要废弃结束。
         * 说的就是这种情况,当一个线程在acquireQueued()方法中因为被中断(响应中断的方法lockInterruptibly()在识别到终端信号之后会直接抛出异常就会将try{}代码块执行结束掉)或等待超时等原因而提前结束,就需要将该线程node节点生命状态设置为CANCELLED
         * 在未来该节点的后继节点进入到shouldParkAfterFailedAcquire()方法时,就会将该节点给移出队列
         */
        // 如果阻塞线程失败了
        if (failed)
            // 取消获取锁
            cancelAcquire(node);
    }
}
 
// AbstractQueuedSynchronizer.setHead()
/**
 * 将head指向node节点,并且将node节点中的thread线程属性,prev前驱结点指向都清空。将node节点构造成一个空节点
 */
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}
 
// AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire()
/**
 * 这个方法是在上面的for()循环里面调用的。该方法用来判断node节点能不能被阻塞,判断规则就是看node节点是不是可被唤醒,只有可被唤醒的节点才可以放心将其阻塞。
 * 通过获取node节点的前驱节点pred的waitStatus属性,来判断node节点可不可以被唤醒
 * 第一次调用会把前一个节点的等待状态设置为SIGNAL,并返回false
 * 第二次调用才会返回true
 * 
 * 在本章节学习的内容中,总共有两个方法(shouldParkAfterFailedAcquire和unparkSuccessor)会涉及到对Node节点的waitestate属性进行修改,下面我们来简述一下大致变化流程:
 * 假设当前持有锁的线程是T0,T1是等待队列中的第一个线程节点
 * T0线程调用release()方法释放锁时,release()会判断等待队列的head指针节点的waitestate是否不等于0,如果不等于0成立,就会在unparkSuccessor()方法中将head指针节点的waitstate属性设置为0
 * release()方法还会通过unparkSuccessor()将head.next,也就是等待队列第一个线程节点T1唤醒,然后这个线程节点就会去继续尝试获取锁
 * 但是在非公平锁的场景下,T1获取锁可能会再次失败,被外来的T2线程插队,T2线程抢占到锁。这个时候T1就会需要再次被阻塞,然后再重新走一遍之前走过的流程
 * 此时T1的waitestate = 0,所以在第一次进入到shouldParkAfterFailedAcquire()当法中会先将其设置为SIGNA(-1),然后返回到上层循环中再进行一轮循环,这个时候houldParkAfterFailedAcquire()才会返回true,然后执行后续的阻塞线程操作
 * 所以这个T1线程会经历两轮循环,经历了waitestate由 -1 -> 0 -> -1的过程
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {    
    /**
     * 注意node的waitStatus字段我们在上面创建Node的时候并没有指定
     * 也就是说使用的是默认值0
     * 这里把各种等待状态再贴出来
     * 
     * static final int CANCELLED =  1; //代表出现异常,中断引起的,需要废弃结束
     * static final int SIGNAL    = -1; //可被唤醒   指这个节点的下一个节点可被唤醒
     * static final int CONDITION = -2; // 条件等待,这个是在条件锁中使用的Node状态
     * static final int PROPAGATE = -3; // 传播,可以用来广播的状态
     */
    // 获得node节点的前驱节点的waitStatus属性,用来标识node节点是不是可以被唤醒
    int ws = pred.waitStatus;
    // 如果node节点的前驱节点waitStatus属性为SIGNAL,说明node节点可以被唤醒,也就是可以被阻塞,返回true
    if (ws == Node.SIGNAL)
        return true;
    // 如果node节点的前驱节点waitStatus大于0,说明前驱结点是出现异常的,也就是已取消状态,应该给移除掉(注意waitStatus = 1表示的是当前节点是异常状态,而不是后继节点是异常状态,所以当前节点node会被保留在队列中)
    if (ws > 0) {
        // 这里就进行一个循环,将node前面连续的,所有waitStatus > 0的结点都从队列中移除
        do {
            // 这个很好理解,就是一个简单的删除链表节点的过程
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0); // 遍历到节点waitStatus状态不再大于0时,停止删除操作
        // 更新node最新的前驱节点
        pred.next = node;
    // 如果是其他情况,就将前驱结点的waitStatus设置为SIGNAL,也就是将node节点设置为可唤醒状态。
    // 但是本次调用还是会返回false,在上一层调用shouldParkAfterFailedAcquire的位置是一个循环,所以返回上一层进行第二轮循环的时候,再次调用shouldParkAfterFailedAcquire,就会直接进入到第一个分支返回true了。
    } else {
        /**
         * 如果前一个节点的状态小于等于0,则把其状态设置为等待唤醒
         * 因为这一章节是以互斥锁为例进行讲解,所以这里可以简单地理解为把初始状态0设置为SIGNAL
         * CONDITION是条件锁的时候使用的
         * PROPAGATE是共享锁使用的
         */
        // 原子操作,将前驱节点的waitStatus设置为SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
 
// AbstractQueuedSynchronizer.parkAndCheckInterrupt()
// 将当前线程进行阻塞
private final boolean parkAndCheckInterrupt() {
    // 阻塞当前线程
    // 底层调用的是Unsafe的park()方法
    LockSupport.park(this);
    /**
     * 这个作用就是告知上一层这个线程是不是被中断信号唤醒的
     * 因为Park阻塞线程唤醒有两种方式:
     *   1、中断(interrupt)
     *   2、release()
     * 
     * 如果当前线程是中断信号唤醒的,我们需要将这个中断信号向上层传递,告知上一层这个线程被中断了,当这个线程成功获取到锁之后,需要去响应中断
     */
    /**
     * 返回当前线程的中断信号,并且将中断信号清除
     * 
     * 之前有一个疑问,这里向上层返回当前线程的中断信号,为什么不直接使用isInterrupted()来得到中断信号向上返回,而非得使用interrupted()来获取中断信号向上层返回
     * 当时觉得用interrupted()获取了中断信号,并且将其清除。将中断信号传递到外层之后,还需要再次使用selfInterrupt()来重新设置中断标志位,多此一举
     * 但实际这里使用interrupted()是非常正确的,因为如果一个线程的中断标志位为true,那么LockSupport.park()对该线程将会失去效果,也就是无法对该线程进行阻塞。
     * 当一个线程被唤醒以后,并不一定能成功获取锁,可能会被再次阻塞:
     *      1、当公平锁等待队列中的非头部线程节点被中断信号唤醒之后,它并不能成功获取到锁,所以会被再次阻塞
     *      2、当非公平锁等待队列中的线程节点(头部线程节点或非头部线程节点)被唤醒(release唤醒或中断信号唤醒)都不能保证其一定能获取到锁,都有可能再次被阻塞
     * 所以必须保证下次仍能成功将其阻塞才可以,这里必须先暂时将线程的中断信号清除
     */
    return Thread.interrupted();
}
 
// AbstractQueuedSynchronizer.cancelAcquire()
// 取消当前线程节点node获取锁,并且将当前节点的waitStatus设置为CANCELLED取消状态。但是此时并不会将该节点移出队列,在未来该节点的后继节点进入到shouldParkAfterFailedAcquire()方法时,才就会将该节点给移出队列
// acquireQueued()方法中如果线程没有成功获取锁,try{}代码块中的代码中执行结束,在finally{}中就会执行这个方法,将当前线程的node设置为取消状态
private void cancelAcquire(Node node) {
    if (node == null)
        return;
    // 将node节点的线程属性置为空
    node.thread = null;
    // 将当前节点前面所有连续的waitStatus > 0 的取消状态的节点都从队列中删除
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    // 删除取消状态的节点后,将node节点的前驱结点的next节点赋值给predNext
    Node predNext = pred.next;
    // 将当前节点设置为取消状态
    node.waitStatus = Node.CANCELLED;
    // 如果当前节点node是队列的尾部节点,则将node移除掉(使用原子操作,防止并发异常)
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
                (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            unparkSuccessor(node);
        }
        node.next = node; // help GC
    }
}
 
// AbstractQueuedSynchronizer.selfInterrupt()
// 在acquire()中调用该方法,只有当线程在队列中阻塞等待时,被中断信号唤醒,等到这个线程成功获取到锁之后,就会向上层传递中断信号,进而会调用该方法来响应中断
static void selfInterrupt() {
    // 设置当前线程的中断信号为true
    Thread.currentThread().interrupt();
}
主要方法的调用关系
ReentrantLock#lock() // 获取锁
->ReentrantLock.FairSync#lock() // 公平模式获取锁
    ->AbstractQueuedSynchronizer#acquire() // AQS的获取锁方法
        ->ReentrantLock.FairSync#tryAcquire() // 尝试获取锁
        ->AbstractQueuedSynchronizer#addWaiter()  // 尝试获取锁失败,将线程添加到等待队列
            ->AbstractQueuedSynchronizer#enq()  // 如果上一个方法入队没有成功,则在这个方法中自旋,不断尝试入队
        ->AbstractQueuedSynchronizer#acquireQueued() // 这个方法就是来将入队的线程阻塞,并且里面有个for()循环,线程被唤醒后会在该方法中自旋继续尝试获取锁
            ->AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire() // 检查当前节点是否可以被唤醒,进而决定能否对其进行阻塞
            ->AbstractQueuedSynchronizer#parkAndCheckInterrupt()  // 真正线程阻塞的方法

非公平锁

这里我们假设ReentrantLock的实例是通过以下方式获得的(ReentrantLock默认是非公平锁):

ReentrantLock reentrantLock = new ReentrantLock();
 
// ReentrantLock默认是非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}

下面的是加锁的主要逻辑:

// ReentrantLock.lock()
public void lock() {
    sync.lock();
}
 
// ReentrantLock.NonfairSync.lock()
// 这个方法在公平锁模式下是直接调用的acquire(1);
final void lock() {
    // 和公平锁相比,这里会先直接尝试CAS更新状态变量,如果此时锁没有被线程占用,就能成功将状态变量由0变为1,即成功获取到锁
    if (compareAndSetState(0, 1))
        // 如果更新成功,说明获取到锁,把当前线程设为该锁的独占线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 如果没有成功获取到锁,则执行acquire(1)方法,
        acquire(1);
}
 
// AbstractQueuedSynchronizer.acquire()
// 这里面的操作和公平锁都是一样的
public final void acquire(int arg) {
    // 调用尝试获取锁的方法,如果尝试获取锁失败,则将该线程添加到等待队列中
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
 
// ReentrantLock.NonfairSync.tryAcquire()
// 这里和公平锁不太一样,这里调用的是非公平锁的acquires方法:nonfairTryAcquire()
protected final boolean tryAcquire(int acquires) {
    // 调用父类的方法
    return nonfairTryAcquire(acquires);
}
 
// ReentrantLock.Sync.nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取当前state值
    int c = getState();
    if (c == 0) {
        // 如果状态变量的值为0,再次尝试CAS更新状态变量的值
        // 相对于公平锁模式少了!hasQueuedPredecessors()条件,非公平锁不需要判断是是否有线程在等待,而是直接去尝试获取锁
        if (compareAndSetState(0, acquires)) {
            // 将当前线程设置为此锁的占有者
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果当前线程就是正在持有此资源的线程,则可直接获取,也就是实现了可重入锁
    else if (current == getExclusiveOwnerThread()) {
        // 重入锁,累加state值
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 设置state的值
        setState(nextc);
        return true;
    }
    // 没有成功获取锁,则返回false
    return false;
}

除了上述代码以外,其他代码流程非公平锁和公平锁都是一样的。

公平锁和非公平锁总结

公平锁和非公平锁只有两处不同:

  1. 非公平锁在调用 lock() 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。但是公平锁会先判断等待列中是否有处于等待状态的线程,如果有的话,就乖乖加入到等待线程中去排队,而不能直接插队获取锁。
  2. 非公平锁在调用 lock() 中的第一次CAS 失败后,和公平锁一样都会进入到acquire()方法,然后就会进入到tryAcquire()方法,在 tryAcquire 方法中,非公平锁调用的是自己实现好的nonfairTryAcquire()非公平tryAcquire方法,如果发现锁这个时候被释放了(state == 0),非公平锁就会直接 CAS 抢锁,不会管当前等待队列中有没有等待线程。但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

公平锁和非公平锁就这两点区别,相对于公平锁,非公平锁在一开始就多了两次直接尝试获取锁的过程。如果非公平锁这两次 CAS获取锁 都不成功,那么后面非公平锁和公平锁的流程就是一样的,都要进入到阻塞队列等待唤醒。相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态

为什么ReentrantLock默认采用的是非公平模式?

因为非公平模式效率比较高。之所以非公平效率高,是因为非公平模式会在一开始就尝试两次获取锁,如果当时正好state的值为0,它就会成功获取到锁,少了排队导致的阻塞/唤醒过程,并且减少了线程频繁的切换带来的性能损耗。

但是非公平模式也有一些弊端,比如非公平模式有可能会导致一开始排队的线程一直获取不到锁,导致线程饿死。

lockInterruptibly()方法

  • ReentrantLock.lockInterruptibly():允许在尝试获取锁时由其它线程调用尝试获取锁的线程的Thread.interrupt方法来中断线程而直接返回,这时不用获取到锁,而会直接抛出一个InterruptedException。
  • ReentrantLock.lock():方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,获取失败则阻塞等待。只是在最后获取锁成功后再把当前线程置为interrupted状态。

总的来说,lockInterruptibly()支持线程中断,它与lock()方法的主要区别在于lockInterruptibly()获取锁的时候如果线程中断了,会抛出一个异常,而lock()不会管线程是否中断都会一直尝试获取锁,获取锁之后把自己标记为已中断,继续执行自己的逻辑,后面也会正常释放锁。我们就记住:

使用lockInterruptibly时:当前线程可以被其他线程直接中止结束,并且在其他线程中抛出异常信息;(优先考虑响应中断)
使用lock时:当前线程也可以响应其他线程的中断命令,但不会抛出异常信息,不会直接中止,而是会在成功获取到锁时将自己的中断标志设置true;(优先考虑获取锁)

具体实现以及与lock()方法的对比如下:

lockInterruptibly()

// ReentrantLock.lockInterruptibly()
// 相当于ReentrantLock.lock()
public void lockInterruptibly() throws InterruptedException {
    // 这里调用的sync的acquireInterruptibly()方法,该方法是sync继承自AbstractQueuedSynchronizer的方法,由抽象类AbstractQueuedSynchronizer实现
    sync.acquireInterruptibly(1);
}
 
// AbstractQueuedSynchronizer.acquireInterruptibly()
// 相当于acquire(int arg)方法
public final void acquireInterruptibly(int arg) throws InterruptedException {
    // 在第一次调用acquireInterruptibly时,如果发现当前线程已经被中断了,则直接抛出中断异常,响应中断
    if (Thread.interrupted())
        throw new InterruptedException();
    
    // 尝试获取锁
    // 这里调用的sync的tryAcquire()方法,它的具体实现是根据当前ReentrantLock是公平锁还是非公平锁来决定的
    // 如果当前sync是公平锁tryAcquire()方法就是使用的FairSync实现类中的tryAcquire()实现方法,如果当前是非公平锁,则使用的是NonfairSync实现类中的tryAcquire()方法,也就是nonfairTryAcquire()方法
    if (!tryAcquire(arg))
        // 尝试获取所失败之后
        doAcquireInterruptibly(arg);
}

doAcquireInterruptibly()方法与acquireQueue()差别在于方法的返回途径有两种,一种是for循环结束,正常获取到锁;另一种是线程被唤醒后检测到中断请求,则立即抛出中断异常,该操作导致方法结束。

// AbstractQueuedSynchronizer.doAcquireInterruptibly()
// 相当于acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),将获取锁失败的线程添加到等待队列并阻塞
private void doAcquireInterruptibly(int arg) throws InterruptedException {
    // 为当前线程创建Node线程节点,并将其入队
    final Node node = addWaiter(Node.EXCLUSIVE);
    // 入队之后,开始对线程进行阻塞
    // 阻塞线程是否失败
    boolean failed = true;
    try {
        // 自旋
        for (;;) {
            // 获取当前节点的前一个节点
            final Node p = node.predecessor();
            // 如果当前节点是等待队列中的第一个线程节点,则调用tryAcquire()尝试获取锁
            if (p == head && tryAcquire(arg)) {
                // 尝试获取锁成功,下面就需要将当前节点的node出队
                // 将head指针节点指向node,并且将node节点的前驱节点和代表的线程属性都设置为null
                setHead(node);
                p.next = null; // help GC
                // 将阻塞标志设置为false,说明没有阻塞节点线程
                failed = false;
                // 1.这里没有中断标识,也就没有返回中断标志,而是直接返回结束方法
                // lock和lockInterruptibly区别就是对中断的处理方式 
                return;
            }
            // shouldParkAfterFailedAcquire:判断线程可否安全阻塞
            // parkAndCheckInterrupt:挂起线程并返回当时中断标识Thread.interrupted()
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 2.当parkAndCheckInterrupt返回中断标识为true时立即抛出异常中断线程,而不是和lock()一样记录中断标志,等到获取锁成功之后再响应中断
                throw new InterruptedException();
        }
 
    // finally的执行一定会在throw new InterruptedException();抛出异常之前执行。finally代码块中的代码不管什么情况下,一定会被执行的
    } finally {
 
        /**
         * failed标识当前线程的阻塞有没有失败
         * doAcquireInterruptibly()最初始把failed设置为true,只有进入到成功获取到锁的分支中,才会将failed设置为false。
         * 所以除了成功获取锁return导致try{}执行结束以外,其他任何情况导致try{}执行结束都会使failed仍然等于true,也就是没有成功将线程阻塞过
         * 
         * waitStatus = CANCELLED =  1,表示线程已被取消(等待超时或者被中断),需要废弃结束。
         * 说的就是这种情况,当一个线程在doAcquireInterruptibly()方法中因为被中断(响应中断的方法lockInterruptibly()在识别到终端信号之后会直接抛出异常就会将try{}代码块执行结束掉)或等待超时等原因而提前结束,就需要将该线程node节点生命状态设置为CANCELLED
         * 在未来该节点的后继节点进入到shouldParkAfterFailedAcquire()方法时,就会将该节点给移出队列
         */
        // 如果阻塞线程失败了
        if (failed)
            // 取消获取锁,标注当前节点的生命状态为canceld信号量,这种状态的节点应该剔除
            cancelAcquire(node);
    }
}

除以上代码,其他的方法源码都和lock()一致。

lock()

lock()会先尝试获取锁,失败后再将线程入队并阻塞等待,会在获取锁成功之后调用selfInterrupt()来将线程的中断标志设置true来响应中断:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 获取锁成功后,中断当前线程。默认处理中断方式
        selfInterrupt();
}

addWaiter封装Node节点插入到队列尾部,acquireQueued负责队列的挂起、出队、是否中断。acquireQueued方法只有一种返回途径,就是通过for循正常返回时,必定是成功获取到了锁。

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;
                // 1.在独占锁后,才返回中断标识
                return interrupted;
            }
            //shouldParkAfterFailedAcquire:判断线程可否安全挂起
            //parkAndCheckInterrupt:挂起线程并返回当时中断标识Thread.interrupted()
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 2.当parkAndCheckInterrupt返回中断标识为true时修改interrupted
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

总结

ReentrantLock的lockInterruptibly(中断)和lock(非中断加锁模式)的区别在于:线程尝试获取锁操作失败后,在等待过程中,如果该线程被其他线程中断了,它是如何响应中断请求的。lock方法会忽略中断请求,继续获取锁直到成功;而lockInterruptibly则直接抛出中断异常来立即响应中断,由上层调用者处理中断。

那么,为什么要分为这两种模式呢?这两种加锁方式分别适用于什么场合呢?根据它们的实现语义来理解,lock()适用于锁获取操作不受中断影响的情况,此时可以忽略中断请求正常执行加锁操作,因为该操作仅仅记录了中断状态(通过Thread.currentThread().interrupt()操作,只是恢复了中断状态为true,并没有对中断进行响应)。如果要求被中断线程不能参与锁的竞争操作,则此时应该使用lockInterruptibly方法,一旦检测到中断请求,立即返回不再参与锁的竞争并且取消锁获取操作(即finally中的cancelAcquire操作)

tryLock()方法

尝试获取一次锁,成功了就返回true,没成功就返回false,不会继续尝试。

// ReentrantLock.tryLock()
public boolean tryLock() {
    // 直接调用Sync的nonfairTryAcquire()方法
    return sync.nonfairTryAcquire(1);
}
 
// ReentrantLock.Sync.nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 如果判断此时锁没有被占用
    if (c == 0) {
        // 通过CAS尝试获取锁,如果尝试失败则直接返回false
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果此时锁已经被线程占用了,但是占用着就是当前线程,则直接进入累加同步状态变量。实现ReentrantLock的可重入特性
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 如果获取锁失败,直接返回false,不进入等待队列阻塞等待
    return false;
}

tryLock()方法比较简单,直接以非公平的模式去尝试获取一次锁,获取到了或者锁本来就是当前线程占有着就返回true,否则返回false。

tryLock(long time, TimeUnit unit)方法

尝试获取锁,并等待一段时间,如果在这段时间内都没有获取到锁,就返回false。该方法也优先响应中断,检测到中断信号就直接抛出异常结束方法。

// ReentrantLock.tryLock()
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    // 调用AQS中的方法
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
 
// AbstractQueuedSynchronizer.tryAcquireNanos()
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
    // 如果线程中断了,抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 先尝试获取一次锁
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}
 
// AbstractQueuedSynchronizer.doAcquireNanos()
// 相当于acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),将获取锁失败的线程添加到等待队列并阻塞。并且该方法也优先响应中断,如果检测到中断信号就会直接抛出异常结束方法
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
    // 如果时间已经到期了,直接返回false
    if (nanosTimeout <= 0L)
        return false;
    // 到期时间
    final long deadline = System.nanoTime() + nanosTimeout;
    // AbstractQueuedSynchronizer.addWaiter()  
    // 为当前线程创建一个Node,添加到阻塞等待队列中,队列就是一个双向链表。在这个方法中的线程还是在运行当中的
    // 注意addWaiter()这里传入的节点模式为独占模式。addWaiter()入参是用来标识当前锁是共享锁还是互斥锁的标志,ReentrantLock是一个互斥锁,所以这里传入的是Node.EXCLUSIVE互斥标志
    final Node node = addWaiter(Node.EXCLUSIVE);
    // 将阻塞标志设置为true,如果中途因为异常结束方法,还没来得及成功获取资源将failed设置为false,则在finally代码块中执行取消获取锁的方法
    boolean failed = true;
    try {
        for (;;) {
            // 获取当前节点的前一个节点
            final Node p = node.predecessor();
            // 判断当前节点的前一个节点是否为head指针节点,如果是,说明当前节点是队列中的第一个线程节点,来让这个线程再去尝试抢一次资源。
            // tryAcquire(arg) 抢占线程的实现方式取决于当前sync是公平锁还是非公平锁,这里就是使用的sync.tryAcquire()。
            if (p == head && tryAcquire(arg)) {
                // 抢占资源成功
                setHead(node);
                p.next = null; // help GC
                // 未阻塞线程
                failed = false;
                // 返回true
                return true;
            }
            // 计算当前离阻塞终止时间还有多久
            nanosTimeout = deadline - System.nanoTime();
            // 如果离阻塞终止时间小于等于0,说明已经到期了,就直接返回false,结束阻塞等待
            if (nanosTimeout <= 0L)
                return false;
            // spinForTimeoutThreshold = 1000L;
            // 只有到期时间大于1000纳秒,才阻塞
            // 小于等于1000纳秒,直接自旋解决就得了
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                // 阻塞一段时间
                // tryLock(long time, TimeUnit unit)方法在阻塞的时候会使用LockSupport.parkNanos()方法加上阻塞时间,并且会随时检查是否到期,只要到期了没获取到锁就返回false。
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        //  failed标识当前线程的阻塞有没有失败
        if (failed)
            // 如果阻塞线程失败了,则取消获取锁
            cancelAcquire(node);
    }
}

unlock()方法

下面,我们再来看释放锁的unlock()方法源码。

公平锁和非公平锁在释放锁时是没有区别的,都是通过调用的AQS提供的模板方法实现的。

// java.util.concurrent.locks.ReentrantLock.unlock()
public void unlock() {
    // 这里调用AQS释放锁的模板方法
    sync.release(1);
}
 
// java.util.concurrent.locks.AbstractQueuedSynchronizer.release()  AQS提供的模板方法
//  Park阻塞线程唤醒有两种方式:
// 1、中断
// 2、release()
public final boolean release(int arg) {
    // 调用AQS实现类的tryRelease()方法释放锁
    if (tryRelease(arg)) {
        // 成功释放资源
        Node h = head;
        // 如果头节点不为空,且等待状态不是0,就唤醒下一个节点
        // 还记得waitStatus吗?
        // 在每个节点阻塞之前会把其上一个节点的等待状态设为SIGNAL(-1)
        // 所以,SIGNAL的准确理解应该是唤醒下一个等待的线程
        if (h != null && h.waitStatus != 0)
            /**
             * 将head的next节点的线程唤醒,并且将head节点的waitStatus重新恢复成0
             * (这里之前我觉得保持原样-1更好,这样就能避免如果唤醒的线程再一次抢占资源的时候失败(这种情况在非公平锁可能发生,但是在公平锁不可能发生),
             * 然后重新去对其进行阻塞的时候,判断它的是否可换性状态的时候,就可以直接判断是-1,然后进行阻塞了,不需要再重新将0设置为-1,
             * 然后再进行第二轮循环,去设置对其进行阻塞。但是上这里将waitStatus还是保留-1都可以,源码这样写应该只是为了让抢锁的线程多一次抢占机会而已,没有别的作用)
             */
            unparkSuccessor(h);
        return true;
    }
    return false;
}
 
// java.util.concurrent.locks.ReentrantLock.Sync.tryRelease()   Sync实现类覆写的tryRelease()
protected final boolean tryRelease(int releases) {
    // 计算释放锁之后该占有该资源的线程数
    int c = getState() - releases;
    // 如果当前线程不是占有着锁的线程,抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 标识是否成功释放资源    
    boolean free = false;
    // 如果状态变量的值为0了,说明完全释放了锁,则将占有该资源的线程变量置为空,这样其他的线程就可以来抢占该资源了
    // 这也就是为什么重入锁调用了多少次lock()就要调用多少次unlock()的原因
    // 如果不这样做,会导致锁不会完全释放,别的线程永远无法获取到锁
    if (c == 0) {
        // 置为true
        free = true;
        // 清空占有线程
        setExclusiveOwnerThread(null);
    }
    // 设置状态变量的值
    setState(c);
    // 返回是否完全释放了资源
    return free;
}
 
// java.util.concurrent.locks.AbstractQueuedSynchronizer.unparkSuccessor()
// 这里就是将线程节点唤醒,接触阻塞状态, 传入的node就是队列的head指针节点
private void unparkSuccessor(Node 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.
    * 如果状态是负的,尝试去清除这个信号,就把它设置为0。当然,如果清除失败或者说被其他
    * 等待获取锁的线程修改了,也没关系。
    * 这里为什么要去把状态修改为0呢?其实这个线程是要被唤醒的,修不修改都无所谓。
    * 回忆一下上面的acquireQueued方法中调用了shouldParkAfterFailedAcquire
    * 去把前一个节点状态改为-1,而在改之前会抢占一次锁,所以说这里的操作
    * 其实并没有太大用处,可能可以为争抢锁的线程再多一次抢锁机会,故而成功失败均不影响
    */
    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.
    * 唤醒后继节点,通常是next节点,但是如果next节点被取消了或者为空,那么
    * 就需要从尾部开始遍历,将无效节点先剔除
    */
    // 头节点的下一个节点
    Node s = node.next;
    // 如果下一个节点为空,或者其等待状态大于0(实际为已取消)
    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);
}

释放锁的过程大致为:

  • 将state的值减1;
  • 如果state减到了0,说明已经完全释放锁了,唤醒下一个等待着的节点;

条件锁

ReentrantLock是通过继承AQS的内部类来实现条件锁的。通过创建一个条件锁的实体类ConditionObject,然后通过这个类来实现各种条件锁的功能。

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;
    // 创建条件锁
    public Condition newCondition() {
        return sync.newCondition();
    }
    /**
     * 抽象内部类
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        // 条件锁
        final ConditionObject newCondition() {
            return new ConditionObject();
        }
    }
}

ReentrantLock总结图

总体流程图

同步队列图

调用层级图

性质分析

实现可重入

可重入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,该特性的首先需要解决以下两个问题:

线程再次获取锁:所需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次获取成功;
锁的最终释放:线程重复n次获取了锁,随后在第n次释放该锁后,其它线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前线程被重复获取的次数,而被释放时,计数自减,当计数为0时表示锁已经成功释放。
ReentrantLock是通过自定义同步器来实现锁的获取与释放,我们以非公平锁(默认)实现为例,对锁的获取和释放进行详解。

公平锁与非公平锁

公平性与否是针对锁获取顺序而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合FIFO原则。我们在前面介绍了非公平锁NonfairSync调用的nonfairTryAcquire(int)方法,在该方法中,只要通过CAS操作修改同步状态成功,则当前线程就获取到了锁,而公平锁则不同,

与nonfairTryAcquire(int)方法比较,唯一不同的位置为判断条件多了hasQueuedPredecessors()方法,

该方法主要是对同步队列中当前节点是否有前驱节点进行判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

显式锁和隐式锁的区别(synchronized和lock的区别)

在面试的过程中有可能会问到:在Java并发编程中,锁有两种实现:使用隐式锁和使用显示锁分别是什么?两者的区别是什么?

所谓的显式锁和隐式锁的区别也就是说说Synchronized(下文简称:sync)和lock(下文就用ReentrantLock来代之lock)的区别。

Synchronized是JVM级别实现的锁和ReentantLock实现是完全不同的。

本节的主要内容:将通过七个方面详细介绍sync和lock的区别。通过生活case中的X二代和普通人比较大家更容易理解这两者之间的区别

Java中隐式锁:synchronized;显式锁:lock

出身不同

从sync和lock的出身(原始的构成)来看看两者的不同。

Sync:Java中的关键字,是由JVM来维护的。是JVM层面的锁。
Lock:是JDK5以后才出现的具体的类。使用lock是调用对应的API。是API层面的锁。

sync是底层是通过monitorenter进行加锁(底层是通过monitor对象来完成的,其中的wait/notify等方法也是依赖于monitor对象的。只有在同步块或者是同步方法中才可以调用wait/notify等方法的。因为只有在同步块或者是同步方法中,JVM才会调用monitory对象的);通过monitorexit来退出锁的。

而lock是通过调用对应的API方法来获取锁和释放锁的。

使用方式不同

Sync是隐式锁,Lock是显示锁。所谓的显示和隐式就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁的操作。

我们大家都知道,在使用sync关键字的时候,我们使用者根本不用写其他的代码,然后程序就能够获取锁和释放锁了。那是因为当sync代码块执行完成之后,系统会自动的让程序释放占用的锁。Sync是由系统维护的,如果非逻辑问题的话话,是不会出现死锁的。

在使用lock的时候,我们使用者需要手动的获取和释放锁。如果没有释放锁,就有可能导致出现死锁的现象。手动获取锁方法:lock.lock()。释放锁:unlock方法。需要配合tyr/finaly语句块来完成。

用生活中的一个case来形容这个不同:官二代和普通人的你在进入机关大院的时候待遇。官二代不需要出示什么证件就可以进入,但是你需要手动出示证件才可以进入。

等待是否可中断

Sync是不可中断的。除非抛出异常或者正常运行完成。

Lock可以中断的。中断方式:

1:调用设置超时方法tryLock(long timeout ,timeUnit unit)
2:调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断

生活中小case来理解这一区别:官二代一般不会做饭。都会去餐厅点餐等待着餐厅出餐。普通人的你既可以去餐厅等待,如果等待时间长的话,你就可以回去自己做饭了。

加锁的时候是否可以公平

Sync:非公平锁

lock:两者都可以的。默认是非公平锁。在其构造方法的时候可以传入Boolean值,true:公平锁,false:非公平锁

生活中小case来理解这个区别:官二代一般都不排队,喜欢插队的。普通人的你虽然也喜欢插队。但是如果遇到让排队的情况下,你还是会排队的。

锁绑定多个条件来condition

Sync:没有。要么随机唤醒一个线程;要么是唤醒所有等待的线程。
Lock: 用来实现分组唤醒需要唤醒的线程,可以精确的唤醒,而不是像sync那样,不能精确唤醒线程。

从性能比较

synchronized是托管给JVM执行的,而lock是Java写的控制锁的代码。

在Java8版本中,在一开始线程没有很多的情况下,ReentrantLock效率比synchronized略高,但是仍在一个数量级上,随着线程增加到一定的数量,synchronized的效率比ReentrantLock非公平锁高了。

生活小case理解:在我们一般的认知中,官二代一般都是比较坑爹的吧。但是这几年也有很多官二代或者是富二代改变了态度,端正自己态度,靠自己能力而不是拼爹了。

从使用锁的方式比较

synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swa )。我们已经详细讲解了ReentrantLock的源代码,也发现了其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。

现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而compareAndSet()就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

Synchronized 和 ReenTrantLock 的对比总结

两者都是可重入锁

两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,是Java原生关键字锁。前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。

ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),是Java语言层面提供的锁。所以我们可以通过查看它的源代码,来看它是如何实现的。

ReenTrantLock 比 synchronized 增加了一些高级功能

相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。

性能已不是选择标准

在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作。

既然ReentrantLock的功能更丰富,而且效率也不低,我们是不是可以放弃使用synchronized了呢?

并不是的。因为synchronized是Java原生支持的,随着Java版本的不断升级,Java团队也是在不断优化synchronized,所以我认为在功能相同的前提下,最好还是使用原生的synchronized关键字来加锁,这样我们就能获得Java版本升级带来的免费的性能提升的空间。

另外,在Java8的ConcurrentHashMap中已经把ReentrantLock换成了synchronized来分段加锁了,这也是Java版本不断升级带来的免费的synchronized的性能提升。

功能 ReentrantLock synchronized
可重入 支持 支持
非公平 支持(默认) 支持
加锁/解锁方式 需要手动加锁、解锁,一般使用try..finally..保证锁能够释放 手动加锁,无需刻意解锁
按key锁 不支持,比如按用户id加锁 支持,synchronized加锁时需要传入一个对象
公平锁 支持,new ReentrantLock(true) 不支持
中断 支持,lockInterruptibly() 不支持
尝试加锁 支持,tryLock() 不支持
超时锁 支持,tryLock(timeout, unit) 不支持
获取当前线程获取锁的次数 支持,getHoldCount() 不支持
获取等待的线程 支持,getWaitingThreads() 不支持
检测是否被当前线程占有 支持,isHeldByCurrentThread() 不支持
检测是否被任意线程占有 支持,isLocked() 不支持
条件锁 可支持多个条件,condition.await(),condition.signal(),condition.signalAll() 只支持一个,obj.wait(),obj.notify(),obj.notifyAll()