ReentrantLock 使用

1. 前言

本节内容主要是对 ReentrantLock 的使用进行讲解,之前对于 Lock 接口进行了讲解,ReentrantLock 是 Lock 接口的常用实现子类,占据着十分重要的地位。本节内容的知识点如下:

  • ReentrantLock 基本方法的使用,即 lock 与 unlock 方法的使用,这是最基础的方法使用,为重点内容;
  • ReentrantLock lockInterruptibly 与 tryLock 方法的使用,也是经常使用到的方法,为本节重点内容;
  • ReentrantLock 公平锁与非公平锁的使用,也是本节的重点内容;
  • ReentrantLock 其他方法的介绍与使用。

通篇来看,ReentrantLock 所有的知识点均为重点内容,是必须要掌握的内容。

2. ReentrantLock 介绍

ReentrantLock 在 Java 中也是一个基础的锁,ReentrantLock 实现 Lock 接口提供一系列的基础函数,开发人员可以灵活的使用函数满足各种复杂多变应用场景。

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

ReentrantLock,顾名思义,它是支持可重入锁的锁,是一种递归无阻塞的同步机制。除此之外,该锁还支持获取锁时的公平和非公平选择。

公平性:ReentrantLock 的内部类 Sync 继承了 AQS,分为公平锁 FairSync 和非公平锁 NonfairSync。

如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。公平锁的获取,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。

ReentrantLock 的公平与否,可以通过它的构造函数来决定。

3. ReentrantLock 基本方法 lock 与 unlock 的使用

我们使用一个之前涉及到的 synchronized 的场景,通过 lock 接口进行实现。

场景回顾

  • 创建两个线程,创建方式可自选;
  • 定义一个全局共享的 static int 变量 count,初始值为 0;
  • 两个线程同时操作 count,每次操作 count 加 1;
  • 每个线程做 100 次 count 的增加操作。

结果预期:获取到的结果为 200。之前我们使用了 synchronized 关键字和乐观锁 Amotic 操作进行了实现,那么此处我们进行 ReentrantLock 的实现方式。

实现步骤

  • step 1 :创建 ReentrantLock 实例,以便于调用 lock 方法和 unlock 方法;
  • step 2:在 synchronized 的同步代码块处,将 synchronized 实现替换为 lock 实现。

实例

public class DemoTest{
    private static int count = 0; //定义count = 0
    private static ReentrantLock lock = new ReentrantLock();//创建 lock 实例
    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) { //通过for循环创建两个线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //每个线程让count自增100次
                    for (int i = 0; i < 100; i++) {
                        try {
                            lock.lock(); //调用 lock 方法
                            count++;
                        } finally {
                            lock.unlock(); //调用unlock方法释放锁
                        }
                    }
                }
            }). start();
        }
        try{
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(count);
    }
}

代码分析
我们通过 try finally 模块,替代了之前的 synchronized 代码块,顺利的实现了多线程下的并发。

4. tryLock 方法

我们之前进行过介绍,Lock 接口包含了两种 tryLock 方法,一种无参数,一种带参数。

  • boolean tryLock():仅在调用时锁为空闲状态才获取该锁。如果锁可用,则获取锁,并立即返回值 true。如果锁不可用,则此方法将立即返回值 false;
  • boolean tryLock(long time, TimeUnit unit):如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁;

为了了解两种方法的使用,我们先来设置一个简单的使用场景。

场景设置

  • 创建两个线程,创建方式自选;
  • 两个线程同时执行代码逻辑;
  • 代码逻辑使用 boolean tryLock () 方法,如果获取到锁,执行打印当前线程名称,并沉睡 5000 毫秒;如果未获取锁,则打印 timeout,并处理异常信息;
  • 观察结果并进行分析;
  • 修改代码,使用 boolean tryLock (long time, TimeUnit unit) 方法,设置时间为 4000 毫秒;
  • 观察结果并进行分析;
  • 再次修改代码,使用 boolean tryLock (long time, TimeUnit unit) 方法,设置时间为 6000 毫秒;
  • 观察结果并进行分析。

实例:使用 boolean tryLock () 方法

public class DemoTest implements Runnable{
    private static Lock locks = new ReentrantLock();
    @Override
    public void run() {
        try {
            if(locks.tryLock()){ //尝试获取锁,获取成功则进入执行,不成功则执行finally模块
                System.out.println(Thread.currentThread().getName()+"-->");
                Thread.sleep(5000);
            }else{
                System.out.println(Thread.currentThread().getName()+" time out ");
            }
        } catch (InterruptedException e) {
             e.printStackTrace();
        }finally {
            try {
                locks.unlock();
            } catch (Exception e) {
                System.out.println(Thread.currentThread().getName() + "未获取到锁,释放锁抛出异常");
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        DemoTest test =new DemoTest();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);
        t1. start();
        t2. start();
        t1.join();
        t2.join();
        System.out.println("over");
    }
}

结果验证

Thread-1-->
Thread-0 time out 
Thread-0 未获取到锁,释放锁抛出异常
over

结果分析:从打印的结果来看, Thread-1 获取了锁权限,而 Thread-0 没有获取锁权限,这就是 tryLock,没有获取到锁资源则放弃执行,直接调用 finally。

实例:使用 boolean tryLock (4000 ms) 方法
将 if 判断进行修改如下:

 if(locks.tryLock(4000,TimeUnit.MILLISECONDS)){ //尝试获取锁,获取成功则进入执行,不成功则执行finally模块
      System.out.println(Thread.currentThread().getName()+"-->");
      Thread.sleep(5000);
  }

结果验证

Thread-1-->
Thread-0 time out 
Thread-0 未获取到锁,释放锁抛出异常
over

结果分析:tryLock 方法,虽然等待 4000 毫秒,但是这段时间不足以等待 Thread-1 释放资源锁,所以还是超时。 我们换成 6000 毫秒试试。

实例:使用 boolean tryLock (6000 ms) 方法
将 if 判断进行修改如下:

 if(locks.tryLock(6000,TimeUnit.MILLISECONDS)){ //尝试获取锁,获取成功则进入执行,不成功则执行finally模块
      System.out.println(Thread.currentThread().getName()+"-->");
      Thread.sleep(5000);
  }

结果验证

Thread-1-->
Thread-0-->
over

结果分析:tryLock 方法,等待 6000 毫秒,Thread-1 先进入执行,5000 毫秒后 Thread-0 进入执行,都能够有机会获取锁。

总结:以上就是 tryLock 方法的使用,可以指定最长的获取锁的时间,如果获取则执行,未获取则放弃执行。

5. 公平锁与非公平锁

分类:根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁。

公平锁:表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。

非公平锁:非公平锁则在运行时闯入,不遵循先到先执行的规则。

ReentrantLock:ReentrantLock 提供了公平和非公平锁的实现。

ReentrantLock 实例

//公平锁
ReentrantLock pairLock = new ReentrantLock(true);
//非公平锁
ReentrantLock pairLock1 = new ReentrantLock(false);
//如果构造函数不传递参数,则默认是非公平锁。
ReentrantLock pairLock2 = new ReentrantLock();

场景介绍:通过模拟一个场景假设,来了解公平锁与非公平锁。

  • 假设线程 A 已经持有了锁,这时候线程 B 请求该锁将会被挂起;
  • 当线程 A 释放锁后,假如当前有线程 C 也需要获取该锁,如果采用非公平锁方式,则根据线程调度策略,线程 B 和线程 C 两者之一可能获取锁,这时候不需要任何其他干涉;
  • 而如果使用公平锁则需要把 C 挂起,让 B 获取当前锁,因为 B 先到所以先执行。

Tips:在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。

6. lockInterruptibly 方法

lockInterruptibly () 方法:能够中断等待获取锁的线程。当两个线程同时通过 lock.lockInterruptibly () 获取某个锁时,假若此时线程 A 获取到了锁,而线程 B 只有等待,那么对线程 B 调用 threadB.interrupt () 方法能够中断线程 B 的等待过程。

场景设计

  • 创建两个线程,创建方式可自选实现;
  • 第一个线程先调用 start 方法,沉睡 20 毫秒后调用第二个线程的 start 方法,确保第一个线程先获取锁,第二个线程进入等待;
  • 最后调用第二个线程的 interrupt 方法,终止线程;
  • run 方法的逻辑为打印 0,1,2,3,4,每打印一个数字前,先沉睡 1000 毫秒;
  • 观察结果,看是否第二个线程被终止。

实例

public class DemoTest{
    private Lock lock = new ReentrantLock();

    public void doBussiness() {
        String name = Thread.currentThread().getName();
        try {
            System.out.println(name + " 开始获取锁");
            lock.lockInterruptibly(); //调用lockInterruptibly方法,表示可中断等待
            System.out.println(name + " 得到锁,开工干活");
            for (int i=0; i<5; i++) {
                Thread.sleep(1000);
                System.out.println(name + " : " + i);
            }
        } catch (InterruptedException e) {
            System.out.println(name + " 被中断");
        } finally {
            try {
                lock.unlock();
                System.out.println(name + " 释放锁");
            } catch (Exception e) {
                System.out.println(name + " : 没有得到锁的线程运行结束");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final DemoTest lockTest = new DemoTest();
        Thread t0 = new Thread(new Runnable() {
                    public void run() {
                        lockTest.doBussiness();
                    }});
        Thread t1 = new Thread(new Runnable() {
                    public void run() {
                        lockTest.doBussiness();
                    }});

        t0. start();
        Thread.sleep(20);
        t1. start();
        t1.interrupt();
    }
}

结果验证:可以看到,thread -1 被中断了。

Thread-0 开始获取锁
Thread-0 得到锁,开工干活
Thread-1 开始获取锁
Thread-1 被中断
Thread-1 : 没有得到锁的线程运行结束
Thread-0 : 0
Thread-0 : 1
Thread-0 : 2
Thread-0 : 3
Thread-0 : 4
Thread-0 释放锁

7. ReentrantLock 其他方法介绍

对 ReentrantLock 来说,方法很多样,如下介绍 ReentrantLock 其他的方法,有兴趣的同学可以自行的尝试使用。

  • getHoldCount():当前线程调用 lock () 方法的次数;
  • getQueueLength():当前正在等待获取 Lock 锁的线程的估计数;
  • getWaitQueueLength(Condition condition):当前正在等待状态的线程的估计数,需要传入 Condition 对象;
  • hasWaiters(Condition condition):查询是否有线程正在等待与 Lock 锁有关的 Condition 条件;
  • hasQueuedThread(Thread thread):查询指定的线程是否正在等待获取 Lock 锁;
  • hasQueuedThreads():查询是否有线程正在等待获取此锁定;
  • isFair():判断当前 Lock 锁是不是公平锁;
  • isHeldByCurrentThread():查询当前线程是否保持此锁定;
  • isLocked():查询此锁定是否由任意线程保持。

8. 小结

本节内容对 ReentrantLock 进行了比较详细的讲解,通篇内容皆为重点内容,需要同学们进行细致的掌握。核心内容即为 ReentrantLock 的使用,可以根据小节中的实例进行自行的编码和试验,更深刻的理解 ReentrantLock 的使用。