ReentrantLock详解

目录

一、ReentrantLock的含义

二、RerntrantLock当中的常用方法

     ①lock()和unlock()方法

   ②构造方法

   ③tryLock()方法

       tryLock()无参数

        tryLock(timeout,Times)有参数

    ④lockInterruptibly() throws InterruotedException

经典面试问题: 

ReentrantLock和synchronized有什么不同

使用方法上面的区别

①ReentrantLock可以提供公平+非公平两种特性

 ②ReentrantLock的加锁、解锁操作都是需要手动进行,

③synchronized无法提供lock.tryLock()这样的尝试获取锁的特性,而ReentrantLock可以提供。

④ReentrantLock可以提供中断式加锁。 

 ⑤ReentrantLock借助Condition类:可以指定唤醒线程

 锁的实现方式上面的区别

     提供的级别不一样:

       原理不一样: 

      ReentrantLock的原理 


一、ReentrantLock的含义

        ReentrantLock也是Java当中提供的一种锁。这种锁和synchronized类似也可以起到互斥使用,保证线程安全的的作用。

        关于synchronized的作用,已经在这一篇文章当中提及:

        (3条消息) Java对于synchronized的初步认识_革凡成圣211的博客-CSDN博客https://blog.csdn.net/weixin_56738054/article/details/128062475?spm=1001.2014.3001.5501

        但是,仍然在使用的语法上面和synchronized有一些差别。下面,将具体介绍一下ReentrantLock的使用以及ReentrantLock的各个特性。


二、RerntrantLock当中的常用方法

     ①lock()和unlock()方法

        顾名思义,lock()方法就是线程进入同步代码块之后的加锁操作,而unlock()就是需要离开代码块之后的解锁操作。

       在上述代码当中,锁就是lock对象。当多个线程同时尝试调用lock.lock()方法之后,只有其中一个线程可以获得锁,其余线程都需要阻塞等待。当线程执行到unlock()方法之后,说明已经解锁了,其他线程可以继续获取lock。


       但是, 上面的写法不是规范的写方法,规范的写法,应当把lock.unlock()写进finally代码块当中,并且lock.lock()方法需要在try方法上方的第一行

’ 如下图所示:

       原因:

       如果在上述代码当中,出现了if语句,线程进入if语句之后调用了lock.lock()方法加锁成功。但是线程离开if语句的时候,没有调用lock.unlock()方法,这样也就意味着线程提前返回了,没有解锁。那么其他线程如果有阻塞等待的,将一直阻塞等待。   

       因此,为了防止忘记解锁的情况,应当把lock.unlock()放到finally当中。

       如图:故意不解锁,看看有什么后果。场景,此时有两个线程,一个是thread1,另外一个是thread2。两个线程分别通过同一个对象count调用add()方法:

      运行:

     

可以看到,控制台始终输出了-->"现在的线程是....thread1...",说明thread2无法再次获取到锁。


   ②构造方法

ReentrantLock lock1=new ReentrantLock(true);
        
ReentrantLock lock=new ReentrantLock(false);

       如果构造方法当中,指定了true作为参数,那么lock将是公平锁。如果没有指定布尔值,或者指定了布尔值为false,那么lock将是非公平锁。


   ③tryLock()方法

       tryLock()无参数

        tryLock()方法有两个作用:

       当调用lock.tryLock()的时候,如果lock此时还没有被其他线程占有,那么它会立刻获取到锁,并且返回true。

        如果lock已经被其他线程占用了,那么调用lock.tryLock()的线程将不会阻塞等待,而是继续往下执行。       


        tryLock(timeout,Times)有参数

         当线程调用lock.tryLock(timeout,TimeUtil.时间单位常量)

         方法的时候,会发生以下的情况:

         (1)当前线程将会在lock.tryLock(timeout,TimeUtil.时间单位常量)这行代码处阻塞等待timeout时间,如果获取到锁的线程在这个timeout时间内释放锁了,那么正在等待的线程可以重新获取锁。

         (2)如果阻塞等待的线程直到timeout时间了,加锁的线程仍然没有释放锁,那么原来在等待的线程将不再等待,直接返回。

        (3)如果超时等待的线程在等待锁释放的timeout时间内被中断(其他线程调用t.interrupt())方法中断正在等待的线程,那么当前正在等待的线程会抛出InterruptException,也会终止等待

        因此,正确使用tryLock()的方式为:首先进行判断,如果得到结果为false,也就是获取不到锁,直接return返回即可。

        


    ④lockInterruptibly() throws InterruotedException

      这个方法,类似于"lock"也是属于"加锁"的方法。 

      和单纯的lock()方法不同,线程调用了lockInterrupt()方法之后,可以"响应中断"式地加锁。

      假设,在某一时刻,t1线程获取到锁,在t1调用lockInterruptibly()方法获取到锁之后,如果t2也调用这个方法获取锁,那么t2会进入阻塞等待的状态。

      如果t2在阻塞等待的过程当中,被其他线程调用t2.interrupt()方法,那么线程t2会被触发异常(InterruptException),并且被"唤醒"。


     代码实现:

     add()方法,使用sleep(1000)的目的是减慢循环的速度

class Count1{
    public int number;
    ReentrantLock lock=new ReentrantLock();
    public void add(){
        try {
            //使用"可中断"式地加锁
            lock.lockInterruptibly();
            //标志位默认为false
            while (true){
                number++;
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

      启动t1,t2线程,让t1先获取到锁,t2后面才获取到锁:

public class ThreadDemo32 {
    public static void main(String[] args) {
        Count1 count1=new Count1();
        Thread t1=new Thread(new Runnable() {
            @Override
            public void run() {
                count1.add();
            }
        },"t1");
        t1.start();
        //让主线程休眠1000毫秒,确保t1一定启动成功了
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
       
        Thread t2=new Thread(new Runnable() {
            @Override
            public void run() {
                count1.add();
            }
        },"t2");
        t2.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //中断t2
        t2.interrupt();
    }
}

      图解:

      

但是,一运行程序,发现出现了下面的问题:

  

出现第一个问题的原因我们找到了,那出现第二个问题,,锁状态异常又是什么原因呢? 

回到add()方法当中:

 这就是为什么thread2在被唤醒之后,触发锁状态异常的原因。


经典面试问题: 

ReentrantLock和synchronized有什么不同

使用方法上面的区别

①ReentrantLock可以提供公平+非公平两种特性

     当ReentrantLock构造方法中指定了参数为true的时候,这个锁被确定为公平锁。

      而synchronized无法提供公平锁的特性


 ②ReentrantLock的加锁、解锁操作都是需要手动进行,

       而synchronized的话可以进行自动的加锁、解锁操作。

       synchronized可以有效避免加锁之后忘记解锁的情况。

     当代码执行到synchronized修饰的代码块的时候,如果在同步代码块内部发生了异常,没有及时处理的话,会提前退出并且让线程释放锁。

      而ReentrantLock无法做到立刻解锁,因此,unLock()的解锁操作一定要在finally代码块当中,避免加锁之后忘记解锁的情况。 


③synchronized无法提供lock.tryLock()这样的尝试获取锁的特性,而ReentrantLock可以提供。

        线程如果在指定的时间之内无法获取到锁,或者锁已经被占用了,那么lock.tryLock()可以有效减少线程阻塞等待的情况,或者减少阻塞等待的时间。

       而synchronized只会让无法获取到锁的线程"死等"。直到获取到锁的线程释放锁


④ReentrantLock可以提供中断式加锁。 

        ④ReentrantLock在调用lock.lockInterruptibly()时候,可以让获取不到锁,进入阻塞等待的线程被提前"唤醒",但是synchronized不可以。具体的操作已经在上面解释了。

  下面,给一个场景,验证可中断式加锁。

public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock=new ReentrantLock(true);
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                //让thread获取到锁
                lock.lock();
                System.out.println("thread获取到了锁");
                //不提供解锁的操作,一直让thread占有这把锁
            }
        });
        thread.start();
        //确保thread已经启动,并且持有锁了
        Thread.sleep(1000);

        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                //不让thread1获取到锁,同时thread1是"可中断式"加锁
                try {
                    lock.lockInterruptibly();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("thread1的阻塞等待被中断了");
                }
            }
        });
        thread1.start();
        //main线程尝试唤醒thread1
        thread1.interrupt();
    }

   运行程序,可以看到:thread1的阻塞被中断了 


 ⑤ReentrantLock借助Condition类:可以指定唤醒线程

      指定了可以notify()的线程。

      类比于synchronized,如果多个线程因为获取不到锁进入了WAITING或者TIME_WAITING状态。那么,在notify()的时候,只可以随机唤醒一个正在WAITING或者TIME_WAITING的线程。

       但是ReentrantLock借助Condition接口,可以指定唤醒线程


 锁的实现方式上面的区别

     提供的级别不一样:

       ReentrantLock是Java当中的一个具体的,是在API级别提供的锁,

       而synchronized是Java当中提供的一个关键字,是JVM级别提供的锁


       原理不一样: 

       synchronized加锁的过程,涉及了锁升级的过程。

       从无锁->偏向锁->轻量级锁->重量级锁;并且还可能涉及锁粗化、锁消除。

       在下面这一篇文章当中已经提到了:
(2条消息) 【JavaEE多线程】synchronized原理篇_革凡成圣211的博客-CSDN博客https://blog.csdn.net/weixin_56738054/article/details/128826633?spm=1001.2014.3001.5502      但是,ReentrantLock是基于AQS来实现的。

       在这一篇文章当中,我们也提到了什么是AQS,它的核心就是两个属性。一个是内部封装的队列。用来保存获取不到锁的线程。另外一个是state属性,用来标记是否可以获取锁。
(2条消息) Java当中的AQS_革凡成圣211的博客-CSDN博客https://blog.csdn.net/weixin_56738054/article/details/128664083?spm=1001.2014.3001.5502


      ReentrantLock的原理 

       ReentrantLock在初始化的时候,就提供了公平和非公平的两种方式。

        

       在ReentrantLock内部封装了两个静态内部类,一个是FairSync,另外一个是NofairSymc

       分别提供了公平的加锁方式和不公平的加锁方式。

        这两个类都继承于ReentrantLock内部的一个Syn,然后这个Syn又继承于AQS。

          当有线程调用lock方法的时候:

          如果线程获取到锁了,那么就会通过CAS的方式把AQS内部的state设置成为1

          


       这个时候,当前线程就获取到锁了。

        可以看到,只有首部的节点(head节点封装的线程)可以获取到锁。

       其他线程都会加入到这一个阻塞队列当中。

        如果是公平锁的话,当head节点释放锁之后,会优先唤醒head.next这一个节点对应的线程。令head=head.nxet,让下一个节点对应的线程获取到锁。


        如果是非公平锁的话,会让之前head节点之后的节点对应的线程一起采用CAS的方式获取锁。       

     

  • 14
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
重入锁(ReentrantLock)是一种独占锁,也就是说同一时间只能有一个线程持有该锁。与 synchronized 关键字不同的是,重入锁可以支持公平锁和非公平锁两种模式,而 synchronized 关键字只支持非公平锁。 重入锁的实现原理是基于 AQS(AbstractQueuedSynchronizer)框架,利用了 CAS(Compare And Swap)操作和 volatile 关键字。 重入锁的核心思想是“可重入性”,也就是说如果当前线程已经持有了该锁,那么它可以重复地获取该锁而不会被阻塞。在重入锁内部,使用了一个计数器来记录当前线程持有该锁的次数。每当该线程获取一次锁时,计数器就加 1,释放一次锁时,计数器就减 1,只有当计数器为 0 时,其他线程才有机会获取该锁。 重入锁的基本使用方法如下: ```java import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockTest { private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + " get lock"); Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); System.out.println(Thread.currentThread().getName() + " release lock"); } }, "Thread-1").start(); new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + " get lock"); } finally { lock.unlock(); System.out.println(Thread.currentThread().getName() + " release lock"); } }, "Thread-2").start(); } } ``` 在上面的示例代码中,我们创建了两个线程,分别尝试获取重入锁。由于重入锁支持可重入性,因此第二个线程可以成功地获取到该锁,而不会被阻塞。当第一个线程释放锁后,第二个线程才会获取到锁并执行相应的操作。 需要注意的是,使用重入锁时一定要记得在 finally 块中释放锁,否则可能会导致死锁的问题。同时,在获取锁时也可以设置超时时间,避免由于获取锁失败而导致的线程阻塞问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值