才华能力出众的ReentrantLock

主要内容

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

多线程和并发并不是什么新鲜事物,但是,Java是第一个把支持跨平台线程模型和内存模型直接纳入语言规范的主流编程语言。
诸如,在类库里有用于创建、启动、操作线程的Thread类,在语言特性上有用于多线程之间协作的synchronized和volatile。
它简化了跨平台并发程序的开发,但并不意味着编写并发应用就变得非常容易。

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的可伸缩性比较

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpspxj.html