并发编程学习之Lock同步锁
目录
一、Lock接口简介
二、Lock常用API
三、ReentrantLock可重入锁
四、ReadWriteLock读写锁
五、总结
一、Lock接口简介
在学习Lock同步锁之前,先来看看传统的synchronized同步锁有什么缺陷。
- 如果一段代码被synchronized锁住,那么当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁。如果某个时刻获得锁的线程发生阻塞现象,那么这把锁会被它一直持有,而其他线程永远无法获取锁,正常来说,不能让其他线程永远在那里等待。
本篇文章将要介绍的Lock同步锁,其实跟synchronized达到的效果差不多,都是用于线程同步的,只不过,Lock具有如下一些优势:
- 使用Lock锁的话,提供了一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断);
- 使用Lock锁的话,通过tryLock()方法可以尝试获取锁,可以判断线程有没有成功获取到锁;
Lock是一个接口,接口声明代码如下:
public interface Lock {//获取锁,如果锁已被其他线程获取,则进行等待。void lock();//当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态void lockInterruptibly() throws InterruptedException;//尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回falseboolean tryLock();//指定时间内尝试获取锁,在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回falseboolean tryLock(long time, TimeUnit unit) throws InterruptedException;//释放锁void unlock();//创建ConditionCondition newCondition();
}
主要的方法有:lock()获取锁、tryLock()尝试获取锁、unlock()手动释放锁,注意使用Lock一定要手动释放锁,一般放在finally块中保证锁得到释放;
二、Lock常用API
(1)、如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{ xxx }....catch{ xxx }...finally { xxx }块中使用,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。下面是JDK官网推荐的写法:
Lock l = ...;l.lock();try {// access the resource protected by this lock} finally {l.unlock();}
(2)、尝试获取锁tryLock()
Lock lock = ...;
if(lock.tryLock()) {try{//处理业务}catch(Exception ex){}finally{//手动释放lock.unlock(); }
} else {//未成功获取锁,则处理其他业务逻辑
}
三、ReentrantLock可重入锁
ReentrantLock是可重入锁,也是经常使用到的同步锁之一。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。ReentrantLock的类图如下:
ReentrantLock接口声明如下:
public class ReentrantLock implements Lock, java.io.Serializable {//...
}
下面通过一个简单的示例说明ReentrantLock的使用方法。我们模拟三个窗口卖30张火车票,如果使用Synchronized同步的话,比较简单,这里就不过多介绍了,我们使用ReentrantLock可重入锁来实现。
/*** 模拟三个售票员卖出30张票*/
public class T01_SaleTicketDemo01 {public static void main(String[] args) {//共享资源Ticket ticket = new Ticket();//窗口1new Thread(ticket::sale, "A").start();//窗口2new Thread(ticket::sale, "B").start();//窗口3new Thread(ticket::sale, "C").start();}
}/*** 共享资源类*/
class Ticket {private int number = 10;//声明可重入锁private Lock lock = new ReentrantLock();public void sale() {//获取锁lock.lock();try {while (true) {if (number > 0) {TimeUnit.MILLISECONDS.sleep(10);System.out.println(Thread.currentThread().getName() + "卖出了:" + (number--) + ",剩下:" + number);} else {break;}}} catch (InterruptedException e) {e.printStackTrace();} finally {//必须手动释放锁,且放在finally块中lock.unlock();}}
}
运行结果如下:
A卖出了:10,剩下:9
A卖出了:9,剩下:8
B卖出了:8,剩下:7
C卖出了:7,剩下:6
A卖出了:6,剩下:5
A卖出了:5,剩下:4
A卖出了:4,剩下:3
A卖出了:3,剩下:2
A卖出了:2,剩下:1
A卖出了:1,剩下:0
我们可以看到,三个线程只要有一个线程获取了锁,其他线程就只能等待去操作共享资源,保证了线程安全。
四、ReadWriteLock读写锁
ReadWriteLock也是一个接口,声明如下:
public interface ReadWriteLock {/*** 获取读锁*/Lock readLock();/*** 获取写锁,属于排他锁*/Lock writeLock();
}
上面两个方法一个用来获取读锁,一个用来获取写锁,将读和写的锁的操作分开,正常来说,读操作应该允许多个线程同时读,当一个线程写的时候,其他线程不能写也不能读。常见的实现类就是ReentrantReadWriteLock读写锁:
可见,ReentrantReadWriteLock里面分了两把锁:readerLock和writerLock。下面通过例子来介绍一下ReentrantReadWriteLock具体用法:
/*** ReentrantReadWriteLock读写锁* <p>* 读操作共享,写操作独占* 即读读可共享、写读写写要独占*/
public class T11_ReentrantReadWriteLock {public static void main(String[] args) {//操作共享资源SharedData sharedData = new SharedData();//五个线程写for (int i = 1; i <= 5; i++) {final int num = i;new Thread(() -> {sharedData.put(String.valueOf(num), String.valueOf(num));}, "A" + num).start();}//五个线程读for (int i = 1; i <= 5; i++) {final int num = i;new Thread(() -> {sharedData.get(String.valueOf(num));}, "A" + num).start();}}
}class SharedData {private volatile Map<String, Object> map = new HashMap<>();/*** 模拟写操作*/public void put(String key, Object value) {System.out.println(Thread.currentThread().getName() + "写入开始....");try {TimeUnit.MILLISECONDS.sleep(400);} catch (InterruptedException e) {e.printStackTrace();}map.put(key, value);System.out.println(Thread.currentThread().getName() + "写入完成....value = " + String.valueOf(value));}/*** 模拟读操作*/public Object get(String key) {System.out.println(Thread.currentThread().getName() + "读取开始....");try {TimeUnit.MILLISECONDS.sleep(400);} catch (InterruptedException e) {e.printStackTrace();}Object result = map.get(key);System.out.println(Thread.currentThread().getName() + "读取完成....result = " + result);return result;}
}
运行结果:
A1写入开始....
A2写入开始....
A3写入开始....
A4写入开始....
A5写入开始....
A1读取开始....
A2读取开始....
A3读取开始....
A4读取开始....
A5读取开始....
A1读取完成....result = null
A2读取完成....result = null
A3读取完成....result = null
A4读取完成....result = null
A3写入完成....value = 3
A2写入完成....value = 2
A1写入完成....value = 1
A5读取完成....result = 5
A5写入完成....value = 5
A4写入完成....value = 4
从上面的结果可以看出,比如A1开始写入数据,到A1写入成功期间,居然那么多线程对共享资源进行了操作, 这明显有问题。正确结果应该是A1开始写入数据紧接着就是A1写入成功...依次类推。下面我们使用ReentrantReadWriteLock改造一下:
/*** ReentrantReadWriteLock读写锁* <p>* 读操作共享,写操作独占* 即读读可共享、写读写写要独占*/
public class T11_ReentrantReadWriteLock {public static void main(String[] args) {//操作共享资源SharedData sharedData = new SharedData();//五个线程写for (int i = 1; i <= 5; i++) {final int num = i;new Thread(() -> {sharedData.put(String.valueOf(num), String.valueOf(num));}, "A" + num).start();}//五个线程读for (int i = 1; i <= 5; i++) {final int num = i;new Thread(() -> {sharedData.get(String.valueOf(num));}, "A" + num).start();}}
}class SharedData {private volatile Map<String, Object> map = new HashMap<>();/*** 引入读写锁*/private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();/*** 模拟写操作*/public void put(String key, Object value) {readWriteLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName() + "写入开始....");try {TimeUnit.MILLISECONDS.sleep(400);} catch (InterruptedException e) {e.printStackTrace();}map.put(key, value);System.out.println(Thread.currentThread().getName() + "写入完成....value = " + value);} catch (Exception e) {e.printStackTrace();} finally {readWriteLock.writeLock().unlock();}}/*** 模拟读操作*/public Object get(String key) {readWriteLock.readLock().lock();Object result = null;try {System.out.println(Thread.currentThread().getName() + "读取开始....");try {TimeUnit.MILLISECONDS.sleep(400);} catch (InterruptedException e) {e.printStackTrace();}result = map.get(key);System.out.println(Thread.currentThread().getName() + "读取完成....result = " + result);} catch (Exception e) {e.printStackTrace();} finally {readWriteLock.readLock().unlock();}return result;}
}
运行结果:
A1写入开始....
A1写入完成....value = 1
A2写入开始....
A2写入完成....value = 2
A3写入开始....
A3写入完成....value = 3
A4写入开始....
A4写入完成....value = 4
A5写入开始....
A5写入完成....value = 5
A1读取开始....
A2读取开始....
A3读取开始....
A4读取开始....
A5读取开始....
A1读取完成....result = 1
A3读取完成....result = 3
A2读取完成....result = 2
A5读取完成....result = 5
A4读取完成....result = 4
可以看到,多个线程只能有一个线程进行写操作,其他线程不能加塞,而读操作则允许多个线程同时读,这样就大大提升了读操作的效率。
注意:
如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待其他线程释放读锁;
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁;
五、总结
本篇文章主要总结了Lock同步锁的基本使用方法,并介绍了两个常见的同步锁实现类:RenentrantLock和ReentrantReadWriteLock的简单使用。这里总结一下Lock同步锁和Synchronized实现同步的区别:
- Lock是一个接口,而synchronized是Java中的关键字;
- Lock在代码执行时出现异常时不会自动释放锁,必须手动释放,而synchronized抛出异常时会自动释放锁;
- Lock如果没有及时释放锁,很可能产生死锁现象,而synchronized由于会自动释放锁,不会导致死锁现象发生;
- Lock可以让等待锁的线程响应中断,而synchronized不能响应中断,只能一直等待;
- Lock通过tryLock尝试获取锁可知是否成功获取锁,而synchronized则不行;
- 如果对共享资源竞争不激烈情况下,两者的性能其实差不多,但是如果竞争激烈,此时Lock的性能要远远优于synchronized方式;
并发编程学习之Lock同步锁相关推荐
- 并发编程之原子性及同步锁
并发编程之同步锁 一.概述 对之前写的Synchronized详解补充. 如果多个线程在做同一件事情的时候,会出现安全性问题: 原子性 Synchronized, AtomicXXX.Lock 可见性 ...
- java公平锁和非公平锁_java并发编程学习之再谈公平锁和非公平锁
在java并发编程学习之显示锁Lock里有提过公平锁和非公平锁,我们知道他的使用方式,以及非公平锁的性能较高,在AQS源码分析的基础上,我们看看NonfairSync和FairSync的区别在什么地方 ...
- java 并发编程学习之二 ---- lock
在Java中有两种方法实现锁机制,一种是在前一篇博客中([java7并发编程实战]-–线程同步机制:synchronized)介绍的synchronized,而另一种是比synchronized更加强 ...
- libevent c++高并发网络编程_高并发编程学习(2)——线程通信详解
前序文章 高并发编程学习(1)--并发基础 - https://www.wmyskxz.com/2019/11/26/gao-bing-fa-bian-cheng-xue-xi-1-bing-fa-j ...
- 高并发编程学习(2)——线程通信详解
前序文章 高并发编程学习(1)--并发基础 - https://www.wmyskxz.com/2019/11/26/gao-bing-fa-bian-cheng-xue-xi-1-bing-fa-j ...
- Java高并发编程学习(三)java.util.concurrent包
简介 我们已经学习了形成Java并发程序设计基础的底层构建块,但对于实际编程来说,应该尽可能远离底层结构.使用由并发处理的专业人士实现的较高层次的结构要方便得多.要安全得多.例如,对于许多线程问题,可 ...
- java同步锁实例_Java lock同步锁使用实例解析
这篇文章主要介绍了Java lock同步锁使用实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1)Lock是一个接口,而synchroniz ...
- Python 3 并发编程多进程之进程同步(锁)
Python 3 并发编程多进程之进程同步(锁) 进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的,竞争带来的结果就是错乱,如何控制,就是加锁处理. 1. ...
- java并发编程学习一
java并发编程学习一 什么是进程和线程? 进程是操作系统进行资源分配的最小单位 进程跟进程之间的资源是隔离的,同一个进程之中的线程可以共享进程的资源. 线程是进程的一个实体,是CPU 调度和分派的基 ...
- java中解决脏读_java并发编程学习之脏读代码示例及处理
使用interrupt()中断线程 当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一个标志,表示它已经被中断,并立即 ...
最新文章
- Linux-2.6设备模型与sysfs文件系统
- 去掉“3_人民日报语料”中每行前边的数字编号,改成“1, 2,......”
- C++库文件导出可见性
- angular5项目端口冲突之解决办法
- 图解|查找数组中最大值的5种方法!
- testNG单元测试学习
- BZOJ 1007 [HNOI2008]水平可见直线 ——半平面交 凸包
- Computer Vision阅读文章总结纪要
- NC6 转库单、采购入库单、库存委托入库单签字后自动生成调拨订单
- “如何写好一篇学术论文?”这大概是最详实的一则攻略了!
- Flutter技术在会展云中大显身手
- 软件测试工程师,不只是你眼中的点点点
- Android 使用 7z 压缩字符串(工作总结)
- 传统情感分类方法与深度学习的情感分类方法对比
- 苹果手机微信端打开网页长按保存图片可以唤醒但是点击保存、发好友无效
- 关闭打印机和无线服务器,打印机无线连接断开了怎么办?
- 最大比例(辗转相除)
- 查找缺失的DLL工具Dependency Walker
- 条码打印软件如何连接激光打印机打印条码标签
- IBM 混合数据仓库架构介绍
热门文章
- 在线图片压缩png 和 gif,docsmall.com
- mysql-8.0.26-winx64 的下载与安装教程
- 虚拟机一直安装程序正在启动服务器失败,安装使用Vmware出现的问题及解决方法...
- mysql exporter怎么配置_prometheus mysqld_exporter监控mysql-5.7
- Redis Zadd 命令 Redis 有序集合(sorted set)Redis Zadd 命令用于将一个或多个成员元素及其分数值加入到有序集当中。如果某个成员已经是有序集的成员,那么更新
- 180.连续出现的数字
- 并发编程游玩---ExecutorService的isShutdown()和isTerminated(),以及一些方法的区别
- jar命令成功完成 java -jar 命令却提示“没有主清单属性”!
- 数据分析工作常见的七种错误及规避技巧
- 【ML小结5】决策树(ID3、C4.5、CART)