Java并发-显式锁篇【可重入锁+读写锁】
作者:汤圆
个人博客:javalover.cc
前言
在前面并发的开篇,我们介绍过内置锁synchronized
;
这节我们再介绍下显式锁Lock
显式锁包括:可重入锁ReentrantLock
、读写锁ReadWriteLock
关系如下所示:
简介
显式锁和内置锁最大的区别就是:显式锁需手动获取锁和释放锁,而内置锁不需要
关于显式锁,本节会分别介绍可它的实现类 - 可重入锁,以及它的相关类 - 读写锁
可重入锁,实现了显式锁,意思就是可重入的显式锁(内置锁也是可重入的)
读写锁,将显式锁分为读写分离,即读读可并行,多个线程同时读不会阻塞(读写,写写还是串行)
下面让我们开始吧
文章如果有问题,欢迎大家批评指正,在此谢过啦
目录
- 可重入锁 ReentrantLock
- 读写锁 ReadWriteLock
- 区别
正文
1.可重入锁 ReentrantLock
我们先来看下它的几个方法:
public ReentrantLock()
;构造函数,默认构造非公平的锁(可插队,如果某个线程获取锁时,刚好锁被释放,那么这个线程就会立马获得锁,而不管队列里的线程是否在等待)public void lock()
:获取锁,以阻塞的方式(如果其他线程持有锁,则阻塞当前线程,直到锁被释放);public void lockInterruptibly() throws InterruptedException
:获取锁,以可被中断的方式(如果当前线程被中断,则抛出中断异常);public boolean tryLock()
: 尝试获取锁,如果锁被其他线程持有,则立马返回falsepublic 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就没有被浪费,从而提高了程序的性能
这也是为啥默认是非公平锁的原因(一般情况下,非公平锁的性能高于公平锁)
那什么时候应该用公平锁呢?
- 持有锁的时间较长,即线程的任务执行耗时较长
- 请求锁的时间间隔较长
因为这种情况下,如果线程插队获取到锁,结果任务还半天执行不完,那么队列中被唤醒的线程醒来发现锁还是被占有的,就会被再次放到队列中(此时并不会提高性能,还有可能降低)
接下来我们看下关键的部分:获取锁
获取锁有多个方法,我们用代码来看下他们之间的区别
- 先来看下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()获取锁时,会以阻塞的方式来获取
- 接下来看下 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,而不是阻塞等待
- 最后我们来看下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++,但是后面的几个线程因为被突然停止,所以抛出中断异常
- 最后就是释放锁, 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. 区别
我们用表格来展示吧,细节如下:
锁的特点 | 内置锁 | 可重入锁 | 读写锁 |
---|---|---|---|
灵活性 | 低 | 高 | 高 |
公平性 | 不确定 | 非公平(默认)+公平 | 非公平(默认)+公平 |
定时性 | 无 | 可定时 | 可定时 |
中断性 | 无 | 可中断 | 可中断 |
互斥性 | 互斥 | 互斥 | 读读共享,其他都互斥 |
建议优先选择内置锁,只有在内置锁满足不了需求时,再采用显式锁(比如可定时、可中断、公平性)
如果是读多写少的场景(比如配置数据),推荐用读写锁
总结
- 可重入锁 ReentrantLock:需显式获取锁和释放锁,切记要在finally块中释放锁
- 读写锁 ReadWriteLock:基于显式锁(显式锁有的它都有),多了读写分离,实现了读读共享(多个线程同时读),其他都不共享(读写,写写)
- 区别:内置锁不支持手动获取/释放锁、公平性选择、定时、中断,显式锁支持
建议使用锁时,优先考虑内置锁
因为现在内置锁的性能跟显式锁差别不大
而且显式锁因为需要手动释放锁(需在finally块中释放),所以会有忘记释放的风险
如果是读多写少的场合,则推荐用读写锁(成对的读锁和写锁需从同一个读写锁类获取)
参考内容:
- 《Java并发编程实战》
- 《实战Java高并发》
后记
最后,祝愿所有人都心想事成,阖家欢乐
Java并发-显式锁篇【可重入锁+读写锁】的更多相关文章
- 探索JAVA并发 - 可重入锁和不可重入锁
本人免费整理了Java高级资料,涵盖了Java.Redis.MongoDB.MySQL.Zookeeper.Spring Cloud.Dubbo高并发分布式等教程,一共30G,需要自己领取.传送门:h ...
- Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等
Java 中15种锁的介绍 Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等,在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类 ...
- JAVA锁机制-可重入锁,可中断锁,公平锁,读写锁,自旋锁,
如果需要查看具体的synchronized和lock的实现原理,请参考:解决多线程安全问题-无非两个方法synchronized和lock 具体原理(百度) 在并发编程中,经常遇到多个线程访问同一个 ...
- 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!
网上关于Java中锁的话题可以说资料相当丰富,但相关内容总感觉是一大串术语的罗列,让人云里雾里,读完就忘.本文希望能为Java新人做一篇通俗易懂的整合,旨在消除对各种各样锁的术语的恐惧感,对每种锁的底 ...
- Java 种15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁等等…
Java 中15种锁的介绍 1,在读很多并发文章中,会提及各种各样的锁,如公平锁,乐观锁,下面是对各种锁的总结归纳: 公平锁/非公平锁 可重入锁/不可重入锁 独享锁/共享锁 互斥锁/读写锁 乐观锁/悲 ...
- 写文章 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!
网上关于Java中锁的话题可以说资料相当丰富,但相关内容总感觉是一大串术语的罗列,让人云里雾里,读完就忘.本文希望能为Java新人做一篇通俗易懂的整合,旨在消除对各种各样锁的术语的恐惧感,对每种锁的底 ...
- “全栈2019”Java多线程第二十九章:可重入锁与不可重入锁详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- Java不可重入锁和可重入锁的简单理解
基础知识 Java多线程的wait()方法和notify()方法 这两个方法是成对出现和使用的,要执行这两个方法,有一个前提就是,当前线程必须获其对象的monitor(俗称“锁”),否则会抛出Ille ...
- Java中的常见锁(公平和非公平锁、可重入锁和不可重入锁、自旋锁、独占锁和共享锁)
公平和非公平锁 公平锁:是指多个线程按照申请的顺序来获取值.在并发环境中,每一个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个就占有锁,否者就会加入到等待队列中,以 ...
随机推荐
- 了解PSexec
PSExec允许用户连接到远程计算机并通过命名管道执行命令.命名管道是通过一个随机命名的二进制文件建立的,该文件被写入远程计算机上的ADMIN $共享,并被SVCManager用来创建新服务. 您可以 ...
- Trie、并查集、堆、Hash表学习过程以及遇到的问题
Trie.并查集.堆.Hash表: Trie 快速存储和查找字符串集合 字符类型统一,将单词在最后一个字母结束的位置上打上标记 练习题:Trie字符串统计 import java.util.*; pu ...
- [树形DP]电子眼
电 子 眼 电子眼 电子眼 题目描述 中山市石一个环境优美.气候宜人的小城市.因为城市的交通并不繁忙,市内的道路网很稀疏.准确地说,中山市有N-1条马路和N个路口,每条马路连接两个路口,每两个路口之间 ...
- 死磕Spring之AOP篇 - Spring AOP常见面试题
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读. Spring 版本:5.1 ...
- Java刷题-tree
一.分别按照二叉树先序,中序和后序打印所有的节点. 这道题就是书上的算法思想的实际使用,唯一需要特别注意到的是用递归的方式建树,还是比较巧妙的,因为一棵树的建立过程字符流是重复使用的,用递归的方式对根 ...
- Crackme_003
功能: 拿到文件,先执行一下.功能如下: 1.nag窗口 会先出现如下nag窗口,持续几秒 2.注册窗口: 出现错误会提示:You Get Wrong Try Again 破解: 1.查壳: 无壳, ...
- 对象存储服务MinIO安装部署分布式及Spring Boot项目实现文件上传下载
目录 一.MinIO快速入门 1. MinIO简介 2. CentOS7更换成阿里云镜像 3. 安装 3.1 下载 3.2 运行测试 4. 配置脚本执行文件 4.1 创建配置执行文件 4.2 执行 二 ...
- 1. chmod命令
(一) 简介 chmod命令可以修改文件和目录的权限.控制文件或目录的,读,写,执行权限. 可以采用数字或字符的方式对文件或目录的权限进行变更. 通过命令 ls -l 查看到的9位权限位,rw- ...
- 次小生成树 详解及模板 (仅kruskal)
思路 关于次小生成树,首先求出最小生成树,然后枚举每条不在最小生成树上的边(在原本的节点上添加一个vis属性进行判断即可),并把这条边放到最小生成树上面,然后就一定会形成环,那么我们在这条环路中取出一 ...
- JAVAEE_Servlet_11_GetAndPost
Get请求和Post请求 * Get请求 和 Post请求各方面分析 - 什么情况下浏览器发送的是Get请求? 1. 通过浏览器的地址栏输入地址,所访问的URL都是get请求,如果以post定义,那么 ...