ReentrantLock入门讲解

目录

一、概述

二、ReentrantLock基本语法

三、ReentrantLock的特性

3.1可重入

3.2可打断

3.3锁超时

3.4公平锁

3.5条件变量


一、概述

ReentrantLock字面意思就是可重入锁(re表示重新,entrant表示进入,lock表示锁),它其实是属于JUC包(java,util.concurrent)下的一个类。在多线程环境下我们为了避免并发引发的一些问题常常会选择给可能引起并发问题的代码(临界区)加锁,说到加锁大家最熟悉的可能就是synchronized关键字,除此以外的另一个就是ReentrantLock类。ReentrantLock相比于synchronized有如下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

以上四点是ReentrantLock相比于synchronized的主要区别,除了以上四个区别外他们之间还有一共同的特点就是“都支持可重入锁”。所谓可重入锁就是指“同一个线程对同一个对象可以反复的进行加锁操作”。以上概念性的东西听着比较懵逼,下面我们就以上概念具体展开讲解一下。

二、ReentrantLock基本语法

synchronized加锁只需写上synchronized关键字即可,它是在关键字层面的加锁,而想要使用ReentrantLock对临界区加锁必须先把ReentrantLock的对象new出来才行,它是在对象级别对临界区进行的加锁,看一下ReentrantLock使用的基本语法:

//创建ReentrantLock对象
ReentrantLock reentrantLock = new ReentrantLock();
//获取锁
reentrantLock.lock();
try{
   //临界区
} finally{
   //释放锁
   reentrantLock.unlock();
}

以上代码需要注意两点:

  • lock()和unlock()方法一定要成对出现,加了锁一定要释放;
  • 用ReentrantLock加锁时,临界区的代码一般放到try块中,释放锁的操作放在finally块中,这样可以保证即使临界区的代码出现了异常也能正确的把锁释放掉。

作为对比,我们放一段同样的逻辑用synchronized编写是什么样子:

Object obj = new Object();
synchronized (obj){
     //临界区
}

虽然看上synchronized比ReentrantLock要简单很多,但ReentrantLock比synchronized有很多其他的特性,我们接下来就这二者的异同展开讲讲。

三、ReentrantLock的特性

3.1可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁,如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。这个特点是synchronized和ReentrantLock都具备的,他们都是可重入的。今天讲解ReentrantLock,我们就以ReentrantLock为例看一下它的可重入特性:

public class ReentrantLockDemo {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        lock.lock();
        try {
            System.out.println("enter main");
            m1();
        }finally {
            lock.unlock();
        }
    }

    public static void m1(){
        lock.lock();
        try {
            System.out.println("enter m1");
            m2();
        }finally {
            lock.unlock();
        }
    }

    public static void m2(){
        lock.lock();
        try {
            System.out.println("enter m2");
        }finally {
            lock.unlock();
        }
    }
}

上面的代码只有一个主线程在运行,我们在main方法中通过调用lock.lock()方法加锁,之后在main中调用m1(),在m1()中又加锁,m1()中调用了m2(),在m2()中再次加锁,同一个线程加了三次锁,但在第2、3次加锁的时候并没有因为之前加过锁而发生阻塞,这就是可重入的特性体现,看一下输出结果(三个方法都执行了输出语句,证明没有阻塞):

enter main
enter m1
enter m2

3.2可打断

所谓可打断就是指某个线程在等待锁的过程中,其他线程可以通过interrupt()方法中止其等待,这个特征是ReentrantLock特有的,synchronized并没有。也就是说如果通过synchronized进行加锁,如果这个线程一直没有等到其他线程释放掉锁那么它会一直等下去,而ReentrantLock如果等的时间过长还没有发应,它是可以被中止等待的。直接看代码吧:

public class ThreadLocalBDemo02 {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        //开启一个线程t1
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //如果没有竞争那么此方法就会获取lock锁对象
                    //如果有竞争就进入阻塞队列,可以被其他线程用interrupt方法打断
                    System.out.println("尝试获取锁");
                    lock.lockInterruptibly();
                }catch (InterruptedException e){
                    e.printStackTrace();
                    System.out.println("被打断,没有获得锁,返回");
                    return;
                }
                try {
                    System.out.println("获取到锁");
                }finally {
                    lock.unlock();
                }
            }
        },"t1");

        //主线程先获取锁,这个时候t1线程就无法获取锁了,它会在哪里等待直到主线程把锁释放掉
        lock.lock();
        t1.start();
        //主线程睡1秒,没有释放锁,之后打断t1,让他不要再等了,我是不会释放的。。。。
        Thread.sleep(1000);
        t1.interrupt();
    }
}

输出日志:

尝试获取锁
打断t1,让他不要再等了
java.lang.InterruptedException
被打断,没有获得锁,返回
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:896)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1221)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:340)
	at com.shhxbean.test.ThreadLocalBDemo02$1.run(ThreadLocalBDemo02.java:16)
	at java.lang.Thread.run(Thread.java:745)

通过输出日志我们可以看到当t1长时间没有获得锁的时候,别的线程是可以通过调用等待线程的interrupt()方法将其打断的,避免无限制的等待下去。和上面演示可重入特性不同的是想要具备被打断的特性在获取锁的时候需要调用ReentrantLock的lockInterruptibly()方法而不是lock()方法。

3.3锁超时

上面3.3中讲的如果线程长时间没有获得锁,可以通过在其他线程中调用interrupt方法来打断他,进而避免死等的发生。只不过这种避免死等的方式是一种被动的方式,必须在其他线程中调用了interrupt方法才可以。而这小节讲的“锁超时”也是一种避免死等情况发生的特性,只不过相比“可打断”的被动,“锁超时”是一种更主动的方式。当某个线程尝试去获得锁的时候发现锁被其他线程占用了,那么它会等一段时间,如果过了一段时间还是没有获取到锁那么它就会主动放弃等待。代码如下:

public class ReentrantLockDemo03 {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //尝试获取锁,获取不到立马放弃
                System.out.println(Thread.currentThread().getName() + "尝试获取锁");
                if(! lock.tryLock()){
                    System.out.println(Thread.currentThread().getName() + "获取不到锁");
                    return;
                }

                try {
                    System.out.println(Thread.currentThread().getName() + "获得到了锁");
                }finally {
                    lock.unlock();
                }
            }
        },"t1");

        //主线程先获取到锁,那t1就获取不到了,获取不到它就会放弃
        System.out.println(Thread.currentThread().getName() + "先获取到锁");
        lock.lock();
        t1.start();
    }
}

输出日志:

main先获取到锁
t1尝试获取锁
t1获取不到锁

首先想要启用ReentrantLock的锁超时方法需调用其tryLock()方法,这个方法有个重载就是可以带参数tryLock(时间长度,时间单位),没带参数的就表示如果当前没有获得到锁那么就立马放弃,如果是带参数的它就会等待设置的参数时常,如果过了这个时常还是没有获取锁那就主动放弃。这个方法的返回值是一个布尔类型的,返回false表示没有获取到锁,反之,返回true表示获取到了锁。

3.4公平锁

所谓锁的公平性我们不妨先拿synchronized做个简单对比,synchronized其实就是一种不公平的锁,因为当一个线程持有了锁以后其他线程就会被阻塞,当锁的持有者释放了锁以后,这些被阻塞的线程就会一拥而上的去抢锁,睡抢到了谁就是锁的新主人,这就是一种不公平性,因为当线程很多的时候很有可能某个线程会一直抢不到进而一直处在阻塞状态。而所谓公平,这里指的就是一种先到先得的特征,根据线程的先后顺序来让其拥有锁,可以简单理解为先进先出。ReentrantLock其实默认也是不公平锁,但是它的某些构造方法可以让其实现公平锁。

//默认非公平锁,传入false也是非公平锁
ReentrantLock unfairLock = new ReentrantLock();
ReentrantLock unfairLock02 = new ReentrantLock(false);
//在构造方法中传入true即可构造出公平锁
ReentrantLock faiLock = new ReentrantLock(true);

3.5条件变量

条件变量这个概念可能你不太理解,我们在使用synchronized的时候有时候会调用线程的两个方法wait()和notify(),当一个线程获得了锁对象但是由于它缺少一些条件不能正常的执行的时候我们就会调用其wait()方法让其把锁让出来,而线程本身进入到一个叫waitSet的队列中等待,直到条件满足了通过调用notity()方法将其唤醒。这里的这个waitSet就可以简单的理解为是一个条件变量(可以类比理解为一个休息室)。不过synchronized中不管你线程当前是因为什么原因、缺少什么样的条件,他都会不加区分的将线程统一放到一个waitSet中,与synchronized不同的是ReentrantLock支持按线程缺少条件的不同,把线程放到不同的waitSet中。我们通过一个例子简单理解一下条件变量:假设有两个线程,其中一个必须抽着烟才能干活,而另一个线程必须吃了外卖才能干活,如果没满足有烟或有外卖的条件,那么线程一或二就必须进入休息室等待条件满足才可执行。我们用代码描述一下:

public class ReentrantLockDemo04 {
    static ReentrantLock ROOM  = new ReentrantLock();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    //等烟的休息室
    static Condition waitCigaretteSet = ROOM.newCondition();
    //等外卖的休息室
    static Condition waitTakeoutSet = ROOM.newCondition();

    public static void main(String[] args) throws InterruptedException {
        //线程1,模拟等烟的线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                ROOM.lock();
                try{
                    System.out.println(Thread.currentThread().getName()+"问有烟吗?  " + hasCigarette);
                    //利用循环不断的去检测是否满足了条件:烟到了
                    while (!hasCigarette){
                        System.out.println(Thread.currentThread().getName()+"没有获取到烟,先歇会儿");
                        //进入专门等烟的休息室去等待
                        waitCigaretteSet.await();
                    }
                    //跳出了while循环,说明有烟了,可以开始干活了
                    if(hasCigarette){
                        System.out.println(Thread.currentThread().getName()+"开始干活儿...");
                    }else {
                        System.out.println(Thread.currentThread().getName()+"没干成活儿...");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    ROOM.unlock();
                }
            }
        },"小男").start();

        //线程2,模拟等外卖的线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                ROOM.lock();
                try{
                    System.out.println(Thread.currentThread().getName()+"问外卖送到了吗?  " + hasTakeout);
                    //利用循环不断的去检测是否满足了条件:外卖到了
                    while (!hasTakeout){
                        System.out.println(Thread.currentThread().getName()+"没有饭,先歇会儿");
                        //进入专门等饭的休息室去等待
                        waitTakeoutSet.await();
                    }
                    //跳出了while循环,说明外卖到了,可以开始干活了
                    if(hasTakeout){
                        System.out.println(Thread.currentThread().getName()+"开始干活儿...");
                    }else {
                        System.out.println(Thread.currentThread().getName()+"没干成活儿...");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    ROOM.unlock();
                }
            }
        },"小女").start();

        //1s以后,送外卖的来了
        Thread.sleep(1000);
        //线程3,模拟送外卖的线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                ROOM.lock();
                try{
                    //把外卖送到专门等饭的休息室
                    hasTakeout = true;
                    waitTakeoutSet.signal();
                }finally {
                    ROOM.unlock();
                }
            }
        },"送外卖的").start();

        //再等1s以后,送烟的来了
        Thread.sleep(1000);
        //线程3,模拟送外卖的线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                ROOM.lock();
                try{
                    //把烟送到专门等烟的休息室
                    hasCigarette = true;
                    waitCigaretteSet.signal();
                }finally {
                    ROOM.unlock();
                }
            }
        },"送烟的").start();
    }
}

输出结果:

小男问有烟吗?  false
小男没有获取到烟,先歇会儿
小女问外卖送到了吗?  false
小女没有饭,先歇会儿
小女开始干活儿...
小男开始干活儿...

代码的详细含义我已在注释中做了比较详细的解析,又看不懂的就留言吧,这就是关于ReentrantLock最基本的用法和一些特性,以后有时间在深入其内部写一些关于其源码层面的和实现原理方面的文章。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: ReentrantLock是Java中的一个锁类,它是一个可重入锁,允许同一个线程多次获得同一个锁。在使用ReentrantLock时,我们需要显式地获取锁和释放锁,可以通过lock()和unlock()方法来完成这些操作。 ReentrantLock采用了一种非公平的获取锁的方式,这意味着当多个线程同时请求锁时,ReentrantLock并不保证锁的获取顺序与请求锁的顺序相同。这种方式的好处是可以减少线程竞争,从而提高系统的并发性能。 另外,ReentrantLock还支持Condition条件变量,可以使用它来实现线程的等待和通知机制,以及更加灵活的线程同步和通信。 总之,ReentrantLock是Java中一个非常强大的锁类,可以帮助我们实现高效的线程同步和并发控制。但是,使用ReentrantLock也需要注意一些问题,比如需要正确地使用try-finally块来释放锁,避免死锁等问题。 ### 回答2: ReentrantLock是Java中的一种可重入锁,它提供了与synchronized关键字相似的功能,但具有更强大的扩展性和灵活性。 ReentrantLock内部使用一个同步器Sync来实现锁机制。Sync是ReentrantLock的核心组件,它有两个实现版本,分别是NonfairSync和FairSync。 NonfairSync是默认的实现版本,它采用非公平方式进行线程获取锁的竞争,即线程请求锁的时候,如果锁可用,则直接将锁分配给请求的线程,而不管其他线程是否在等待。 FairSync是公平版本,它按照线程请求锁的顺序来分配锁,当锁释放时,会优先分配给等待时间最长的线程。 ReentrantLock在实现上使用了Java的锁机制和条件变量来管理线程的等待与唤醒。当一个线程调用lock方法获取锁时,如果锁可用,线程会立即获得锁;如果锁被其他线程占用,调用线程就会被阻塞,进入等待队列。 当一个线程占用了锁之后,可以多次重复地调用lock方法,而不会引起死锁。这就是ReentrantLock的可重入性。每次重复调用lock都需要记住重入次数,每次成功释放锁时,重入次数减1,直到次数为0,锁才会被完全释放。 与synchronized相比,ReentrantLock提供了更多的高级功能。例如,可以选择公平或非公平版本的锁,可以实现tryLock方法来尝试获取锁而不会阻塞线程,可以使用lockInterruptibly方法允许线程在等待时可以被中断等等。 总之,ReentrantLock通过灵活的接口和可重入特性,提供了一种强大的同步机制,使多个线程可以安全地访问共享资源,并且具有更大的灵活性和扩展性。它在并发编程中的应用非常广泛。 ### 回答3: ReentrantLock是一种与synchronized关键字相似的线程同步工具。与synchronized相比,ReentrantLock提供了更灵活的锁操作,在并发环境中能更好地控制线程的互斥访问。 ReentrantLock的原理主要包含以下几个方面: 1. 线程控制:ReentrantLock内部维护了一个线程的等待队列,每个线程通过调用lock()方法来竞争锁资源。当一个线程成功获取到锁资源时,其他线程会被阻塞在等待队列中,直到锁被释放。 2. 重入性:ReentrantLock允许同一个线程多次获取锁资源,而不会发生死锁。这种机制称为重入性。在线程第一次获取到锁资源后,锁的计数器会加1,当该线程再次获取锁时,计数器会再次加1。而在释放锁时,计数器会递减。只有当计数器减为0时,表示锁已完全释放。 3. 公平性和非公平性:ReentrantLock可以根据需要选择公平锁或非公平锁。在公平锁模式下,等待时间最久的线程会优先获取到锁资源。而在非公平锁模式下,锁资源会被直接分配给新到来的竞争线程,不考虑等待时间。 4. 条件变量:ReentrantLock提供了Condition接口,可以创建多个条件变量,用于对线程的等待和唤醒进行管理。与传统的wait()和notify()方法相比,Condition提供了更加灵活的等待和通知机制,可以更加精确地控制线程的流程。 总的来说,ReentrantLock是通过使用等待队列、重入性、公平性和非公平性、条件变量等机制,来实现线程的互斥访问和同步。它的灵活性和粒度更高,可以更好地适应各种复杂的并发场景。但由于使用ReentrantLock需要手动进行锁的获取和释放,使用不当可能会产生死锁等问题,因此在使用时需要仔细思考和设计。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值