ReentrantLock

Posted chenshy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ReentrantLock相关的知识,希望对你有一定的参考价值。

????在了解ReentrantLock之前,我们首先回忆一下synchronized,synchronized是java内置的关键字,锁的获取和释放都是由jvm实现,因此用户就不需要显示的去释放锁,是一种独占的加锁方式,但是虽然方便,也有一定的弊端:

  • 1.当线程尝试获取锁的时候,如果获取不到锁会一直阻塞,这个阻塞的过程,用户无法控制
  • 2.如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待

????接下来我们还需要了解几个相关的概念:

  • 可重入锁:可重入锁是指同一个线程可以多次获得同一把锁;ReentrantLock和关键字Synchronized都是可重入锁
  • 可中断锁:可中断锁时只线程在获取锁的过程中,是否可以相应线程中断操作。synchronized是不可中断的ReentrantLock是可中断的
  • 公平锁和非公平锁:公平锁是指多个线程尝试获取同一把锁的时候,获取锁的顺序按照线程到达的先后顺序获取,而不是随机插队的方式获取。synchronized是非公平锁,而ReentrantLock是两种都可以实现,不过默认是非公平锁

1.ReentrantLock的使用方式

public class Demo {
    ReentrantLock lock = new ReentrantLock();
    public static int num = 0;

    public void add(){
        lock.lock();
        try{
            num++;
        }finally {
            lock.unlock();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();

        Thread t1 = new Thread(()->{
            for(int i=0;i<1000;i++){
                demo.add();
            }
        });

        Thread t2 = new Thread(()->{
            for(int i=0;i<1000;i++){
                demo.add();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(demo.num);
    }
}

使用过程:

  • 创建锁:ReentrantLock lock = new ReentrantLock();
  • 获取锁:lock.lock()
  • 释放锁:lock.unlock();
    注意释放锁要放在finally代码块中,这样能保证无论如何线程都会释放锁。

2.ReentrantLock是可重入锁

public class Demo {
    ReentrantLock lock = new ReentrantLock();
    public static int num = 0;

    public void add(){
        lock.lock();
        lock.lock();
        try{
            num++;
        }finally {
            lock.unlock();
            lock.unlock();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();

        Thread t1 = new Thread(()->{
            for(int i=0;i<1000;i++){
                demo.add();
            }
        });

        Thread t2 = new Thread(()->{
            for(int i=0;i<1000;i++){
                demo.add();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(demo.num);
    }
}

????在这里lock.lock();执行了两遍,第一次执行获取到了锁,第二次同样可以执行,所以一个线程可以多次获取同一把锁,说明ReentrantLock是可重入锁,不过在这里需要注意,获取了多少次锁,就要释放多少次锁,否则,该线程还是一直占有着该把锁,其它线程仍然会一直阻塞。因此lock()unlock()一定要是成对出现的。

2.ReentrantLock与公平锁

????大多数情况下都是非公平锁,ReentrantLock的默认构造方法是非公平锁。如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是先来的就先获取到锁。

根据源码可以知道,构造方法时加上一个true就可以创建公平锁

private final Sync sync;

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

公平锁测试:

public class FairLock {

    ReentrantLock lock = new ReentrantLock(true);

    public void add(String str){
        lock.lock();
        try{
            System.out.println(str);
        }finally {
            lock.unlock();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        FairLock fairlock = new FairLock();

        Thread t1 = new Thread(()->{
            for(int i=0;i<3;i++){
                fairlock.add("T1获取到锁");
            }
        });

        Thread t2 = new Thread(()->{
            for(int i=0;i<3;i++){
                fairlock.add("T2获取到锁");
            }
        });

        Thread t3 = new Thread(()->{
            for(int i=0;i<3;i++){
                fairlock.add("T3获取到锁");
            }
        });

        Thread t4 = new Thread(()->{
            for(int i=0;i<3;i++){
                fairlock.add("T4获取到锁");
            }
        });

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

结果:

T2获取到锁
T1获取到锁
T3获取到锁
T4获取到锁
T2获取到锁
T1获取到锁
T3获取到锁
T4获取到锁
T2获取到锁
T1获取到锁
T3获取到锁
T4获取到锁

可以看到获取锁的顺序都是:2,1,3,4,说明锁时按照先后顺序获得的。

如果改成非公平锁:把true改为false;
那么结果如下

T1获取到锁
T3获取到锁
T3获取到锁
T3获取到锁
T2获取到锁
T2获取到锁
T2获取到锁
T1获取到锁
T1获取到锁
T4获取到锁
T4获取到锁
T4获取到锁

可以看到t3可能会连续获得锁,结果是比较随机的,不公平的。为什么会出现线程连续获得锁的情况呢?当一个线程请求锁时,只要获取了同步状态即成功获取锁,在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。

4.ReentrantLock获取锁的过程是可中断的

????对于synchronized关键字,如果一个线程在等待获取锁,最终只有2种结果:

  • 1.要么获取到锁然后继续后面的操作
  • 2.要么一直等待,直到其他线程释放锁为止

????而ReentrantLock提供了另外一种可能,就是在等的获取锁的过程中(发起获取锁请求到还未获取到锁这段时间内)是可以被中断的,也就是说在等待锁的过程中,程序可以根据需要取消获取锁的请求。有些使用这个操作是非常有必要的。比如:你和好朋友越好一起去打球,如果你等了半小时朋友还没到,突然你接到一个电话,朋友由于突发状况,不能来了,那么你一定达到回府。中断操作正是提供了一套类似的机制,如果一个线程正在等待获取锁,那么它依然可以收到一个通知,被告知无需等待,可以停止工作了。

public class InterruptLockDemo extends Thread{

    private static ReentrantLock lock1 = new ReentrantLock();
    private static ReentrantLock lock2 = new ReentrantLock();

    private int threadNum;

    private String name;

    public InterruptLockDemo(String name,int threadNum){
        super(name);
        this.name = name;
        this.threadNum = threadNum;
    }


    @Override
    public void run() {
        try{
            if(threadNum==1){
                lock1.lockInterruptibly();
                TimeUnit.SECONDS.sleep(1);
                lock2.lockInterruptibly();
            }
            else{
                lock2.lockInterruptibly();
                TimeUnit.SECONDS.sleep(1);
                lock1.lockInterruptibly();
            }
        }catch (InterruptedException e){
            System.out.println("线程"+name+"被中断,中断标志位:"+this.isInterrupted());
            e.printStackTrace();
        }finally {
            // 如果lock1被当前线程拥有,就释放
            if(lock1.isHeldByCurrentThread()){
                lock1.unlock();
            }
            if(lock2.isHeldByCurrentThread()){
                lock2.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new InterruptLockDemo("t1",1);
        Thread t2 = new InterruptLockDemo("t2",2);

        t1.start();
        t2.start();
    }

????运行如上代码会产生死锁,lock1被线程t1占用,lock2倍线程t2占用,线程t1在等待获取lock2,线程t2在等待获取lock1,都在相互等待获取对方持有的锁,最终产生了死锁,如果是在synchronized关键字情况下发生了死锁现象,程序是无法结束的。使用jps和jstack,可以查看到如下信息:

Found one Java-level deadlock:
=============================
"t2":
  waiting for ownable synchronizer 0x000000076b424700, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "t1"
"t1":
  waiting for ownable synchronizer 0x000000076b424730, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "t2"

我们对上面代码改造一下,线程t2一直无法获取到lock1,那么等待5秒之后,我们中断获取锁的操作。主要修改一下main方法,如下:

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new InterruptLockDemo("t1",1);
        Thread t2 = new InterruptLockDemo("t2",2);

        t1.start();
        t2.start();

        // 等待5秒后中断t2线程
        TimeUnit.SECONDS.sleep(5);
        t2.interrupt();

    }

线程t2响应中断请求,然后释放锁

线程t2被中断,中断标志位:false
java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
    at ReentrantLock.InterruptLockDemo.run(InterruptLockDemo.java:33)

为什么中断标志位是false?t2线程调用了interrupt()方法,将线程的中断标志置为true,线程发送中断信号触发InterruptedException异常之后,中断标志将被清空。因此中断标志位变化是:false -> true -> false

  • 关于获取锁的过程中被中断,注意几点:
  1. ReentrankLock中必须使用实例方法lockInterruptibly()获取锁时,在线程调用interrupt()方法之后,才会引发 InterruptedException异常
  2. 线程调用interrupt()之后,线程的中断标志会被置为true
  3. 触发InterruptedException异常之后,线程的中断标志有会被清空,即置为false
  4. 所以当线程调用interrupt()引发InterruptedException异常,中断标志的变化是:false->true->false

5.lock,tryLock,lockInterruptibly区别

  • lock:拿不到锁就会一直阻塞。
  • tryLock:尝试获取锁,如果获取到了锁,就返回true,没有拿到就返回false。如果是带有时间的tryLock,那么没有拿到锁,就先等一段时候,超时之后还未拿到锁,就返回false。
  • lockInterruptibly:lockInterruptibly和lock一样,没有拿到锁,就被阻塞,但是如果此时有interrupt,则“此线程会被唤醒并被要求处理InterruptedException”,并且如果线程已经被interrupt,再使用lockInterruptibly的时候,此线程也会被要求处理interruptedException

获取锁的4种方法对比:
技术图片

总结

  1. ReentrantLock可以实现公平锁和非公平锁
  2. ReentrantLock默认实现的是非公平锁
  3. ReentrantLock的获取锁和释放锁必须成对出现,锁了几次,也要释放几次
    释放锁的操作必须放在finally中执行
  4. lockInterruptibly()实例方法可以相应线程的中断方法,调用线程的interrupt()方法时,lockInterruptibly()方法会触发 InterruptedException异常
  5. 关于 InterruptedException异常说一下,看到方法声明上带有 throwsInterruptedException,表示该方法可以相应线程中断,调用线程的interrupt()方法时,这些方法会触发 InterruptedException异常,触发InterruptedException时,线程的中断中断状态会被清除。所以如果程序由于调用 interrupt()方法而触发 InterruptedException异常,线程的标志由默认的false变为ture,然后又变为false
  6. 实例方法tryLock()获会尝试获取锁,会立即返回,返回值表示是否获取成功
  7. 实例方法tryLock(long timeout, TimeUnit unit)会在指定的时间内尝试获取锁,指定的时间内是否能够获取锁,都会返回,返回值表示是否获取锁成功,该方法会响应线程的中断

附上ReentrantLock源码——获取公平锁、非公平锁、释放锁:

https://www.jianshu.com/p/259d076ada14

原文链接:

https://blog.csdn.net/u013851082/article/details/70140223
https://mp.weixin.qq.com/s/gm-EQp_dP7fKHe4-AzLX0g

以上是关于ReentrantLock的主要内容,如果未能解决你的问题,请参考以下文章

ReentrantLock源码分析

Java ReEntrantLock 之 Condition条件(Java代码实战-002)

ReentrantLock使用示例

互斥锁 & 共享锁

ReentrantLock实现原理深入探究

[图解Java]ReentrantLock重入锁