可重入锁ReentrantLock基础和原理

0 阅读4分钟

ReenterantLock基础

ReenterantLocksynchronized关键字的扩展。它是一个显示锁,意味着锁的获取和释放必须由程序员手动编写代码控制。

相对于synchronized,它具备以下的优势:

  1. 未获取锁后的状态可中断
  2. 可以设置超时时间
  3. 可以设置公平锁
  4. 支持多个条件变量

并且它与synchronized一样都支持可重入。

可重入

可重入是指同一个线程如果首次获得这把锁后,因为它是这把锁的主人,可以再次获得这把锁。

相对的,不可重入是指获得这把锁之后不能再次获得这把锁,自身也会被锁挡住。

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ReentrantLock reentrantLock=new ReentrantLock();
        for (int i=0;i<100;i++){
            testReentrantLock(reentrantLock,i);
        }
    }
    public static void testReentrantLock(ReentrantLock reentrantLock,int number){
        reentrantLock.lock();
        System.out.printf("第%d次获得锁\n",number);
    }

未获得锁后的状态可打断

先看synchronized的行为。

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Object lock=new Object();
        Thread thread1=new Thread(()->{
            synchronized (lock){
                while(true){

                }
            }
        });
        thread1.start();
        TimeUnit.SECONDS.sleep(1);
        Thread thread2=new Thread(()->{
            synchronized (lock){
                
            }
            System.out.println("running");
        });
        thread2.start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("线程1:"+thread1.getState());
        System.out.println("线程2:"+thread2.getState());
        thread2.interrupt();

        System.out.println("线程2:"+thread2.getState());
    }

程序运行结果如下:

image.png

再看ReentrantLocklock行为。

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ReentrantLock lock=new ReentrantLock();
        Thread thread1=new Thread(()->{
            lock.lock();
            while(true){

            }
        });
        thread1.start();
        TimeUnit.SECONDS.sleep(1);
        Thread thread2=new Thread(()->{
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                System.out.println("interrupted");
                return ;
            }
            System.out.println("running");
            lock.unlock();
        });
        thread2.start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("线程1:"+thread1.getState());
        System.out.println("线程2:"+thread2.getState());
        thread2.interrupt();

        System.out.println("线程2:"+thread2.getState());
    }

结果如下:

image.png

可以看出通过synchronized获取锁失败后是进入BLOCKED状态,而通过ReentrantLock获取锁失败后是进入WAITTING状态。

接下来继续观察lockInterruptibly

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ReentrantLock lock=new ReentrantLock();
        Thread thread1=new Thread(()->{
            lock.lock();
            while(true){

            }
        });
        thread1.start();
        TimeUnit.SECONDS.sleep(1);
        Thread thread2=new Thread(()->{
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                System.out.println("interrupted");
                return ;
            }
//            lock.lock();
            System.out.println("running");
            lock.unlock();
        });
        thread2.start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("线程1:"+thread1.getState());
        System.out.println("线程2:"+thread2.getState());
        thread2.interrupt();

        System.out.println("线程2:"+thread2.getState());
    }

结果是:

image.png

先探究一个问题:为什么BLOCKED不可以被打断,WAITING可以被打断?

我们知道,interrupt()Java线程中用于协作式地通知一个线程应该停止当前正在执行的任务的核心机制。它的核心作用是设置线程的中断标志位,而不是强制终止线程。

线程状态调用 t.interrupt()的影响
RUNNABLE仅设置中断标志位。线程的 isInterrupted()将返回 true,但线程会继续运行,直到它自己检查这个标志。
BLOCKED (等锁)仅设置中断标志位。线程依然会卡在 BLOCKED状态,继续等它的锁。不会抛出 InterruptedException。直到它终于拿到锁,进入 RUNNABLE后,才能通过检查标志位来响应中断。
WAITING TIMED_WAITING (等通知、睡眠、合并)立即抛出 InterruptedException,并且清除中断标志位(重置为false)。线程会从 wait(), join(), sleep()等方法调用中退出,进入 RUNNABLE状态。

那为什么ReentrantLocklock也是WAITING状态,为什么不会被打断呢?

因为导致WAITING状态的原因和底层机制不同,ReentrantLock的加锁底层就是LockSupport.park()

进入 WAITING的方法interrupt()的响应说明
Object.wait() Thread.sleep() Thread.join()立即响应,抛出 InterruptedException。并清除中断状态这些是Java标准库提供的协作式中断原语,设计目的就是快速响应中断请求。
LockSupport.park()立即解除阻塞,但不抛出异常,只是安静地返回。但是保留中断状态这是更底层的线程阻塞原语。中断会唤醒它,但行为的控制权完全交给了上层的调用代码。ReentrantLock就构建在此之上。

也就是说其实interrupt是唤醒了线程的,但是它被唤醒之后再次去尝试获得锁,导致又调用了LockSupport.lock

ReentrantLock.lockInterruptibly只是设定了唤醒之后抛出异常,这部分在后面的源码部分再深挖。

锁超时

通过synchronized获得锁时,如果未获得,则会一直BLOCKED,而ReentrantLock可以设置超时的时间。

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ReentrantLock lock=new ReentrantLock();
        Thread thread1=new Thread(()->{
            lock.lock();
            while(true){

            }
        });
        thread1.start();
        TimeUnit.SECONDS.sleep(1);
        Thread thread2=new Thread(()->{
//            try {
//                lock.lockInterruptibly();
//            } catch (InterruptedException e) {
//                System.out.println("interrupted");
//                return ;
//            }
            try {
                lock.tryLock(5,TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("running");
            //lock.unlock();
        });
        thread2.start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("线程1:"+thread1.getState());
        System.out.println("线程2:"+thread2.getState());
       //thread2.interrupt();

        System.out.println("线程2:"+thread2.getState());
    }

结果如下:

image.png

使用tryLock()再尝试在给定的时间内获取锁,如果没有获得,将直接进行剩余代码。

公平锁

synchronized是非公平锁,ReentrantLock默认也是非公平锁。 以下代码启用公平锁:

ReentrantLock lock=new ReentrantLock(true);

支持多个条件变量

synchronized中存在唯一的一个条件变量,也就是waitSet,任何调用wait的线程都将进入waitSet,通过notifyAll将会唤醒waitSet中的所有线程。

ReentrantLock支持多个条件变量。可以实现多个类似的waitSet,实现一种精细化的唤醒。

方法作用说明
await()使当前线程等待释放锁,进入等待状态
signal()唤醒一个等待线程从等待队列中唤醒一个
signalAll()唤醒所有等待线程唤醒该条件的所有线程
awaitNanos(long)限时等待超时自动唤醒