My logo
Published on

JUC显式锁的原理与实战-04

4.1显式锁Lock接口

Lock接口位于java.util.concurrent.locks包中,是JUC显式锁的一个抽象,Lock接口的主要抽象方法如下表

06-thread-22

从Lock提供的接口方法可以看出,显式锁至少比Java内置锁多了以下优势:
(1)可中断获取锁
使用synchronized关键字获取锁的时候,如果线程没有获取到被阻塞,阻塞期间该线程是不响应中断信号(interrupt)的;而调用Lock.lockInterruptibly()方法获取锁时,如果线程被中断,线程将抛出中断异常。
(2)可非阻塞获取锁
使用synchronized关键字获取锁时,如果没有成功获取,线程只有被阻塞;而调用Lock.tryLock()方法获取锁时,如果没有获取成功,线程也不会被阻塞,而是直接返回false。
(3)可限时抢锁
调用Lock.tryLock(long time,TimeUnit unit)方法,显式锁可以设置限定抢占锁的超时时间。而在使用synchronized关键字获取锁时,如果不能抢到锁,线程只能无限制阻塞。
除了以上能通过Lock接口直接观察出来的三点优势之外,显式锁还有不少其他的优势,稍后在介绍显式锁种类繁多的实现类时,大家就能感觉到。

4.1.1 可重入锁ReentrantLock

ReentrantLock是JUC包提供的显式锁的一个基础实现类,ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,但是拥有了限时抢占、可中断抢占等一些高级锁特性。此外,ReentrantLock基于内置的抽象队列同步器(Abstract Queued Synchronized,AQS)实现,在争用激烈的场景下,能表现出表内置锁更佳的性能。
ReentrantLock是一个可重入的独占(或互斥)锁,其中两个修饰词的含义为:
**(1)可重入的含义:**表示该锁能够支持一个线程对资源的重复加锁,也就是说,一个线程可以多次进入同一个锁所同步的临界区代码块。比如,同一线程在外层函数获得锁后,在内层函数能再次获取该锁,甚至多次抢占到同一把锁。
**(2)独占的含义:**在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能等待,只有拥有锁的线程释放了锁后,其他的线程才能够获取锁。

4.1.2 使用显式锁的模板代码

  1. 使用lock()方法抢锁的模板代码

通常情况下,大家会调用lock()方法进行阻塞式的锁抢占,其模板代码如下:

06-thread-22

以上抢锁模板代码有以下几个需要注意的要点:
(1)释放锁操作lock.unlock()必须在try-catch结构的finally块中执行,否则,如果临界区代码抛出异常,锁就有可能永远得不到释放。
(2)抢占锁操作lock.lock()必须在try语句块之外,而不是放在try语句块之内。为什么呢?原因之一是lock()方法没有申明抛出异常,所以可以不包含到try块中;原因之二是lock()方法并不一定能够抢占锁成功,如果没有抢占成功,当然也就不需要释放锁,而且在没有占有锁的情况下去释放锁,可能会导致运行时异常。
(3)在抢占锁操作lock.lock()和try语句之间不要插入任何代码,避免抛出异常而导致释放锁操作lock.unlock()执行不到,导致锁无法被释放。

**  2.调用tryLock()方法非阻塞抢锁的模板代码**
lock()是阻塞式抢占,在没有抢到锁的情况下,当前线程会阻塞。如果不希望线程阻塞,可以调用tryLock()方法抢占锁。tryLock()是非阻塞抢占,在没有抢到锁的情况下,当前线程会立即返回,不会被阻塞。 调用tryLock()方法时,线程拿不到锁就立即返回,这种处理方式在实际开发中使用不多,但是其重载版本tryLock(long time,TimeUnit unit)方法在限时阻塞抢锁的场景中非常有用。

** 3.调用tryLock(long time,TimeUnit unit)方法抢锁的模板代码**
tryLock(long time,TimeUnit unit)方法用于限时抢锁,该方法在抢锁时会进行一段时间的阻塞等待,其中的time参数代表最大的阻塞时长,unit参数为时长的单位(如秒)。

06-thread-22

4.1.3 基于显式锁进行“等待-通知”方式的线程间通信

与Object对象的wait、notify两类方法相类似,基于Lock显式锁,JUC也为大家提供了一个用于线程间进行“等待-通知”方式通信的接口——java.util.concurrent.locks.Condition
Condition的“等待-通知”方法和Object的“等待-通知”方法的语义等效关系为:
·Condition类的await方法和Object类的wait方法等效。
·Condition类的signal方法和Object类的notify方法等效。
·Condition类的signalAll方法和Object类的notifyAll方法等效。
Condition对象是基于显式锁的,所以不能独立创建一个Condition对象,而是需要借助于显式锁实例去获取其绑定的Condition对象。不过,每一个Lock显式锁实例都可以有任意数量的Condition对象。具体来说,可以通过lock.newCondition()方法去获取一个与当前显式锁绑定的Condition实例,然后通过该Condition实例进行“等待-通知”方式的线程间通信。
由于Lock有公平锁和非公平锁之分,而Condition是与Lock绑定的,因此就有与Lock一样的公平特性:如果是公平锁,等待线程按照FIFO(先进先出)顺序从Condition对象的等待队列中唤醒;如果是非公平锁,后续的唤醒次序就不保证FIFO顺序了。

4.1.4 LockSupport

LockSupport是JUC提供的一个线程阻塞与唤醒的工具类,该工具类可以让线程在任意位置阻塞和唤醒,其所有的方法都是静态方法。
LockSupport的常用方法:

06-thread-22

LockSupport.park()和Thread.sleep()的区别:
从功能上说,LockSupport.park()与Thread.sleep()方法类似,都是让线程阻塞,二者的区别如下:
(1)Thread.sleep()没法从外部唤醒,只能自己醒过来;而被LockSupport.park()方法阻塞的线程可以通过调用LockSupport.unpark()方法去唤醒。
(2)Thread.sleep()方法声明了InterruptedException中断异常,这是一个受检异常,调用者需要捕获这个异常或者再抛出;而调用LockSupport.park()方法时不需要捕获中断异常。
(3)被LockSupport.park()方法、 Thread.sleep()方法所阻塞的线程有一个特点,当被阻塞线程的Thread.interrupt()方法被调用时,被阻塞线程的中断标志将被设置,该线程将被唤醒。不同的是,二者对中断信号的响应方式不同:LockSupport.park()方法不会抛出InterruptedException异常,仅仅设置了线程的中断标志;而Thread.sleep()方法会抛出InterruptedException异常。 (4)与Thread.sleep()相比,调用LockSupport.park()能更精准、更加灵活地阻塞、唤醒指定线程。
(5)Thread.sleep()本身就是一个Native方法;LockSupport.park()并不是一个Native方法,只是调用了一个Unsafe类的Native方法(名字也叫park)去实现。
(6)LockSupport.park()方法还允许设置一个Blocker对象,主要用来供监视工具或诊断工具确定线程受阻塞的原因。
LockSupport.park()与Object.wait()的区别:
从功能上说,LockSupport.park()与Object.wait()方法也类似,都是让线程阻塞,二者的区别如下:
(1)Object.wait()方法需要在synchronized块中执行,而LockSupport.park()可以在任意地方执行。
(2)当被阻塞线程被中断时,Object.wait()方法抛出了中断异常,调用者需要捕获或者再抛出;当被阻塞线程被中断时,LockSupport.park()不会抛出异常,调用时不需要处理中断异常。
(3)如果线程在没有被Object.wait()阻塞之前被Object.notify()唤醒,也就是说在Object.wait()执行之前去执行Object.notify(),就会抛出IllegalMonitorStateException异常,是不被允许的;而线程在没有被LockSupport.park()阻塞之前被LockSupport.unPark()唤醒,也就是说在LockSupport.park()执行之前去执行LockSupport.unPark(),不会抛出任何异常,是被允许的。

public class LockSupportDemo {

    public static class ChangeObjectThread extends Thread {
        public ChangeObjectThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            Print.tco("即将进入无限时阻塞");
            // 阻塞当前线程
            LockSupport.park();
            if (Thread.currentThread().isInterrupted()) {
                Print.tco("被中断了,但任然会继续执行");
            } else {
                Print.tco("被重新唤醒了");
            }
        }
    }

    @Test
    public void testLockSupport() {
        ChangeObjectThread t1 = new ChangeObjectThread("线程一");
        ChangeObjectThread t2 = new ChangeObjectThread("线程二");
        //启动线程一
        t1.start();
        sleepSeconds(1);
        //启动线程二
        t2.start();
        sleepSeconds(1);
        //中断线程一
        t1.interrupt();
        // 唤醒线程二
        LockSupport.unpark(t2);
    }

    @Test
    public void testLockSupport2() {

        Thread t1 = new Thread(() ->
        {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Print.tco("即将进入无限时阻塞");
            //阻塞当前线程
            LockSupport.park();
            Print.tco("被重新唤醒了");

        }, "演示线程");
        t1.start();
        //唤醒一次没有使用 LockSupport.park()阻塞的线程
        LockSupport.unpark(t1);
        //再唤醒一次没有使用 LockSupport.park()阻塞的线程
        LockSupport.unpark(t1);
        sleepSeconds(2);
        //中断线程一
        //t1.interrupt();
        //第三唤醒使用 LockSupport.park()阻塞的线程
        LockSupport.unpark(t1);
    }
}

4.2 显式锁的分类

显式锁有很多种,从不同的角度来看,显式锁大概有以下几种分类:可重入锁和不可重入锁、悲观锁和乐观锁、公平锁和非公平锁、共享锁和独占锁、可中断锁和不可中断锁。

4.2.1 悲观锁和乐观锁

从线程进入临界区前是否锁住同步资源的角度来分,显式锁可以分为悲观锁和乐观锁。
悲观锁就是悲观思想,每次进入临界区操作数据的时候都认为别的线程会修改,所以线程每次在读写数据时都会上锁,锁住同步资源,这样其他线程需要读写这个数据时就会阻塞,一直等到拿到锁。总体来说,悲观锁适用于写多读少的场景,遇到高并发写时性能高。
Java的synchronized重量级锁是一种悲观锁。
乐观锁是一种乐观思想,每次去拿数据的时候都认为别的线程不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样就更新),如果失败就要重复读-比较-写的操作。总体来说,乐观锁适用于读多写少的场景,遇到高并发写时性能低。
Java中的乐观锁基本都是通过CAS自旋操作实现的。CAS是一种更新原子操作,比较当前值跟传入值是否一样,是则更新,不是则失败。在争用激烈的场景下,CAS自旋会出现大量的空自旋,会导致乐观锁性能大大降低。
Java的synchronized轻量级锁是一种乐观锁。另外,JUC中基于抽象队列同步器(AQS)实现的显式锁(如ReentrantLock)都是乐观锁。

4.2.2 公平锁与非公平锁

什么是非公平锁呢?非公平锁是指多个线程获取锁的顺序并不一定是其申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,抢锁成功的次序不一定体现为FIFO(先进先出)顺序。非公平锁的优点在于吞吐量比公平锁大,它的缺点是有可能会导致线程优先级反转或者线程饥饿现象。

4.2.3 可中断锁与不可中断锁

可中断锁是指抢占过程可以被中断的锁,JUC的显式锁(如ReentrantLock)是一个可中断锁。不可中断锁是指抢占过程不可以被中断的锁,如Java的synchronized内置锁就是一个不可中断锁。

4.2.4 共享锁与独占锁

在访问共享资源之前进行加锁操作,在访问完成之后进行解锁操作。按照“是否允许在同一时刻被多个线程持有”来区分,锁可以分为共享锁与独占锁。
独占锁也叫排他锁、互斥锁、独享锁,是指锁在同一时刻只能被一个线程所持有。一个线程加锁后,任何其他试图再次加锁的线程都会被阻塞,直到持有锁线程解锁。通俗来说,就是共享资源某一时刻只能有一个线程访问,其余线程阻塞等待。
如果是公平地独占锁,在持有锁线程解锁时,如果有一个以上的线程在阻塞等待,那么最先抢锁的线程被唤醒变为就绪状态去执行加锁操作,其他的线程仍然阻塞等待。
Java中的Synchronized内置锁和ReentrantLock显式锁都是独占锁。
共享锁Semaphore
共享锁就是在同一时刻允许多个线程持有的锁。当然**,获得共享锁的线程只能读取临界区的数据,不能修改临界区的数据。**
JUC中的共享锁包括Semaphore(信号量)、ReadLock(读写锁)中的读锁、CountDownLatch倒数闩。

4.2.5 共享锁CountDownLatch

CountDownLatch是一个常用的共享锁,其功能相当于一个多线程环境下的倒数门闩。CountDownLatch可以指定一个计数值,在并发环境下由线程进行减一操作,当计数值变为0之后,被await方法阻塞的线程将会唤醒。通过CountDownLatch可以实现线程间的计数同步。
下面是一个非常经典的CountDownLatch使用示例:司机(Driver)在开车之前,需要100个乘客并发进行不重复的报数,报数到100之后说明人已经到齐,随后司机可以开车出发,具体代码如下:

/**
 * CountDownLatch 共享锁的使用
 * 司机(Driver)在开车之前,需要100个乘客并发进行不重复的报数,报数到100之后说明人已经到齐,随后司机可以开车出发
 *
 * @author: wangyj
 * @create: 2022-04-20
 * @version: 1.0.0
 **/
public class Driver {

    private static final int N = 100; // 乘客数

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch doneSignal = new CountDownLatch(N);//取得CPU密集型线程池
        Executor e = ThreadUtil.getCpuIntenseTargetThreadPool();

        for (int i = 1; i <= N; ++i) // 启动报数任务
            e.execute(new Person(doneSignal, i));

        doneSignal.await(); //等待报数完成
        Print.tcfo("人数到期,开车");
    }

    static class Person implements Runnable {

        private final CountDownLatch doneSignal;
        private final int i;

        public Person(CountDownLatch doneSignal, int i) {
            this.doneSignal = doneSignal;
            this.i = i;
        }

        @Override
        public void run() {
            try {
                //报数
                Print.tcfo("第" + i + "个人以到");
                doneSignal.countDown();
            } catch (Exception ex) {
            } // return;
        }
    }
}

结合上述示例的运行结果,梳理一下CountDownLatch的使用步骤:
(1)创建倒数闩,初始化CountDownLatch时设置倒数的总次数,比如为100。
(2)等待线程调用倒数闩的await()方法阻塞自己,等待倒数闩的计数器数值为0(倒数线程全部执行结束)。
(3)倒数线程执行完,调用CountDownLatch.countDown()方法将计数器数值减一。

4.2.6 读写锁(ReentrantReadWriteLock)

在介绍完共享锁和独占锁的内容之后,接下来介绍建立在二者基础上的一种组合锁:读写锁。
读写锁的内部包含两把锁:一把是读(操作)锁,是一种共享锁;另一把是写(操作)锁,是一种独占锁。在没有写锁的时候,读锁可以被多个线程同时持有。写锁是具有排他性的:如果写锁被一个线程持有,其他的线程不能再持有写锁,抢占写锁会阻塞;进一步来说,如果写锁被一个线程持有,其他的线程不能再持有读锁,抢占读锁也会阻塞。
JUC包中的读写锁接口为ReadWriteLock

06-thread-22

读写锁ReentrantReadWriteLock
通过ReentrantReadWriteLock类能获取读锁和写锁,它的读锁是可以多线程共享的共享锁,而它的写锁是排他锁,在被占时不允许其他线程再抢占操作。然而其读锁和写锁之间是有关系的:同一时刻不允许读锁和写锁同时被抢占,二者之间是互斥的。
接着进行代码演示,读锁是共享锁,写锁是排他锁:

public class ReadWriteLockTest {

    //创建一个集合
    final static Map<String, String> MAP = new HashMap<String, String>();
    //创建一个读写锁
    final static ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
    //获取读锁
    final static Lock READ_LOCK = LOCK.readLock();
    //获取写锁
    final static Lock WRITE_LOCK = LOCK.writeLock();

    //写操作
    public static Object put(String key, String value) {
        WRITE_LOCK.lock();
        try {
            Print.tco(DateUtil.getNowTime() + " 抢占了WRITE_LOCK,开始执行write操作");
            Thread.sleep(1000);
            String put = MAP.put(key, value);
//            Print.tco("write操作执行完毕");
            return put;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            WRITE_LOCK.unlock();
        }
        return null;
    }

    //读操作
    public static Object get(String key) {
        READ_LOCK.lock();
        try {
            Print.tco(DateUtil.getNowTime() + " 抢占了READ_LOCK,开始执行read操作");
            Thread.sleep(1000);
            String value = MAP.get(key);
//            Print.tco("read操作执行完毕");
            return value;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            READ_LOCK.unlock();
        }
        return null;
    }

    public static void main(String[] args) {
        //创建Runnable可执行实例
        Runnable writeTarget = () -> put("key", "value");
        Runnable readTarget = () -> get("key");

        //创建4条读线程
        for (int i = 0; i < 4; i++) {
            new Thread(readTarget, "读线程" + i).start();
        }
        //创建2条写线程,并启动
        for (int i = 0; i < 2; i++) {
            new Thread(writeTarget, "写线程" + i).start();
        }
    }
}

4.2.7 锁的升级与降级

锁升级是指读锁升级为写锁,锁降级指的是写锁降级为读锁。在ReentrantReadWriteLock读写锁中,只支持写锁降级为读锁,而不支持读锁升级为写锁。具体的演示代码如下:

public class ReadWriteLockTest2 {

    //创建一个集合
    final static Map<String, String> MAP = new HashMap<String, String>();
    //创建一个读写锁
    final static ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
    //获取读锁
    final static Lock READ_LOCK = LOCK.readLock();
    //获取写锁
    final static Lock WRITE_LOCK = LOCK.writeLock();

    //写操作
    public static Object put(String key, String value) {
        WRITE_LOCK.lock();
        try {
            Print.tco(DateUtil.getNowTime() + " 抢占了WRITE_LOCK,开始执行write操作");
            Thread.sleep(1000);
            String put = MAP.put(key, value);
            Print.tco(Thread.currentThread().getName() + "尝试降级写锁为读锁");
            //写锁降级为读锁(成功)
            READ_LOCK.lock();
            Print.tco(Thread.currentThread().getName() + "写锁降级为读锁成功");
            return put;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            READ_LOCK.unlock();
            WRITE_LOCK.unlock();
        }
        return null;
    }

    //读操作
    public static Object get(String key) {
        READ_LOCK.lock();
        try {
            Print.tco(DateUtil.getNowTime() + " 抢占了READ_LOCK,开始执行read操作");
            Thread.sleep(1000);
            String value = MAP.get(key);
            Print.tco(Thread.currentThread().getName() + "尝试升级读锁为写锁");
            //读锁升级为写锁(失败)
            WRITE_LOCK.lock();
            Print.tco(Thread.currentThread().getName() + "读锁升级为写锁成功");

            return value;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            READ_LOCK.unlock();
        }
        return null;
    }

    public static void main(String[] args) {
        //创建Runnable可执行实例
        Runnable writeTarget = () -> put("key", "value");
        Runnable readTarget = () -> get("key");
        //创建1条写线程,并启动

        new Thread(writeTarget, "写线程").start();

        //创建1条读线程

        new Thread(readTarget, "读线程").start();
    }
}

通过结果可以看出:ReentrantReadWriteLock不支持读锁的升级,主要是避免死锁,例如两个线程A和B都占了读锁并且都需要升级成写锁,A升级要求B释放读锁,B升级要求A释放读锁,二者就会由于相互等待形成死锁。

总结起来,与ReentrantLock相比,ReentrantReadWriteLock更适合读多写少的场景,可以提高并发读的效率;而ReentrantLock更适合读写比例相差不大或写比读多的场景。

4.2.7 StampedLock(印戳锁)

StampedLock(印戳锁)是对ReentrantReadWriteLock读写锁的一种改进,主要的改进为:在没有写只有读的场景下,StampedLock支持不用加读锁而是直接进行读操作,最大程度提升读的效率,只有在发生过写操作之后,再加读锁才能进行读操作。
StampedLock的三种模式如下:
(1)悲观读锁:与ReadWriteLock的读锁类似,多个线程可以同时获取悲观读锁,悲观读锁是一个共享锁。
(2)乐观读锁:相当于直接操作数据,不加任何锁,连读锁都不要。
(3)写锁:与ReadWriteLock的写锁类似,写锁和悲观读锁是互斥的。虽然写锁与乐观读锁不会互斥,但是在数据被更新之后,之前通过乐观读锁获得的数据已经变成了脏数据。
StampedLock与ReentrantReadWriteLock对比
StampedLock与ReentrantReadWriteLock语义类似,不同的是,StampedLock并没有实现ReadWriteLock接口,而是定义了自己的锁操作API。

4.2.8 CLH自旋锁(简单学习)

简单的CLH锁可以基于单向链表实现,申请加锁的线程首先会通过CAS操作在单向链表的尾部增加一个节点,之后该线程只需要在其前驱节点上进行普通自旋,等待前驱节点释放锁即可。由于CLH锁只有在节点入队时进行一下CAS的操作,在节点加入队列之后,抢锁线程不需要进行CAS自旋,只需普通自旋即可。因此,在争用激烈的场景下,CLH锁能大大减少CAS操作的数量,以避免CPU的总线风暴。

public class CLHLock implements Lock {

    /**
     * 指向当前节点
     */
    private static ThreadLocal<Node> curNodeLocal = new ThreadLocal();
    private String name;
    /**
     * CLHLock队列的尾部
     * tail属性使用AtomicReference类型是为了使得多个线程并发操作tail时不会发生线程安全问题
     */
    private AtomicReference<Node> tail = new AtomicReference<>();

    public CLHLock() {
        // 设置尾部节点
        tail.getAndSet(Node.EMPTY);
    }

    public CLHLock(String name) {
        this.name = name;
        // 设置尾部节点
        tail.getAndSet(Node.EMPTY);
    }

    //加锁:将节点添加到等待队列的尾部
    @Override
    public void lock() {
        Node curNode = new Node(true, null);
        Node preNode = tail.get();
        //CAS自旋:将当前节点插入到队列的尾部
        while (!tail.compareAndSet(preNode, curNode)) {
            preNode = tail.get();
        }
        //设置前驱
        curNode.setPreNode(preNode);
        //监听前驱节点的locked变量,直到其值为false
        //若前继节点的locked状态为true,则表示前一线程还在抢占或者占有锁
        while (curNode.getPreNode().isLocked()) {
            //让出CPU时间片,提高性能
            Thread.yield();
        }
        // 能执行到这里,说明当前线程获取到了锁
        //  Print.tcfo("获取到了锁!!!");

        //设置在线程本地变量中,用于释放锁
        curNodeLocal.set(curNode);
    }

    @Override
    public void unlock() {
        Node curNode = curNodeLocal.get();
        curNode.setPreNode(null);   //help for GC
        curNodeLocal.set(null);
        curNode.setLocked(false);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        throw new IllegalStateException(
                "方法 'lockInterruptibly' 尚未实现!");
    }

    @Override
    public boolean tryLock() {
        throw new IllegalStateException(
                "方法 'tryLock' 尚未实现!");
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        throw new IllegalStateException(
                "方法 'tryLock' 尚未实现!");
    }

    @Override
    public Condition newCondition() {
        throw new IllegalStateException(
                "方法 'newCondition' 尚未实现!");
    }

    @Override
    public String toString() {
        return "CLHLock{" + name + '}';
    }

    @Data
    static class Node {
        //true:当前线程正在抢占锁、或者已经占有锁
        // false:当前线程已经释放锁,下一个线程可以占有锁了
        volatile boolean locked;
        //前一个节点,需要监听其locked字段
        Node preNode;

        public Node(boolean locked, Node preNode) {
            this.locked = locked;
            this.preNode = preNode;
        }

        //空节点
        public static final Node EMPTY =
                new Node(false, null);
    }
}

4.3 死锁的监测与中断

死锁是指两个或两个以上线程因抢占锁而造成的相互等待的现象。多个线程通过AB-BA模式抢占两个锁是造成多线程死锁比较普遍的原因。AB-BA模式的死锁具体表现为:线程X按照先后次序去抢占锁A与锁B,线程Y按照先后次序去抢占锁B与锁A,当线程X抢到锁A再去抢占锁B时,发现已经被其他线程拿走,然而线程Y拿到锁B后再去抢占锁A时,发现已经被其他线程拿走,于是线程X等待其他线程释放锁B,线程Y等待其他线程释放锁A,两个线程互相等待从而造成死锁。
JDK 8中包含的ThreadMXBean接口提供了多种监视线程的方法,其中包括两个死锁监测的方法,具体如下:
(1)findDeadlockedThreads
用于检测由于抢占JUC显式锁、Java内置锁引起死锁的线程。
(2)findMonitorDeadlockedThreads
仅仅用于检测由于抢占Java内置锁引起死锁的线程。