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

时间:2022-12-30 20:07:06

作者:汤圆

个人博客:javalover.cc

前言

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

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

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

关系如下所示:

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

简介

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

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

  • 可重入锁,实现了显式锁,意思就是可重入的显式锁(内置锁也是可重入的)

  • 读写锁,将显式锁分为读写分离,即读读可并行,多个线程同时读不会阻塞(读写,写写还是串行)

下面让我们开始吧

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

目录

  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高并发》

后记

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

Java并发-显式锁篇【可重入锁+读写锁】的更多相关文章

  1. 探索JAVA并发 - 可重入锁和不可重入锁

    本人免费整理了Java高级资料,涵盖了Java.Redis.MongoDB.MySQL.Zookeeper.Spring Cloud.Dubbo高并发分布式等教程,一共30G,需要自己领取.传送门:h ...

  2. Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等

    Java 中15种锁的介绍 Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等,在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类 ...

  3. JAVA锁机制-可重入锁&comma;可中断锁,公平锁,读写锁,自旋锁&comma;

    如果需要查看具体的synchronized和lock的实现原理,请参考:解决多线程安全问题-无非两个方法synchronized和lock 具体原理(百度) 在并发编程中,经常遇到多个线程访问同一个 ...

  4. 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量&sol;重量级锁、读写锁、各种锁及其Java实现!

    网上关于Java中锁的话题可以说资料相当丰富,但相关内容总感觉是一大串术语的罗列,让人云里雾里,读完就忘.本文希望能为Java新人做一篇通俗易懂的整合,旨在消除对各种各样锁的术语的恐惧感,对每种锁的底 ...

  5. Java 种15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁等等…

    Java 中15种锁的介绍 1,在读很多并发文章中,会提及各种各样的锁,如公平锁,乐观锁,下面是对各种锁的总结归纳: 公平锁/非公平锁 可重入锁/不可重入锁 独享锁/共享锁 互斥锁/读写锁 乐观锁/悲 ...

  6. 写文章 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量&sol;重量级锁、读写锁、各种锁及其Java实现!

    网上关于Java中锁的话题可以说资料相当丰富,但相关内容总感觉是一大串术语的罗列,让人云里雾里,读完就忘.本文希望能为Java新人做一篇通俗易懂的整合,旨在消除对各种各样锁的术语的恐惧感,对每种锁的底 ...

  7. &OpenCurlyDoubleQuote;全栈2019”Java多线程第二十九章:可重入锁与不可重入锁详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  8. Java不可重入锁和可重入锁的简单理解

    基础知识 Java多线程的wait()方法和notify()方法 这两个方法是成对出现和使用的,要执行这两个方法,有一个前提就是,当前线程必须获其对象的monitor(俗称“锁”),否则会抛出Ille ...

  9. Java中的常见锁(公平和非公平锁、可重入锁和不可重入锁、自旋锁、独占锁和共享锁)

    公平和非公平锁 公平锁:是指多个线程按照申请的顺序来获取值.在并发环境中,每一个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个就占有锁,否者就会加入到等待队列中,以 ...

随机推荐

  1. OC基础笔记目录

    OC基础(1) Objective-C简介 OC和C对比 第一个OC程序 面向对象思想 OC基础(2) 类与对象 类的设计 第一个OC类 对象方法的声明和实现 类方法的声明和实现 OC基础(3) 对象 ...

  2. 显示 SQLite 日志

    通过在 Logcat 查看 SQL 执行语句可以帮助你调试 SQLite 问题, 使用 ADB SHELL 执行如下命令即可在 Logcat 输出 SQL 执行日志: adb shell setpro ...

  3. &OpenCurlyDoubleQuote;如何稀释scroll事件”引出的问题

    背景:我在segmentfault提了个问题如何稀释onscroll事件,问题如下: 面试时问到这个问题,是这样的:    面试官问一个关于滚动到某个位置的时候出现一个顶部的导航栏,答完之后,她接着问 ...

  4. java编程规范之java注释规范

    代码要是没有注释,对读者来说就是一堆乱七八糟的字母,为了提高代码的可读性和可维护性,必须对代码进行必要的注释,这里小编整理了一下java注释规范. (一)技巧 1:注释当前行快捷方式:ctrl+/ 2 ...

  5. TP-Link路由器无线WIFi的设置

    TP-Link路由器无线WIFi的设置.. ------------------ 确保网线接好,TP-LINK 路由器接好后,打开浏览器,在地址栏输入:192.168.1.1 输入用户名和密码(默认都 ...

  6. 2015 多校联赛 ——HDU5386(暴力)

    Sample Input 1 3 5 2 2 1 2 3 3 2 1 3 3 3 3 3 3 3 3 3 3 H 2 3 L 2 2 H 3 3 H 1 3 L 2 3   Sample Output ...

  7. 带着新人学springboot的应用05(springboot&plus;RabbitMQ 上)

    这次就来说说RabbitMQ,这个应该不陌生了,随便一查就知道这个是用来做消息队列的.(注意:这一节很多都是概念的东西,需要操作的比较少) 至于AMQP协议(Advanced Message Queu ...

  8. Table Compression

    https://docs.oracle.com/cd/E11882_01/server.112/e40540/tablecls.htm#CNCPT608

  9. eact native生成APP报错:You have not accepted the license agreements of the following SDK components&colon;

    一.报错信息 * What went wrong: A problem occurred configuring project ':app'. > You have not accepted ...

  10. oracle--合并行数据

    select to_char(wmsys.wm_concat(patname)) as WaitPatientName from (SELECT * FROM (SELECT ROW_NUMBER() ...