欢迎来到《并发王者课》,本文是该系列文章中的第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接口,自定义实现一把锁。

延伸阅读与参考资料

关于作者

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

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

并发王者课-铂金1:探本溯源-为何说Lock接口是Java中锁的基础的更多相关文章

  1. 并发王者课-铂金2:豁然开朗-“晦涩难懂”的ReadWriteLock竟如此妙不可言

    欢迎来到<并发王者课>,本文是该系列文章中的第15篇. 在上篇文章中,我们介绍了Java中锁的基础Lock接口.在本文中,我们将介绍Java中锁的另外一个重要的基本型接口,即ReadWri ...

  2. 并发王者课-铂金6:青出于蓝-Condition如何把等待与通知玩出新花样

    欢迎来到<[并发王者课](https://juejin.cn/post/6967277362455150628)>,本文是该系列文章中的**第19篇**. 在上一篇文章中,我们介绍了阻塞队 ...

  3. 并发王者课-铂金8:峡谷幽会-看CyclicBarrier如何跨越重峦叠嶂

    欢迎来到<并发王者课>,本文是该系列文章中的第21篇,铂金中的第8篇. 在上一篇文章中,我们介绍了CountDownLatch的用法.在协调多线程的开始和结束时,CountDownLatc ...

  4. 并发王者课-铂金9:互通有无-Exchanger如何完成线程间的数据交换

    欢迎来到<并发王者课>,本文是该系列文章中的第22篇,铂金中的第9篇. 在前面的文章中,我们已经介绍了ReentrantLock,CountDownLatch,CyclicBarrier, ...

  5. 并发王者课-铂金10:能工巧匠-ThreadLocal如何为线程打造私有数据空间

    欢迎来到<并发王者课>,本文是该系列文章中的第23篇,铂金中的第10篇. 说起ThreadLocal,相信你对它的名字一定不陌生.在并发编程中,它有着较高的出场率,并且也是面试中的高频面试 ...

  6. 并发王者课-青铜5:一探究竟-如何从synchronized理解Java对象头中的锁

    在前面的文章<青铜4:synchronized用法初体验>中,我们已经提到锁的概念,并指出synchronized是锁机制的一种实现.可是,这么说未免太过抽象,你可能无法直观地理解锁究竟是 ...

  7. 并发王者课 - 青铜 2:峡谷笔记 - 简单认识Java中的线程

    在前面的<兵分三路:如何创建多线程>文章中,我们已经通过Thread和Runnable直观地了解如何在Java中创建一个线程,相信你已经有了一定的体感.在本篇文章中,我们将基于前面的示例代 ...

  8. 并发王者课 - 青铜4:synchronized用法初体验

    在前面的文章<双刃剑-理解多线程带来的安全问题>中,我们提到了多线程情况下存在的线程安全问题.本文将以这个问题为背景,介绍如何通过使用synchronized关键字解这一问题.当然,在青铜 ...

  9. 并发王者课-青铜7:顺藤摸瓜-如何从synchronized中的锁认识Monitor

    在前面的文章中,我们已经体验过synchronized的用法,并对锁的概念和原理做了简单的介绍.然而,你可能已经察觉到,有一个概念似乎总是和synchronized.锁这两个概念如影相随,很多人也比较 ...

随机推荐

  1. Windows PE导出表编程2(重组导出表函数地址)

    本次要做的尝试是通过修改导出表的函数地址,实现程序功能的更改,实现这个最大的限制就是堆栈平衡问题. 先写一个DLL和EXE为了测试. DLL代码如下: 这样的话有两个导出函数(我们假设是一个密码验证之 ...

  2. apache-tomcat-7.0.92

    链接:https://pan.baidu.com/s/1wnTSjTknYfaeDV_pakrC9g 提取码:see7

  3. .NET 反射(Reflection)

    这节讲一下.NET 中的一个技术:反射(Reflection). 反射是一种很重要的技术,它可以在程序运行时,动态的获取类的实例,并调用实例中的任何方法.它就像一面镜子,映射出一个类的所有细节.    ...

  4. JAVA中Abstract到底有什么用?都用在哪些方面比较合适?

    功能向上聚合 Abstract作为抽象类和抽象方法,第一种情况是在聚合子类的通用性上起到作用,往往出现在重构过程中自然而然形成的一种层次结构-希望将多个子类的通用方法和逻辑提取到父层的抽象类. 这种重 ...

  5. 03.28,周六,12:00-17:00,ICPC训练联盟周赛,选用试题:UCF Local Programming Contest 2016正式赛。

    A. Majestic 10 题意:三个数均大于10则输出"triple-double",如果两个数大于10则输出"double-double",如果一个大于1 ...

  6. Python设计模式知多少

    设计模式 设计模式是前辈们经过相当长的一段时间的试验和错误总结出来的最佳实践.我找到的资料列举了以下这些设计模式:工厂模式.抽象工厂模式.单例模式.建造者模式.原型模式.适配器模式.桥接模式.过滤器模 ...

  7. 动态内存:delete作用于空指针

    在学习<C++primer 第五版>(中文版)中第12章动态内存与智能指针的时候遇到了一个习题,练习12.13: 练习 12.13:如果执行下面的代码,会发生什么? auto sp=mak ...

  8. VSCode配置MSVC+VSCode使用easyx库,2021.5.13日配置

    VSCode配置MSVC+VSCode使用easyx库,2021.5.13日配置~~ 想必很多人和我一样,想用vscode编程c++,easyx库不支持MinGW,一般人都是直接使用vs2019安装e ...

  9. ES6学习-1 啥是ES6啊

    我不准备刨根问底,找寻他的十八辈祖先,希望用最简单的方式能够说明白ES6是个啥,come on baby ES6的全称是ECMAScript 6.0,他于2015 年 6 月正式发布,它的目标是使得 ...

  10. MSSQL·查看DB中所有表及列的相关信息

    阅文时长 | 0.6分钟 字数统计 | 1013.6字符 主要内容 | 1.引言&背景 2.声明与参考资料 『MSSQL·查看DB中所有表及列的相关信息』 编写人 | SCscHero 编写时 ...