专栏首页汤圆学JavaJava并发-显式锁篇【可重入锁+读写锁】
原创

Java并发-显式锁篇【可重入锁+读写锁】

作者:汤圆

个人博客:javalover.cc

前言

在前面并发的开篇,我们介绍过内置锁synchronized

这节我们再介绍下显式锁Lock

显式锁包括:可重入锁ReentrantLock、读写锁ReadWriteLock

关系如下所示:

简介

显式锁和内置锁最大的区别就是:显式锁需手动获取锁和释放锁,而内置锁不需要

关于显式锁,本节会分别介绍可它的实现类 - 可重入锁,以及它的相关类 - 读写锁

  • 可重入锁,实现了显式锁,意思就是可重入的显式锁(内置锁也是可重入的)
  • 读写锁,将显式锁分为读写分离,即读读可并行,多个线程同时读不会阻塞(读写,写写还是串行)

下面让我们开始吧

文章如果有问题,欢迎大家批评指正,在此谢过啦

目录

  1. 可重入锁 ReentrantLock
  2. 读写锁 ReadWriteLock
  3. 区别

正文

1.可重入锁 ReentrantLock

我们先来看下它的几个方法:

  • public ReentrantLock();构造函数,默认构造非公平的锁(可插队,如果某个线程获取锁时,刚好锁被释放,那么这个线程就会立马获得锁,而不管队列里的线程是否在等待)
  • public void lock()获取锁,以阻塞的方式(如果其他线程持有锁,则阻塞当前线程,直到锁被释放);
  • public void lockInterruptibly() throws InterruptedException获取锁,以可被中断的方式(如果当前线程被中断,则抛出中断异常);
  • public boolean tryLock(): 尝试获取锁,如果锁被其他线程持有,则立马返回false
  • public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException:尝试获取锁,并设置一个超时时间(如果超过这个时间,还没获取到锁,则返回false)
  • public void unlock(): 释放锁 首先我们先看下它的构造方法,内部实现如下: public ReentrantLock() { sync = new NonfairSync(); } 可以看到,这里创建了一个非公平锁 公平锁:如果获取锁时,被其他线程持有,则将当前线程放入等待队列 非公平锁:如果获取锁时,刚好锁被释放,那么这个线程就会立马获得锁,而不管队列里的线程是否在等待 非公平锁的好处就是,可以减少线程的挂起和唤醒开销 如果某个线程的执行任务所需时间很短,甚至比唤醒队列中的线程所消耗的时间还短,那么非公平锁的优势就很明显 我们可以假设这样一个情景:
    • 线程A的任务执行耗时为10ms
    • 而唤醒队列中的线程B到执行真正去执行线程B的任务耗时为20ms
    • 那么当线程A去获取锁时,刚好锁又被释放,此时线程A抢先获得锁,并执行任务,然后释放锁
    • 当线程A释放锁之后,队列中当线程B才被唤醒正要去获取锁,那么线程B被唤醒的这段时间CPU就没有被浪费,从而提高了程序的性能

    这也是为啥默认是非公平锁的原因(一般情况下,非公平锁的性能高于公平锁) 那什么时候应该用公平锁呢?

    • 持有锁的时间较长,即线程的任务执行耗时较长
    • 请求锁的时间间隔较长

    因为这种情况下,如果线程插队获取到锁,结果任务还半天执行不完,那么队列中被唤醒的线程醒来发现锁还是被占有的,就会被再次放到队列中(此时并不会提高性能,还有可能降低) 接下来我们看下关键的部分:获取锁 获取锁有多个方法,我们用代码来看下他们之间的区别

    1. 先来看下lock()方法,示例代码如下:

    public class ReentrantLockDemo { ​ private Lock lock = new ReentrantLock(); ​ private int i = 0; ​ public void add(){ lock.lock(); try { i++; }finally { System.out.println(i); lock.unlock(); } } ​ public static void main(String[] args) throws InterruptedException { ReentrantLockDemo demo = new ReentrantLockDemo(); ExecutorService service = Executors.newFixedThreadPool(5); for (int i = 0; i < 100; i++) { service.submit(()->{ demo.add(); }); } } } 依次输出1~100,这是因为lock()获取锁时,会以阻塞的方式来获取

    1. 接下来看下 tryLock()方法,代码如下:

    public class ReentrantLockDemo { ​ private Lock lock = new ReentrantLock(); ​ private int i = 0; ​ public void tryAdd(){ if(lock.tryLock()){ try { i++; }finally { System.out.println(i); lock.unlock(); } } } ​ public static void main(String[] args) throws InterruptedException { ReentrantLockDemo demo = new ReentrantLockDemo(); ExecutorService service = Executors.newFixedThreadPool(5); for (int i = 0; i < 100; i++) { service.submit(()->{ demo.tryAdd(); }); } } } ​ 运行发现,输出永远都少于100,是因为tryLock()如果获取锁失败,会立马返回false,而不是阻塞等待

    1. 最后我们来看下lockInterruptibly()方法,它也是阻塞获取锁,只是比lock()多了个中断异常,即获取锁时,如果线程被中断,则抛出中断异常

    public class ReentrantLockDemo { ​ private Lock lock = new ReentrantLock(); ​ private int i = 0; ​ public void interruptAdd(){ try { lock.lockInterruptibly(); i++; } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(i); lock.unlock(); } } ​ public static void main(String[] args) throws InterruptedException { ReentrantLockDemo demo = new ReentrantLockDemo(); ExecutorService service = Executors.newFixedThreadPool(5); for (int i = 0; i < 100; i++) { // 第10次,立马关闭线程池,停止所有的线程(包括正在执行的和正在等待的) if (10 == i){ service.shutdownNow(); } service.submit(()->{ demo.interruptAdd(); }); } ​ } } ​ 多运行几次,有可能输出如下: 1 2 3 4 5 6 6 6 6 6 java.lang.InterruptedException at ...... 这就是因为前面几个都是正常获取到锁并执行了i++,但是后面的几个线程因为被突然停止,所以抛出中断异常

    1. 最后就是释放锁, unlock()

    这个就很简单了,上面的代码都有涉及到这个释放锁 不过细心的朋友可能发现了,上面的unlock()都是在finally块中编写的 这是因为在获取锁并执行任务时,有可能抛出异常,此时如果不把unlock()放到finally块中,那么锁不被释放,这在后期是一个很大的隐患(其他线程无法再次获取到这个锁,如果是lock()形式的获取锁,则线程会一直阻塞) 这也是显式锁无法完全替代内置锁的一个原因,有危险 2. 读写锁 ReadWriteLock 读写锁内部就两个方法,分别返回读锁和写锁 读锁属于共享锁,而写锁属于独占锁(前面介绍的可重入锁和内置锁也是独占锁) 读锁允许多个线程同时获取一个锁,因为读不会修改数据,它很适合读多写少的场合 下面我们用代码来看下 先看下读锁,代码如下: public class ReadWriteLockDemo { ​ private int i = 0; private Lock readLock; private Lock writeLock; ​ ​ public ReadWriteLockDemo() { ReadWriteLock lock = new ReentrantReadWriteLock(); this.readLock = lock.readLock(); this.writeLock = lock.writeLock(); } ​ public void readFun(){ readLock.lock(); System.out.println("=== 获取到 读锁 ==="); try { System.out.println(i); }finally { readLock.unlock(); System.out.println("=== 释放了 读锁 ==="); } } ​ public static void main(String[] args) throws InterruptedException { ReadWriteLockDemo demo = new ReadWriteLockDemo(); ExecutorService executors = Executors.newFixedThreadPool(2); for (int i = 0; i < 10; i++) { executors.submit(()->{ demo.readFun(); }); } } } 多次运行,有可能输出下面的结果: === 获取到 读锁 === 0 === 获取到 读锁 === 可以看到,两个线程都获取到了读锁,这就是读锁的优势,多个线程同时读 下面看下写锁,代码如下:(这里用到了ReentrantReadWriteLock类,表示可重入的读写锁) public class ReadWriteLockDemo { ​ private int i = 0; private Lock readLock; private Lock writeLock; ​ public ReadWriteLockDemo() { ReadWriteLock lock = new ReentrantReadWriteLock(); this.readLock = lock.readLock(); this.writeLock = lock.writeLock(); } ​ public void writeFun(){ writeLock.lock(); System.out.println("=== 获取到 写锁 ==="); try { i++; System.out.println(i); }finally { writeLock.unlock(); System.out.println("=== 释放了 写锁 ==="); } } ​ public static void main(String[] args) throws InterruptedException { ReadWriteLockDemo demo = new ReadWriteLockDemo(); ExecutorService executors = Executors.newFixedThreadPool(2); for (int i = 0; i < 10; i++) { executors.submit(()->{ demo.writeFun(); }); } } ​ } ​ 输出如下:可以看到,写锁类似上面的重入锁的lock()方法,阻塞获取写锁 === 获取到 写锁 === 1 === 释放了 写锁 === === 获取到 写锁 === 2 === 释放了 写锁 === === 获取到 写锁 === 3 === 释放了 写锁 === === 获取到 写锁 === 4 === 释放了 写锁 === === 获取到 写锁 === 5 === 释放了 写锁 === === 获取到 写锁 === 6 === 释放了 写锁 === === 获取到 写锁 === 7 === 释放了 写锁 === === 获取到 写锁 === 8 === 释放了 写锁 === === 获取到 写锁 === 9 === 释放了 写锁 === === 获取到 写锁 === 10 === 释放了 写锁 === 关于读写锁,需要注意的一点是,读锁和写锁必须基于同一个ReadWriteLock类才有意义 如果读锁和写锁分别是从两个ReadWrite Lock类中获取的,那么读锁和写锁就是完全无关的两个锁,也就不会起到锁的作用(阻止其他线程访问) 这就类似synchronized(a)和synchronized(b),分别锁了两个对象,此时单个线程是可以同时访问这两个锁的 3. 区别 我们用表格来展示吧,细节如下: 锁的特点内置锁可重入锁读写锁灵活性低高高公平性不确定非公平(默认)+公平非公平(默认)+公平定时性无可定时可定时中断性无可中断可中断互斥性互斥互斥读读共享,其他都互斥建议优先选择内置锁,只有在内置锁满足不了需求时,再采用显式锁(比如可定时、可中断、公平性) 如果是读多写少的场景(比如配置数据),推荐用读写锁 总结

    1. 可重入锁 ReentrantLock:需显式获取锁和释放锁,切记要在finally块中释放锁
    2. 读写锁 ReadWriteLock:基于显式锁(显式锁有的它都有),多了读写分离,实现了读读共享(多个线程同时读),其他都不共享(读写,写写)
    3. 区别:内置锁不支持手动获取/释放锁、公平性选择、定时、中断,显式锁支持

    建议使用锁时,优先考虑内置锁 因为现在内置锁的性能跟显式锁差别不大 而且显式锁因为需要手动释放锁(需在finally块中释放),所以会有忘记释放的风险 如果是读多写少的场合,则推荐用读写锁(成对的读锁和写锁需从同一个读写锁类获取) 参考内容:

    • 《Java并发编程实战》
    • 《实战Java高并发》

    后记 最后,祝愿所有人都心想事成,阖家欢乐

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 并发显式锁之读写锁

    上一篇文章我们介绍了一个显式锁,ReentrantLock ,了解到它是一个『独占式』锁,简而言之就是,

    Single
  • 并发显式锁之读写锁

    上一篇文章我们介绍了一个显式锁,ReentrantLock ,了解到它是一个『独占式』锁,简而言之就是,

    Single
  • 探索JAVA并发 - 可重入锁和不可重入锁

    CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。

    李红
  • 探索JAVA并发 - 可重入锁和不可重入锁

    第二次调用lock后线程就阻塞了,线程开始等待持有锁的线程放手,然而是它是它就是它。

    acupt
  • 浅谈Java中的锁:Synchronized、重入锁、读写锁

    Java开发必须要掌握的知识点就包括如何使用锁在多线程的环境下控制对资源的访问限制

    Java学习录
  • Java并发编程:同步锁、读写锁

    之前我们说过线程安全问题可以用锁机制来解决,即线程必要要先获得锁,之后才能进行其他操作。其实在 Java 的 API 中有这样一些锁类可以提供给我们使用,与其他...

    陈树义
  • Java并发-17.读写锁

    悠扬前奏
  • ZooKeeper 分布式锁 Curator 源码 03:可重入锁并发加锁

    在了解了加锁和锁重入之后,最需要了解的还是在分布式场景下或者多线程并发加锁是如何处理的?

    程序员小航
  • Java并发之显式锁和隐式锁比较

    在面试的过程中有可能会问到:在Java并发编程中,锁有两种实现:使用隐式锁和使用显示锁分别是什么?两者的区别是什么?所谓的显式锁和隐式锁的区别也就是说说Sync...

    凯哥Java
  • Java并发-16.重入锁

    悠扬前奏
  • Java并发之-读写锁ReentrantReadWriteLock

    之前提到的ReentrantLock是排他锁,这种锁同一时刻只允许一个线程访问,而读写锁同一时刻可以多个线程访问,但在写线程访问时,所有读线程和其他写线程都要被...

    胖虎
  • java并发之重入锁-ReentrantLock

    目前主流的锁有两种,一种是synchronized,另一种就是ReentrantLock,JDK优化到现在目前为止synchronized的性能已经和重入锁不分...

    胖虎
  • 并发编程-19AQS同步组件之重入锁ReentrantLock、 读写锁ReentrantReadWriteLock、Condition

    重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对 资源的重复加锁,而不会造成自己阻塞自己。

    小小工匠
  • Java多线程并发之读写锁

    本文主要内容:读写锁的理论;通过生活中例子来理解读写锁;读写锁的代码演示;读写锁总结。通过理论(总结)-例子-代码-然后再次总结,这四个步骤来让大家对读写锁的深...

    凯哥Java
  • 探索 JUC 之美---可重入读写锁 ReentrantReadWriteLock可重入读写锁 ReentrantReadWriteLock实现AQS只有一个状态,那么如何表示 多个读锁 与 单个写锁

    JavaEdge
  • Java并发编程之重入锁(ReentrantLock)

    ReentrantLock重入锁,由于它的加锁和解锁操作需要手动来完成,所以也称为显式锁。

    布禾
  • 死磕Java并发:J.U.C之读写锁:ReentrantReadWriteLock

    重入锁ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服务,而写服务占有的时间较少。然而读...

    程序猿DD
  • 【死磕Java并发】-----J.U.C之读写锁:ReentrantReadWriteLock

    此篇博客所有源码均来自JDK 1.8 重入锁ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供...

    芋道源码
  • 【死磕Java并发】—–J.U.C之读写锁:ReentrantReadWriteLock

    重入锁ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服务,而写服务占有的时间较少。然而读...

    用户1655470

扫码关注云+社区

领取腾讯云代金券