主要内容

1. synchronized介绍
2. ReentrantLock介绍
3. ReentrantLock和synchronized的可伸缩性比较
4. Condition变量
5. ReentrantLock是公平的吗?
6. ReentrantLock这么完美吗?
7. 不要放弃synchronized
8. 什么时候选择ReentrantLock?

多线程和并发并不是什么新鲜事物,但是,Java是第一个把支持跨平台线程模型和内存模型直接纳入语言规范的主流编程语言。

诸如,在类库里有用于创建、启动、操作线程的Thread类,在语言特性上有用于多线程之间协作的synchronizedvolatile

它简化了跨平台并发程序的开发,但并不意味着编写并发应用就变得非常容易。

synchronized介绍

把一个代码块声明为synchronized,会产生两个重要的效果,原子性可见性(atomicity,visibility)。

原子性意味着同一时刻只能有一个线程执行这段被monitor对象(锁)保护的代码,从而可以防止多线程并发修改共享变量时产生冲突。

可见性更微妙一些,它解决了由内存缓存和编译器优化造成的不确定性。

平时,线程采用自己的方式自由地存储变量,不需要关心该变量对其他线程是否立即可见(变量可能在寄存器中、处理器特定的缓存中,经过了指令重排或其他编译器优化)。

但是如果开发者使用了同步,如下面的代码所示,那么当一个线程对变量更新后,synchronized能够保证在该线程退出同步代码块之前,该更新对之后持有相同monitor进入同步快的线程立即可见。(volatile变量也存在类似的规则。)

synchronized (lockObject) {
// update object state
}

因此,同步可以确保可靠地更新多个共享变量而不会发生竞态条件或数据不一致,并可以保证其他线程可以看到最新的值。

有了明确的跨平台内存模型定义(JDK5.0中做了修改,修复了最初定义中的某些错误),就可以保证并发类可以实现"Write Once, Run Anywhere"。

并发类需遵循以下规则:如果你更新的变量可能被另一个线程读取,或者相反的,你要读取另一个线程更新的变量,都必须进行同步。

顺便提一下,在最新的JVM中(JDK5),无竞争的同步(当锁被持有时,没有其他线程试图获取锁)的性能还是不错的。

改进synchronized

所以同步听起来不错,对吗?那么,为什么JSR 166小组花了这么多时间来开发java.util.concurrent.lock框架呢?

答案很简单,同步是好,但不够完美。它有一些功能上的限制,无法中断正在等待获取锁的线程,也无法轮询锁或者尝试获取锁而又不想一直等待。

同步还要求在获取锁的同一栈帧中释放锁,这在大多数情况下是正确的做法(并能与异常处理很好地交互),

但是在少数情况下可能更需要非块结构的锁。(原文是non-block-structured locking,是指不是synchronized代码块形式的锁)

ReentrantLock 类

java.util.concurrent.lock中的Lock框架是对锁的抽象,它允许锁作为一个普通的Java类来实现,而不是Java语言的特性(与之对应的是synchronized关键字)。

它给锁的不同实现留出了空间,你可以实现具有不同调度算法、不同性能特性的锁,甚至不同的锁的语义。

ReentrantLock类就是Lock抽象的一个实现,它具有与synchronized相同的并发性和内存语义,但是它还添加了诸如锁轮训,定时等待,以及等待可中断的特性。

此外,在竞争激烈的情况下,它有更好的性能表现。(换句话说,当多个线程尝试访问共享资源时,JVM将花费更少的时间来调度线程,而将更多的时间用于执行程序。)

那么可重入锁(reentrant lock)是什么意思?简单地说,每个锁都有一个与之关联的计数器,如果线程再次获取它,计数器就加1,然后需要释放两次才能真正释放该锁。

这和synchronized的语义是相似的。如果线程通过已持有的monitor进入了另一个同步块(例如在一个同步方法中进入了另一个同步方法),该线程被允许执行,但是该线程退出第二个同步块时,monitor不会被释放,只有继续退出第一个同步块后,才能真正的释放monitor。

在清单1的代码示例中,Lock和synchronized最大不同就表现出来了——Lock必须在finally中显示释放。否则,如果同步的代码引发异常,则该锁可能永远不会释放!

这种区别听起来似乎微不足道,但实际上,它非常重要。忘记在finally块中释放锁会在程序中埋下定时炸弹,当它最终炸毁您的程序时,你将很难追根溯源。

然而,使用synchronized,JVM确保锁会被自动释放。

Listing 1. Protecting a block of code with ReentrantLock.

Lock lock = new ReentrantLock();
lock.lock();
try {
// update object state
}
finally {
lock.unlock();
}

另外,与当前的synchronized实现相比,ReentrantLock的实现在锁竞争下具有更好的可伸缩性。 (在将来的JVM版本中,synchronized的竞争性能可能会有所改善。)

这意味着,当多线程都争用同一个锁时,使用ReentrantLock会获得更好的吞吐量。

ReentrantLock和synchronized的可伸缩性比较

Tim Peierls(《Java并发编程实战》作者)使用简单的线性同余伪随机数生成器(PRNG)构建了一个简单的基准,用于测量synchronized与Lock的相对可伸缩性。

这个示例很好,因为每次调用nextRandom()时,PRNG实际上都会做一些实际工作,因此该基准测试是合理的,符合实际应用的,而不是通过睡眠计时模拟或不做任何事情。

在此基准测试中,我们有一个PseudoRandom接口,接口中只有一个方法nextRandom(int bound)。该接口与java.util.Random类的功能非常相似。

因为PRNG将上一次生成的数作为下一次生成随机数的输入,并且将上一次生成的数作为实例变量进行维护,所以很重要的一点是,更新该变量的代码块不能被其他线程抢占,

因此我们需要某种形式的锁来确保这一点。 (java.util.Random也是这么做的)

我们分别用ReentrantLock和synchronized实现了两个PseudoRandom。主程序会产生许多线程,每个线程都疯狂地掷骰子,然后计算不同版本每秒能够掷多少次骰子。

图1和图2中是不同线程数下的测试结果。

该基准测试并不完美,它仅在两个系统上运行(具有超线程的dual Xeon运行Linux,一个单处理器Windows系统),但应该足以表明ReentrantLock比同步具有更好的可伸缩性。

Figure 1. Throughput for synchronization and Lock, single CPU

Figure 2. Throughput (normalized) for synchronization and Lock, four CPUs



图1和图2显示了两种实现的每秒吞吐量(已标准化为1个线程同步的情况)。

可以看到,两种实现在稳态吞吐量(steady-state)上都相对较快地收敛,这通常意味着处理器已得到充分利用。

你也许已经注意到,无论哪种情况的竞争,synchronized版本的性能都会显著恶化,而Lock版本在调度开销上花费的时间要少得多,从而为更高的吞吐量和更有效的CPU利用率腾出了空间。

Condition变量

根对象(Object类)中包括一些用于跨线程通信的特殊方法-wait(), notify(), notifyAll()。

这些是高级的并发功能,很多开发人员未曾使用过它们,不过这可能是好事,因为他们的工作机制非常微妙而且容易错误使用。

幸运的是,在JDK5.0中添加了java.util.concurrent后,开发人员能使用到这些方法的情况就更少了。

notify和wait之间存在互相作用,要在一个对象上wait或notify,你必须要持有该对象的monitor(锁)。

就像Lock是synchronized的泛化一样,Lock框架中也有notify和wait的泛化,称为Condition。

Lock对象充当了把Condition变量绑定到锁的工厂对象。与标准的wait和notify方法不同,可以给一个Lock绑定多个Condition变量。

这简化了许多并发算法的开发。例如,在Condition的JavaDoc中展示了一个例子,使用两个条件变量实现一个有界缓冲区,"not full"和 "not empty"。

与每个锁上只有一个等待集(wait set)相比,条件变量更易读且更有效。

类似于wait,notify和notifyAll,Condition的方法被命名为await,signal和signalAll,因为它们无法覆盖Object中的相应方法。

这是不公平的

如果你仔细阅读过Javadoc,会发现ReentrantLock的构造函数中有一个布尔类型的参数,让你选择是需要公平锁还是非公平锁。

公平锁是指线程得到锁的顺序与请求锁的顺序相同,先来先得。非公平锁可能会允许插入,其中某个线程可能会先于其他更早请求锁的线程得到锁。

为什么我们不希望锁都是公平的?毕竟公平是件好事,不公平是坏事,对吧?

实际上,公平锁是非常重的,并且付出了巨大的性能成本。公平即意味着比非公平锁更低的吞吐量。

默认情况下,你应该选择非公平锁,除非你的算法对正确性有严苛的要求,线程必须按照它们排队的顺序执行。

那么synchronized呢?内置的monitor锁是公平的吗?答案是,可能让你很惊讶,他们不是,从来都不是。

没有人会抱怨线程饥饿问题,因为JVM保证了所有正在等待锁的线程最终都会获取锁。

大多数情况下,保证统计学上的公平性已经足够了,而且其成本要比保证绝对公平性要低得多。

因此,默认情况下,ReentrantLock是“不公平的”,和synchronized保持一致。

图3和图4的基准测试与上面的图1、图2是一样的,只是增加了一个公平锁(FAIR)。如你所见,“公平”不是免费的。 所以不要把“公平”作为默认值。

图3. Relative throughput for synchronization, barging Lock, and fair Lock, with four CPUs



图 4. Relative throughput for synchronization, barging Lock, and fair Lock, with single CPU

ReentrantLock这么完美吗?

看起来ReentrantLock在各个方面都比同步更好,同步能做的它都可以做(具有相同的内存和并发语义),同步不能做的它也可以做,并且在负载下具有更好的性能。

那么,我们真的应该放弃synchronized吗?还是等着之后被改进,或者甚至直接用ReentrantLock重写我们现有的synchronized代码?

实际上,有关Java编程的书籍,在多线程的章节中都采用了这种方法,将示例完全用Lock实现,对synchronized是一带而过。我认为这是一件好事。

不要放弃synchronized

尽管ReentrantLock的表现是令人印象深刻的,在同步方面有很多明显的优势,但是我认为急于将synchronized视为不推荐的功能是不明智的。

java.util.concurrent.lock中的锁类是针对高级开发者或者高级组件的。通常,除非你需要Lock的高级功能,或者你有证据(不仅是怀疑)证明使用synchronized遇到了性能瓶颈,否则你都应该坚持使用synchronized。

为什么在面对“更好”的Lock时,我显着有点保守主义?

其实与Lock相比,synchronized仍然具有一些优势。

其一,使用synchronized时你不可能忘记释放锁,因为在退出同步块时JVM帮你做了。

而在finally块中释放锁是很容易忘记的,这对程序的伤害极大。而且一旦出现问题,你很难定位到问题在哪。(这是完全不推荐初级开发人员不使用Lock的充分理由)。

另一个原因是,当JVM使用synchronized获取锁和释放锁时,会保留锁相关的信息,在进行线程dump时这些信息会包括在内。这对程序调试是无价的,因为它可以让你定位到死锁或者其他异常的根源。而Lock只是一个普通的类,JVM也不知道某个线程持有哪个Lock对象。

什么时候选择ReentrantLock?

那么,什么时候应该使用ReentrantLock?答案很简单,在你需要使用synchronized无法提供的功能时,例如定时锁、可中断锁、非块结构锁、多个条件变量、或锁轮询。

ReentrantLock还具有很好的可伸缩性,如果你确实遇到锁竞争激烈的情况,就使用它吧。

不过请记住,绝大多数synchronized块几乎从未遇过任何竞争,更不用说激烈的竞争了。

我建议你先使用synchronized,直到被证明synchronized不能满足需求了,

而不是简单的假设“ReentrantLock的性能会更好”。请记住,Lock是面向高级用户的高级工具。

真正的高手倾向于使用最简单的工具完成工作,直到他们确信简单工具已经不适合了。

一个永恒的真理,先使其正确,然后再考虑是否需要让它更快。

总结

Lock框架是synchronized的兼容替代品,它提供了许多同步未提供的功能,以及在竞争条件下更好的性能。

但是,这些优势还不足以让你总是优先选择ReentrantLock,而冷落了synchronized。

根据你的实际需求来决定是否需要ReentrantLock的魔法力量。

我想,在大多数情况下你是不需要的Lock的,因为同步可以很好的胜任,可以在所有JVM上工作,可以被更广泛的开发者理解,并且不容易出错。

译者注

文中作者多次强调了(感觉有点啰嗦,滑稽脸),非必要情况下不要使用ReentrantLock,而是优先考虑synchronized。

之所以对ReentrantLock这么保守,我想是因为这篇文章写在JDK5刚刚发布的2004年,那时候Lock对于大多数开发者还是一个陌生的东西,对其工作原理和优缺点都不太熟悉。

但是2020年的今天,JDK13都发布了,JDK8已成了主流,那么想用就用吧。

1.non-block-structured locking的说明:https://stackoverflow.com/questions/59677725/what-does-non-block-structured-locking-mean)

2.英文版原文 : https://www.ibm.com/developerworks/java/library/j-jtp10264/index.html?ca=drs-

才华能力出众的ReentrantLock的更多相关文章

  1. ReenTrantLock可重入锁(和synchronized的区别)总结

    ReenTrantLock可重入锁(和synchronized的区别)总结 可重入性: 从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也 ...

  2. Java ReEntrantLock (Java代码实战-001)

    Lock类也可以实现线程同步,而Lock获得锁需要执行lock方法,释放锁需要执行unLock方法 Lock类可以创建Condition对象,Condition对象用来使线程等待和唤醒线程,需要注意的 ...

  3. ReenTrantLock可重入锁和synchronized的区别

    ReenTrantLock可重入锁和synchronized的区别 可重入性: 从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入 ...

  4. synchronized,ReentrantLock解决锁冲突,脏读的问题

    最常见的秒杀系统,解决思路就是从前端.后台服务.数据库层层去掉负载,以达到平衡 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLo ...

  5. Synchronized 与 ReentrantLock 的区别!

    来源:cnblogs.com/baizhanshi/p/7211802.html 之前栈长分享了重入锁的概念:<到底什么是重入锁,拜托,一次搞清楚!>,今天现来深入了解下 Synchron ...

  6. 关于职位的解释---转CSDN的文章

    摘要我在IT职场打滚超过15年了,从小小的程序员做到常务副总.相对于其它行业,IT职场应该算比较光明的了,但也陷阱重重,本文说说我的亲身体会,希望大家能在IT职场上战无不胜! 通用法则 法则1:忍耐是 ...

  7. IT职场求生法则(转)

    摘要:在IT职场打滚超过10年了,从小小的程序员做到常务副总.相对于其它行业,IT职场应该算比较光明的了,但也陷阱重重,本文说说我的亲身体会,希望大家能在IT职场上战无不胜! 作者:张传波 软件知识大 ...

  8. 【转】工科男IT职场求生法则

    转自:http://www.36dsj.com/archives/3459 我在IT职场打滚超过10年了,从小小的程序员做到常务副总.相对于其它行业,IT职场应该算比较光明的了,但也陷阱重重,本文说说 ...

  9. IT职场生存法则

    转!!!!!!!!!!!!! 摘要我在IT职场打滚超过15年了,从小小的程序员做到常务副总.相对于其它行业,IT职场应该算比较光明的了,但也陷阱重重,本文说说我的亲身体会,希望大家能在IT职场上战无不 ...

随机推荐

  1. Codeforce 1251C. Minimize The Integer

    C. Minimize The Integer time limit per test2 seconds memory limit per test256 megabytes inputstandar ...

  2. Jenkins如何进行权限管理

    一.安装插件 插件名:Role-based Authorization Strategy 二.配置授权策略 三.创建用户 四.添加并配置权限 4.1.添加Global Role 普通角色拥有全局只读权 ...

  3. libevhtp初探

    libevent的evhttp不适合多线程,libevhtp重新设计了libevent的http API,采用了和memcached类似的多线程模型. worker线程的管道读事件的回调函数为htp_ ...

  4. java基础篇 之 集合概述(List)

    list,有序集合,元素可重复 LinkedList:底层用链表实现,查找慢,增删快.为什么?? ArrayList:底层用数组实现,查找看,增删慢.为什么?? Vector:跟ArrayList一样 ...

  5. metasploit payload运行原理浅析

    背景 最近在做一些msf相关的事情,今天听到免杀相关的,去查询了下相关资料. 第一个不能错过的就是cobalt strike作者早年写的metasploit-loader项目了,我看了项目源码,找了一 ...

  6. Qt插件系统

    说明 近期入职新公司,新公司的项目用到了Qt的插件系统,花时间了解了一下,还以为Qt的插件系统有多么高级呢,原来归根到底还是 dll 的动态调用时获取其中的类那一招啊,原理和之前的文章<DLL的 ...

  7. LeetCode #188场周赛题解

    A题链接 给你一个目标数组 target 和一个整数 n.每次迭代,需要从 list = {1,2,3..., n} 中依序读取一个数字. 请使用下述操作来构建目标数组 target : Push:从 ...

  8. SVN 分支代码合并到主线

    SVN 分支代码合并到主线 步骤一安装TortoiseSVN 客户端,在本地 checkout主线代码 步骤二:创建branches分支 步骤三.branches修改提交代码 步骤四:分支代码合并到主 ...

  9. Hexo 博客利用 Nginx 实现中英文切换

    本文记录了对 Hexo 博客进行中英文切换的配置过程,实现同一应用共用模版,任何页面可以切换到另一语言的对应页面,并对未明确语言的访问地址,根据浏览器语言进行自动跳转 实现细则 中英文地址区分 博客中 ...

  10. HDU-6351 Beautiful Now 全排列暴力

    Beautiful Now 题意 给出一个最大为10^9的数字n,以及一个k,你最多交换n中任意两个位置的数字k次,问形成的最大数字和最小数字. 思路 看到这题,我靠这题暴力交换一下,不难啊,咋没人做 ...