欢迎来到《并发王者课》,本文是该系列文章中的第14篇

黄金系列中,我们介绍了并发中一些问题,比如死锁、活锁、线程饥饿等问题。在并发编程中,这些问题无疑都是需要解决的。所以,在铂金系列文章中,我们会从并发中的问题出发,探索Java所提供的锁的能力以及它们是如何解决这些问题的。

作为铂金系列文章的第一篇,我们将从Lock接口开始介绍,因为它是Java中锁的基础,也是并发能力的基础。

一、理解Java中锁的基础:Lock接口

在青铜系列文章中,我们介绍了通过synchronized关键字实现对方法和代码块加锁的用法。然而,虽然synchronized非常好用、易用,但是它的灵活度却十分有限,不能灵活地控制加锁和释放锁的时机。所以,为了更灵活地使用锁,并满足更多的场景需要,就需要我们能够自主地定义锁。于是,就有了Lock接口

理解Lock最直观的方式,莫过于直接在JDK所提供的并发工具类中找到它,如下图所示:

可以看到,Lock接口提供了一些能力API,并有一些具体的实现,如ReentrantLock、ReentrantReadWriteLock等。

1. Lock的五个核心能力API

  • void lock():获取锁。如果当前锁不可用,则会被阻塞直至锁释放
  • void lockInterruptibly():获取锁并允许被中断。这个方法和lock()类似,不同的是,它允许被中断并抛出中断异常
  • boolean tryLock():尝试获取锁。会立即返回结果,而不会被阻塞
  • boolean tryLock(long timeout, TimeUnit timeUnit):尝试获取锁并等待一段时间。这个方法和tryLock(),但是它会根据参数等待–会,如果在规定的时间内未能获取到锁就会放弃
  • void unlock():释放锁。

2. Lock的常见实现

在Java并发工具类中,Lock接口有一些实现,比如:

  • ReentrantLock:可重入锁;
  • ReentrantReadWriteLock:可重入读写锁;

除了列举的两个实现外,还有一些其他实现类。对于这些实现,暂且不必详细了解,后面会详细介绍。在目前阶段,你需要理解的是Lock是它们的基础

二、自定义Lock

接下来,我们基于前面的示例代码,看看如何将synchronized版本的锁用Lock来实现。

 public static class WildMonster {private boolean isWildMonsterBeenKilled;public synchronized void killWildMonster() {String playerName = Thread.currentThread().getName();if (isWildMonsterBeenKilled) {System.out.println(playerName + "未斩杀野怪失败...");return;}isWildMonsterBeenKilled = true;System.out.println(playerName + "斩获野怪!");}}

1. 实现一把简单的锁

创建类WildMonsterLock并实现Lock接口,WildMonsterLock将是取代synchronized的关键:

// 自定义锁
public class WildMonsterLock implements Lock {private boolean isLocked = false;// 实现lock方法public void lock() {synchronized (this) {while (isLocked) {try {wait();} catch (InterruptedException e) {e.printStackTrace();}}isLocked = true;}}// 实现unlock方法public void unlock() {synchronized (this) {isLocked = false;this.notify();}}
}

在实现Lock接口时,你需要实现它上述的所有方法。不过,为了简化代码方便展示,我们移除了WildMonsterLock类中的tryLock等方法。

对于waitnotify方法的时候,如果你不熟悉的话,可以查看青铜系列的文章。这里需要提醒的是,notify在使用时务必要和wait是同一个监视器

基于刚才定义的WildMonsterLock,创建WildMonster类,并在方法killWildMonster中使用WildMonsterLock对象,从而取代synchronized.

// 使用刚才自定义的锁public static class WildMonster {private boolean isWildMonsterBeenKilled;public void killWildMonster() {// 创建锁对象Lock lock = new WildMonsterLock(); // 获取锁lock.lock(); try {String playerName = Thread.currentThread().getName();if (isWildMonsterBeenKilled) {System.out.println(playerName + "未斩杀野怪失败...");return;}isWildMonsterBeenKilled = true;System.out.println(playerName + "斩获野怪!");} finally {// 执行结束后,无论如何不要忘记释放锁lock.unlock();}}
}

输出结果如下:

哪吒斩获野怪!
典韦未斩杀野怪失败...
兰陵王未斩杀野怪失败...
铠未斩杀野怪失败...Process finished with exit code 0

从结果中可以看到:只有哪吒一人斩获了野怪,其他几个英雄均以失败告终,结果符合预期。这说明,WildMonsterLock达到了和synchronized一致的效果。

不过,这里有细节需要注意。在使用synchronized时我们无需关心锁的释放,JVM会帮助我们自动完成。然而,在使用自定义的锁时,一定要使用try...finally来确保锁最终一定会被释放,否则将造成后续线程被阻塞的严重后果。

2. 实现可重入的锁

synchronized中,锁是可以重入的所谓锁的可重入,指的是锁可以被线程重复或递归调用。比如,加锁对象中存在多个加锁方法时,当线程在获取到锁进入其中任一方法后,线程应该可以同时进入其他的加锁方法,而不会出现被阻塞的情况。当然,前提条件是这个加锁的方法用的是同一个对象的锁(监视器)。

在下面这段代码中,方法A和B都是同步方法,并且A中调用B. 那么,线程在调用A时已经获得了当前对象的锁,那么线程在A中调用B时可以直接调用,这就是锁的可重入性。


public class WildMonster {public synchronized void A() {B();}public synchronized void B() {doSomething...}
}

所以,为了让我们自定义的WildMonsterLock也支持可重入,我们需要对代码进行一点改动。

public class WildMonsterLock implements Lock {private boolean isLocked = false;// 重点:增加字段保存当前获得锁的线程private Thread lockedBy = null;// 重点:增加字段记录上锁次数private int lockedCount = 0;public void lock() {synchronized (this) {Thread callingThread = Thread.currentThread();// 重点:判断是否为当前线程while (isLocked && lockedBy != callingThread) {try {wait();} catch (InterruptedException e) {e.printStackTrace();}}isLocked = true;lockedBy = callingThread;lockedCount++;}}public void unlock() {synchronized (this) {// 重点:判断是否为当前线程if (Thread.currentThread() == this.lockedBy) {lockedCount--;if (lockedCount == 0) {isLocked = false;this.notify();}}}}
}

在新的WildMonsterLock中,我们增加了this.lockedBylockedCount字段,并在加锁和解锁时增加对线程的判断。在加锁时,如果当前线程已经获得锁,那么将不必进入等待。而在解锁时,只有当前线程能解锁

lockedCount字段则是为了保证解锁的次数和加锁的次数是匹配的,比如加锁了3次,那么相应的也要3次解锁。

3. 关注锁的公平性

在黄金系列文章中,我们提到了线程在竞争中可能被饿死,因为竞争并不是公平的。所以,我们在自定义锁的时候,也应当考虑锁的公平性

三、小结

以上就是关于Lock的全部内容。在本文中,我们介绍了Lock是Java中各类锁的基础。它是一个接口,提供了一些能力API,并有着完整的实现。并且,我们也可以根据需要自定义实现锁的逻辑。所以,在学习Java中各种锁的时候,最好先从Lock接口开始。同时,在替代synchronized的过程中,我们也能感受到Lock有一些synchronized所不具备的优势:

  • synchronized用于方法体或代码块,而Lock可以灵活使用,甚至可以跨越方法

  • synchronized没有公平性,任何线程都可以获取并长期持有,从而可能饿死其他线程。而基于Lock接口,我们可以实现公平锁,从而避免一些线程活跃性问题

  • synchronized被阻塞时只有等待,而Lock则提供了tryLock方法,可以快速试错,并可以设定时间限制,使用时更加灵活

  • synchronized不可以被中断,而Lock提供了lockInterruptibly方法,可以实现中断

另外,在自定义锁的时候,要考虑锁的公平性。而在使用锁的时候,则需要考虑锁的安全释放。

夫子的试炼

  • 基于Lock接口,自定义实现一把锁。

延伸阅读与参考资料

  • Locks in Java
  • 《并发王者课》大纲与更新进度总览

关于作者

关注公众号【庸人技术笑谈】,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶尔也聊聊生活和理想。不贩卖焦虑,不做标题党。

如果本文对你有帮助,欢迎点赞关注监督,我们一起从青铜到王者

铂金1:探本溯源-为何说Lock接口是Java中锁的基础相关推荐

  1. 并发王者课-铂金1:探本溯源-为何说Lock接口是Java中锁的基础

    欢迎来到<并发王者课>,本文是该系列文章中的第14篇. 在黄金系列中,我们介绍了并发中一些问题,比如死锁.活锁.线程饥饿等问题.在并发编程中,这些问题无疑都是需要解决的.所以,在铂金系列文 ...

  2. java lock的原理,Java中Lock原理探究

    在对于lock锁的使用上,很多人只是掌握了最基础的方法,但是对实现的过程不是很清楚.这里我们对lock锁功能的实现进行分析,以ReentrantLock为例,分析它的锁类型,并对相关的调用方法进行展示 ...

  3. Java高并发编程(五):Java中的锁Lock

    1. Lock接口 锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁).虽然它缺少了(通过synchr ...

  4. java.util.concurrent.locks.Lock 接口 源码

    2019独角兽企业重金招聘Python工程师标准>>> 相关类图: java.util.concurrent.locks.Lock 源码: package java.util.con ...

  5. 测试并发应用 (一)监控Lock接口

    声明:本文是< Java 7 Concurrency Cookbook >的第八章, 作者: Javier Fernández González 译者:郑玉婷   校对:方腾飞 监控Loc ...

  6. 1、Lock接口以及ReentrantLock可重入锁

    1.序 文章目录 1.序 2.Lock 接口 3.AbstractQueuedSynchronizer 3.1 双端队列 3.2 state变量 4.ReentrantLock简介以及其非公平锁模式 ...

  7. java 中lock,java中lock获取锁的四种方法

    在java接口中会存放着许多方法,方便线程使用时的直接调用.对于lock接口大家都不陌生,我们已经初步对概念进行了理解.那么在获取锁的方法上想必还不是很清楚.下面我们就lock获取锁的四种方法分别进行 ...

  8. Java多线程之Lock接口

    为什么80%的码农都做不了架构师?>>>    Lock接口通过底层框架的形式为设计更面向对象.可更加细粒度控制线程代码.更灵活控制线程通信提供了基础.实现Lock接口且使用得比较多 ...

  9. java同步锁售票_Java基础学习笔记: 多线程,线程池,同步锁(Lock,synchronized )(Thread类,ExecutorService ,Future类)(卖火车票案例)...

    学习多线程之前,我们先要了解几个关于多线程有关的概念. 进程:进程指正在运行的程序.确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能. 线程:线程是 ...

最新文章

  1. R语言使用geompointdensity包的geom_pointdensity函数将散点图和密度图结合起来、使用viridis包的scale_color_virdis函数为密度数据添加调色板色彩渐变
  2. 给jqGrid数据行添加修改和删除操作链接
  3. cinder块存储配置使用lvm
  4. mysql 语法积累
  5. 服务的默认端口_Informatica端口管理
  6. Java web小项目_个人主页(1)—— 云环境搭建与项目部署
  7. 状态压缩 + 暴力 HDOJ 4770 Lights Against Dudely
  8. 洛谷-求同构数的个数-NOIP2013提高组复赛
  9. 掌控谈话~重复对方的话
  10. 在 CentOS 5.4 下编译安装MySQL时
  11. ECMAScript 运算符--逗号运算符
  12. ip tcp udp mpeg4头结构的定义
  13. 依据经纬度解析商圈scala实现
  14. Windows远程桌面开发之九-虚拟显示器(Windows 10 Indirect Display 虚拟显示器驱动开发)
  15. 每天定投10元基金有意义吗?
  16. CC++数组练习题(头歌)朋友圈点赞
  17. 股票学习-量柱和k线-第二天
  18. 神州优车拟41亿元收购宝沃汽车67%股权
  19. C++ 高级程序设计
  20. 【pandas问题】UnicodeDecodeError: ‘utf-8‘ codec can‘t decode byte 0xca in position 0: invalid continuati

热门文章

  1. 免费一个彩虹屁机器人
  2. 使用Onvif抓取海康摄像头图片需要账号密码验证问题
  3. 基于Pythonn开发的兔子小游戏设计
  4. 分享ActionScript视频系列教程——第31讲 聊天室程序
  5. mysql端口被占用问题/无法启动
  6. java 反射Reflection;Class类
  7. ConcurrentHashMap
  8. java hessian 协议_hessian 协议 版本 兼容
  9. 苹果13用什么样的充电宝?支持苹果13的充电宝推荐
  10. iOS开发系列--通讯录、蓝牙、内购、GameCenter、iCloud、Passbook系统服务开发汇总,icloudpassbook