java并发- 什么是可重入锁 ReentrantLock

一、什么是ReentrantLock

通常情况下,锁可以用来控制多线程的访问行为。那对于同一个线程,如果连续两次对同一把锁进行lock。对于一般的锁来说,这个线程就会被永远卡死在那边,比如:

void handle() {
    lock();
    lock();  //和上一个lock()操作同一个锁对象,那么这里就永远等待了
    unlock();
    unlock();
}

这个特性相当不好用,因为在实际的开发过程中,函数之间的调用关系可能错综复杂,一个不小心就可能在多个不同的函数中,反复调用lock(),这样的话,线程就自己和自己卡死了。

重入锁就是用来解决这个问题的。重入锁使得同一个线程可以对同一把锁,在不释放的前提下,反复加锁,而不会导致线程卡死。你唯一需要保证的,就是unlock()的次数和lock()一样多。

Java中的重入锁

可重入锁
参考URL: https://mp.weixin.qq.com/s/GDno-X1N8zc98h9MZ8_KoA

Java中的锁都来自与Lock接口。

ReentrantLock 重入锁提供的最重要的方法就是lock()

  • void lock():加锁,如果锁已经被别人占用了,就无限等待。
    这个lock()方法,提供了锁最基本的功能,拿到锁就返回,拿不到就等待。因此,大规模得在复杂场景中使用,是有可能因此死锁的。因此,使用这个方法得非常小心。

  • boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException:尝试获取锁,等待timeout时间。同时,可以响应中断。
    这是一个比单纯lock()更具有工程价值的方法,如果大家阅读过JDK的一些内部代码,就不难发现,tryLock()在JDK内部被大量的使用。

与lock()相比,tryLock()至少有下面一个好处:

  1. 可以不用进行无限等待。直接打破形成死锁的条件。如果一段时间等不到锁,可以直接放弃,同时释放自己已经得到的资源。这样,就可以在很大程度上,避免死锁的产生。因为线程之间出现了一种谦让机制。
  2. 可以在应用程序这层进行进行自旋,你可以自己决定尝试几次,或者是放弃。
  3. 等待锁的过程中可以响应中断,如果此时,程序正好收到关机信号,中断就会触发,进入中断异常后,线程就可以做一些清理工作,从而防止在终止程序时出现数据写坏,数据丢失等悲催的情况。
    当然了,当锁使用完后,千万不要忘记把它释放了。不然,程序可能就会崩溃啦~
  • void unlock() :释放锁
    此外, 重入锁还有一个不带任何参数的tryLock()。
  • public boolean tryLock()
    这个不带任何参数的tryLock()不会进行任何等待,如果能够获得锁,直接返回true,如果获取失败,就返回false,特别适合在应用层自己对锁进行管理,在应用层进行自旋等待。

重入锁的实现原理

可重入锁
参考URL: https://mp.weixin.qq.com/s/GDno-X1N8zc98h9MZ8_KoA

重入锁内部实现的主要类如下图:
在这里插入图片描述重入锁的核心功能委托给内部类Sync实现,并且根据是否是公平锁有FairSync和NonfairSync两种实现。这是一种典型的策略模式。

实现重入锁的方法很简单,就是基于一个状态变量state。这个变量保存在AbstractQueuedSynchronizer对象中

private volatile int state;

当这个state==0时,表示锁是空闲的,大于零表示锁已经被占用, 它的数值表示当前线程重复占用这个锁的次数。因此,lock()的最简单的实现是:

final void lock() {
 // compareAndSetState就是对state进行CAS操作,如果修改成功就占用锁
 if (compareAndSetState(0, 1))
     setExclusiveOwnerThread(Thread.currentThread());
 else
 //如果修改不成功,说明别的线程已经使用了这个锁,那么就可能需要等待
     acquire(1);
}

下面是acquire() 的实现:

 public final void acquire(int arg) {
 //tryAcquire() 再次尝试获取锁,
 //如果发现锁就是当前线程占用的,则更新state,表示重复占用的次数,
 //同时宣布获得所成功,这正是重入的关键所在
 if (!tryAcquire(arg) &&
     // 如果获取失败,那么就在这里入队等待
     acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
     //如果在等待过程中 被中断了,那么重新把中断标志位设置上
     selfInterrupt();
}

公平的重入锁
**默认情况下,重入锁是不公平的。**什么叫不公平呢。也就是说,如果有1,2,3,4 这四个线程,按顺序,依次请求锁。那等锁可用的时候,谁会先拿到锁呢?在非公平情况下,答案是随机的。如下图所示,可能线程3先拿到锁。

在这里插入图片描述如果你是一个公平主义者,强烈坚持先到先得的话,那么你就需要在构造重入锁的时候,指定这是一个公平锁:

ReentrantLock fairLock = new ReentrantLock(true);

这样一来,每一个请求锁的线程,都会乖乖的把自己放入请求队列,而不是上来就进行争抢。但一定要注意,公平锁是有代价的。维持公平竞争是以牺牲系统性能为代价的。

那公平锁和非公平锁实现的核心区别在哪里呢?来看一下这段lock()的代码:

//非公平锁 
 final void lock() {
     //上来不管三七二十一,直接抢了再说
     if (compareAndSetState(0, 1))
         setExclusiveOwnerThread(Thread.currentThread());
     else
         //抢不到,就进队列慢慢等着
         acquire(1);
 }

 //公平锁
 final void lock() {
     //直接进队列等着
     acquire(1);
 }

从上面的代码中也不难看到,非公平锁如果第一次争抢失败,后面的处理和公平锁是一样的,都是进入等待队列慢慢等。

Condition

Condition可以理解为重入锁的伴生对象。**它提供了在重入锁的基础上,进行等待和通知的机制。**可以使用 newCondition()方法生成一个Condition对象,如下所示。

private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();

那Condition对象怎么用呢。在JDK内部就有一个很好的例子。让我们来看一下ArrayBlockingQueue吧。ArrayBlockingQueue是一个队列,你可以把元素塞入队列(enqueue),也可以拿出来take()。但是有一个小小的条件,就是如果队列是空的,那么take()就需要等待,一直等到有元素了,再返回。那这个功能,怎么实现呢?这就可以使用Condition对象了。

实现在ArrayBlockingQueue中,就维护一个Condition对象

lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();

这个notEmpty 就是一个Condition对象。它用来通知其他线程,ArrayBlockingQueue是不是空着的。
在这里插入图片描述

二、怎么使用

彻底理解ReentrantLock可重入锁的使用
参考URL: https://zhuanlan.zhihu.com/p/88884729

相关阿里规范

【强制】 在使用阻塞等待获取锁的方式中,必须在 try 代码块之外并且在加锁方法与 try 代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁。
说明一:如果在 lock 方法与 try 代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功
获取锁。
说明二:如果 lock 方法在 try 代码块之内,可能由于其它方法抛出异常,导致在 finally 代码块中,unlock
对未加锁的对象解锁,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),抛出
IllegalMonitorStateException 异常。
说明三:在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常,产生的后果与说明二相同。

正例:
Lock lock = new XxxLock();
// ...
lock.lock();
try {
    doSomething();
    doOthers();
} finally {
    lock.unlock();
}


反例:
Lock lock = new XxxLock();

try {
    // 如果此处抛出异常,则直接执行 finally 代码块
    doSomething();
    // 无论加锁是否成功,finally 代码块都会执行
    lock.lock();
    doOthers();
} finally {
    lock.unlock();
}

总结: 很好,请仔细理解和体会!记得 lock.lock(); 放在try前,中间尽量不要有其它代码。

【强制】 在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。
说明:Lock 对象的 unlock 方法在执行时,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),如果
当前线程不持有锁,则抛出 IllegalMonitorStateException 异常。

正例:
Lock lock = new XxxLock();
// ...
boolean isLocked = lock.tryLock();
if (isLocked) {
    try {
        doSomething();
        doOthers();
    } finally {
        lock.unlock();
    }
}

基本使用demo

ReentrantLock 提供以下重要的方法

  • lock():获得锁,如果锁已被占用,则等待
  • lockInterruptibly():获得锁,但有限响应中断
  • unlock():释放锁
  • tryLock():尝试获取锁。如果获得,返回true;否则返回false
  • tryLock(long time, TimeUnit unit):在给定时间内获得锁。如果获得返回true;否则返回false

在ReentrantLock 中,lock()方法是一个无条件的锁,与synchronize意思差不多,但是另一个方法 tryLock()方法只有在成功获取了锁的情况下才会返回true,如果别的线程当前正持有锁,则会立即返回false!如果为这个方法加上timeout参数,则会在等待timeout的时间才会返回false或者在获取到锁的时候返回true。

demo1: 简单使用
public class ReentrantLockTest {

    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                test();
            }
        },"线程A").start();

        new Thread(() -> test(),"线程B").start();

    }
    public static void  test()  {

        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"获取了锁");
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            System.out.println(Thread.currentThread().getName()+"释放了锁");
            lock.unlock();
        }


    }

}
demo2: 公平锁实现

公平锁的含义:就是谁等的时间最长,谁就先获取锁。

首先new一个ReentrantLock的时候参数为true,表明实现公平锁机制。在这里我们多定义几个线程ABCDE,然后在test方法中循环执行了两次加锁和解锁的过程

public class ReentrantLockTest2 {
    private static final Lock lock = new ReentrantLock(true);
    public static void main(String[] args) {
        new Thread(() -> test(),"线程A").start();
        new Thread(() -> test(),"线程B").start();
        new Thread(() -> test(),"线程C").start();
        new Thread(() -> test(),"线程D").start();
        new Thread(() -> test(),"线程E").start();
    }
    public static void  test()  {
        for(int i=0;i<2;i++) {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName()+"获取了锁");
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
}

三、参考

ReentrantLock笔记(一) 重入锁应用
参考URL: https://www.cnblogs.com/coloz/p/12821218.html
可重入锁
参考URL: https://mp.weixin.qq.com/s/GDno-X1N8zc98h9MZ8_KoA

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

西京刀客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值