目录

一、Lock接口简介

二、Lock常用API

三、ReentrantLock可重入锁

四、ReadWriteLock读写锁

五、总结


一、Lock接口简介

在学习Lock同步锁之前,先来看看传统的synchronized同步锁有什么缺陷。

  • 如果一段代码被synchronized锁住,那么当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁。如果某个时刻获得锁的线程发生阻塞现象,那么这把锁会被它一直持有,而其他线程永远无法获取锁,正常来说,不能让其他线程永远在那里等待。

本篇文章将要介绍的Lock同步锁,其实跟synchronized达到的效果差不多,都是用于线程同步的,只不过,Lock具有如下一些优势:

  1. 使用Lock锁的话,提供了一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断);
  2. 使用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同步锁相关推荐

  1. 并发编程之原子性及同步锁

    并发编程之同步锁 一.概述 对之前写的Synchronized详解补充. 如果多个线程在做同一件事情的时候,会出现安全性问题: 原子性 Synchronized, AtomicXXX.Lock 可见性 ...

  2. java公平锁和非公平锁_java并发编程学习之再谈公平锁和非公平锁

    在java并发编程学习之显示锁Lock里有提过公平锁和非公平锁,我们知道他的使用方式,以及非公平锁的性能较高,在AQS源码分析的基础上,我们看看NonfairSync和FairSync的区别在什么地方 ...

  3. java 并发编程学习之二 ---- lock

    在Java中有两种方法实现锁机制,一种是在前一篇博客中([java7并发编程实战]-–线程同步机制:synchronized)介绍的synchronized,而另一种是比synchronized更加强 ...

  4. libevent c++高并发网络编程_高并发编程学习(2)——线程通信详解

    前序文章 高并发编程学习(1)--并发基础 - https://www.wmyskxz.com/2019/11/26/gao-bing-fa-bian-cheng-xue-xi-1-bing-fa-j ...

  5. 高并发编程学习(2)——线程通信详解

    前序文章 高并发编程学习(1)--并发基础 - https://www.wmyskxz.com/2019/11/26/gao-bing-fa-bian-cheng-xue-xi-1-bing-fa-j ...

  6. Java高并发编程学习(三)java.util.concurrent包

    简介 我们已经学习了形成Java并发程序设计基础的底层构建块,但对于实际编程来说,应该尽可能远离底层结构.使用由并发处理的专业人士实现的较高层次的结构要方便得多.要安全得多.例如,对于许多线程问题,可 ...

  7. java同步锁实例_Java lock同步锁使用实例解析

    这篇文章主要介绍了Java lock同步锁使用实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1)Lock是一个接口,而synchroniz ...

  8. Python 3 并发编程多进程之进程同步(锁)

    Python 3 并发编程多进程之进程同步(锁) 进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的,竞争带来的结果就是错乱,如何控制,就是加锁处理. 1. ...

  9. java并发编程学习一

    java并发编程学习一 什么是进程和线程? 进程是操作系统进行资源分配的最小单位 进程跟进程之间的资源是隔离的,同一个进程之中的线程可以共享进程的资源. 线程是进程的一个实体,是CPU 调度和分派的基 ...

  10. java中解决脏读_java并发编程学习之脏读代码示例及处理

    使用interrupt()中断线程     当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一个标志,表示它已经被中断,并立即 ...

最新文章

  1. Linux-2.6设备模型与sysfs文件系统
  2. 去掉“3_人民日报语料”中每行前边的数字编号,改成“1, 2,......”
  3. C++库文件导出可见性
  4. angular5项目端口冲突之解决办法
  5. 图解|查找数组中最大值的5种方法!
  6. testNG单元测试学习
  7. BZOJ 1007 [HNOI2008]水平可见直线 ——半平面交 凸包
  8. Computer Vision阅读文章总结纪要
  9. NC6 转库单、采购入库单、库存委托入库单签字后自动生成调拨订单
  10. “如何写好一篇学术论文?”这大概是最详实的一则攻略了!
  11. Flutter技术在会展云中大显身手
  12. 软件测试工程师,不只是你眼中的点点点
  13. Android 使用 7z 压缩字符串(工作总结)
  14. 传统情感分类方法与深度学习的情感分类方法对比
  15. 苹果手机微信端打开网页长按保存图片可以唤醒但是点击保存、发好友无效
  16. 关闭打印机和无线服务器,打印机无线连接断开了怎么办?
  17. 最大比例(辗转相除)
  18. 查找缺失的DLL工具Dependency Walker
  19. 条码打印软件如何连接激光打印机打印条码标签
  20. IBM 混合数据仓库架构介绍

热门文章

  1. 在线图片压缩png 和 gif,docsmall.com
  2. mysql-8.0.26-winx64 的下载与安装教程
  3. 虚拟机一直安装程序正在启动服务器失败,安装使用Vmware出现的问题及解决方法...
  4. mysql exporter怎么配置_prometheus mysqld_exporter监控mysql-5.7
  5. Redis Zadd 命令 Redis 有序集合(sorted set)Redis Zadd 命令用于将一个或多个成员元素及其分数值加入到有序集当中。如果某个成员已经是有序集的成员,那么更新
  6. 180.连续出现的数字
  7. 并发编程游玩---ExecutorService的isShutdown()和isTerminated(),以及一些方法的区别
  8. jar命令成功完成 java -jar 命令却提示“没有主清单属性”!
  9. 数据分析工作常见的七种错误及规避技巧
  10. 【ML小结5】决策树(ID3、C4.5、CART)