java.util.concurrent.locks包为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。该框架允许更灵活地使用锁和条件,但以更难用的语法为代价。
Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock。
ReadWriteLock 接口以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现,即 ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。
LockSupport 类提供了更低级别的阻塞和解除阻塞支持,这对那些实现自己的定制锁类的开发人员很有用。
一、锁的概念
1.可重入锁
可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。也就是说如果当前线程已经获得了某个监视器对象所持有的锁,那么该线程在该方法中调用另外一个同步方法也同样持有该锁。
如果锁具备可重入性,则称作为可重入锁。像 synchronized和 ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个 synchronized方法时,比如说 methodA,而在 methodA中会调用另外一个synchronized方法 methodB,此时线程不必重新去申请锁,而是可以直接执行方法 methodB。
如以下情况:
public synchronized void methodA() {
// 調用相同监视器对象中的其他 synchronized方法
this.methodB();
}
public synchronized void methodB() {
// 其他代码
}
因为进入 methodA时已经获得了该监视器对象持有的锁,当从 methodA跳转到 methodB时就不必再去获取锁了。
所以使用以上代码修改后的示例为:
public class LockTest implements Runnable {
public synchronized void methodA() {
System.out.println("methodA:" + Thread.currentThread().getId());
// 调用同线程内另一个 synchronized方法
methodB();
}
public synchronized void methodB() {
System.out.println("methodB:" + Thread.currentThread().getId());
}
public void run() {
methodA();
}
public static void main(String[] args) {
LockTest lt = new LockTest();
new Thread(lt).start();
new Thread(lt).start();
}
}
//结果:
methodA:9
methodB:9
methodA:10
methodB:10
假如 synchronized不具备可重入性,此时 methodA线程就需要重新申请锁。但是这就会造成一个问题,因为 methodA线程已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会造成 methodA线程一直等待永远不会获取到的锁。
所以可重入锁最大的作用是避免死锁。
2.可中断锁
顾名思义,就是在某些条件下可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而 Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
上一篇文章已经介绍过 lockInterruptibly()方法的使用场景,所以 lockInterruptibly()的用法就体现了 Lock的可中断性。
以下是中断锁的一个示例应用:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest implements Runnable {
private Lock lock = new ReentrantLock();
public void methodA() throws InterruptedException {
lock.lockInterruptibly(); // 如果抛出InterruptedException异常说明已经被中断,需要在外层判断处理
try {
System.out.println(Thread.currentThread().getName() + " 获得锁");
long startTime = System.currentTimeMillis();
// 等待5秒
for (;;) {
if (System.currentTimeMillis() - startTime >= 5000)
break;
}
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁");
}
}
public void run() {
try {
methodA();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中断");
}
}
public static void main(String[] args) {
LockTest lt = new LockTest();
Thread t1 = new Thread(lt);
Thread t2 = new Thread(lt);
t1.start();
t2.start();
t2.interrupt();
}
}
//结果:
Thread-0 获得锁
Thread-1 被中断
Thread-0 释放锁
当调用 lockInterruptibly()方法中断锁的获取时,会抛出 InterruptedException异常。这里不应该使用catch捕获异常,否则将继续执行 lockInterruptibly()方法之后的代码,从而报未获取锁的错误。应向外层抛出该异常以证明获取锁操作已经被中断,从而进行其他处理。
3.公平锁
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
而非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
而对于 ReentrantLock和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是在初始化时可以设置为公平锁。
Lock lock=new ReentrantLock(true);
如果参数为 true表示为公平锁,为 fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。
在 ReentrantLock中定义了2个静态内部类,一个是 NotFairSync,一个是 FairSync,分别用来实现非公平锁和公平锁。ReentrantLock中还有很多与这两种锁相关的方法,在下面的章节中会逐一介绍。
4.读写锁
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。
正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock就是读写锁接口,ReentrantReadWriteLock是这个接口的实现。
可以通过 readLock()获取读锁,通过 writeLock()获取写锁。
几种常用的锁类型已经了解,接下来就从具体实现来入手,深入学习它们的用法及原理。
二、ReentrantLock
1.简介
java.util.concurrent.lock 中的 Lock 框架是锁的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似轮询锁、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上)
ReentrantLock是一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
可重入锁意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。
ReentrantLock 将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁并返回。如果当前线程已经拥有该锁,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。
2.构造方法
ReentrantLock类的构造方法接受一个可选的公平(fair)参数。当设置为 true 时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程。否则此锁将无法保证任何特定访问顺序。与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是,公平锁不能保证线程调度的公平性。因此,使用公平锁的众多线程中的一员可能获得多倍的成功机会,这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。还要注意的是,未定时的 tryLock 方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。
以下是 ReentrantLock的两种构造方法:
/**
* 创建 ReentrantLock实例,相当于使用 new ReentrantLock(false);
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 根据指定的公平策略创建 ReentrantLock实例
*/
public ReentrantLock(boolean fair) {
sync = (fair) ? new FairSync() : new NonfairSync();
}
默认构造方法相当于构建了一个非公平策略的 ReentrantLock实例。
3.ReentrantLock使用
1)lock()方法
之前已经有相关实例展现了 lock()方法的使用,使用 lock方法值得注意的是需要在finally块中主动释放锁,否则其他线程将阻塞。
public class LockThread {
Lock lock = new ReentrantLock();
public void lock() {
// 获取锁
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + " get the lock");
} finally {
// 释放锁
lock.unlock();
System.out.println(Thread.currentThread().getName() + " release the lock");
}
}
public static void main(String[] args) {
final LockThread lt = new LockThread();
new Thread(new Runnable() {
public void run() {
lt.lock();
}
}).start();
new Thread(new Runnable() {
public void run() {
lt.lock();
}
}).start();
}
}
//结果:
Thread-0 get the lock
Thread-0 release the lock
Thread-1 get the lock
Thread-1 release the lock
2)unlock()方法
unlock方法需要配合 lock()方法使用,unlock方法需要在 catch或 finally块中声明。当未获得锁时,使用unlock方法会抛出 IllegalMonitorStateException异常。
注释以上代码中 lock方法,将产生以下结果:
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:127)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1175)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:431)
at LockThread.lock(LockThread.java:14)
at LockThread$1.run(LockThread.java:23)
at java.lang.Thread.run(Thread.java:619)
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:127)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1175)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:431)
at LockThread.lock(LockThread.java:14)
at LockThread$2.run(LockThread.java:28)
at java.lang.Thread.run(Thread.java:619)
3)tryLock()方法
tryLock方法仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
如果该锁没有被另一个线程保持,并且立即返回 true 值,则将锁的保持计数设置为 1。即使已将此锁设置为使用公平排序策略,但是调用 tryLock() 仍将 立即获取锁(如果有可用的),而不管其他线程当前是否正在等待该锁。在某些情况下,此“闯入”行为可能很有用,即使它会打破公平性也如此。如果希望遵守此锁的公平设置,则使用 tryLock(0, TimeUnit.SECONDS) ,它几乎是等效的(也检测中断)。
如果当前线程已经保持此锁,则将保持计数加 1,该方法将返回 true。 如果锁被另一个线程保持,则此方法将立即返回 false 值。
示例代码如下:
public class LockThread {
Lock lock = new ReentrantLock();
public void lock() {
// 尝试获取锁
if (lock.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " get the lock");
while (true) {
//block here
}
} finally {
// 释放锁
lock.unlock();
System.out.println(Thread.currentThread().getName() + " release the lock");
}
} else {
System.out.println(Thread.currentThread().getName() + " get the lock fail");
}
}
public static void main(String[] args) {
final LockThread lt = new LockThread();
new Thread(new Runnable() {
public void run() {
lt.lock();
}
}).start();
new Thread(new Runnable() {
public void run() {
lt.lock();
}
}).start();
}
}
//结果:
Thread-0 get the lock
Thread-1 get the lock fail
其中利用 while循环产生阻塞,导致 Thread-0线程无法释放锁。当 Thread-1利用 tryLock方法尝试获取锁时,发现锁暂时无法被获取,tryLock方法返回 false,获取锁失败。
4)tryLock(long timeout, TimeUnit unit)方法
如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
如果该锁没有被另一个线程保持,并且立即返回 true 值,则将锁的保持计数设置为 1。如果为了使用公平的排序策略,已经设置此锁,并且其他线程都在等待该锁,则不会 获取一个可用的锁。这与 tryLock() 方法相反。如果想使用一个允许闯入公平锁的定时 tryLock,那么可以将定时形式和不定时形式组合在一起:
if (lock.tryLock() || lock.tryLock(timeout, unit) ) {
...
}
如果当前线程已经保持此锁,则将保持计数加 1,该方法将返回 true。如果超出了指定的等待时间,则返回值为 false。如果该时间小于等于 0,则此方法根本不会等待。
将tryLock部分示例中的lock方法代码修改为:
public void lock() {
try {
// 尝试10秒内获取锁
if (lock.tryLock() || lock.tryLock(10L, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " get the lock");
} finally {
long startTime = System.currentTimeMillis();
for (;;) {
if (System.currentTimeMillis() - startTime >= 5000) {
// 释放锁
System.out.println(Thread.currentThread().getName() + " release the lock");
lock.unlock();
break;
}
}
}
} else {
System.out.println(Thread.currentThread().getName() + " get the lock fail");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//结果:
Thread-0 get the lock
Thread-0 release the lock
Thread-1 get the lock
Thread-1 release the lock
结果打印正确。如果将阻塞时间修改的比 tryLock方法时间要长,则结果为:
Thread-0 get the lock
Thread-1 get the lock fail
Thread-0 release the lock
5)lockInterruptibly()方法
如果当前线程未被中断,则获取锁。
如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。
如果当前线程已经保持此锁,则将保持计数加 1,并且该方法立即返回。
如果锁被另一个线程保持,则出于线程调度目的,禁用当前线程,并且在发生以下两种情况之一以前,该线程将一直处于休眠状态:
• 锁由当前线程获得;或者
• 其他某个线程中断当前线程。
如果当前线程获得该锁,则将锁保持计数设置为 1。
如果当前线程:
• 在进入此方法时已经设置了该线程的中断状态;或者
• 在等待获取锁的同时被中断。
则抛出 InterruptedException,并且清除当前线程的已中断状态。
在此实现中,因为此方法是一个显式中断点,所以要优先考虑响应中断,而不是响应锁的普通获取或重入获取。
lockInterruptibly方法的示例在本文刚开始时已经展现,这里就不再复述了。
6)getHoldCount()方法
查询当前线程保持此锁的次数。
对于与解除锁操作不匹配的每个锁操作,线程都会保持一个锁。
保持计数信息通常只用于测试和调试。例如,如果不应该使用已经保持的锁进入代码的某一部分,则可以声明如下:
ReentrantLock lock = new ReentrantLock();
assert lock.getHoldCount() == 0;
lock.lock();
try {
// ...
} finally {
lock.unlock();
}
其中 assert关键字用法如下:
(1)assert
如果
如果为false,则程序抛出AssertionError,并终止执行。
(2)assert
如果
如果为false,则程序抛出java.lang.AssertionError,并输入<错误信息表达式>。
三、ReentrantLock内部类
1.ReentrantLock.Sync类
可重入锁内部实现的超类,主要实现了公平与非公平锁的共有方法,并提供了加锁操作的统一抽象:abstract void lock();,还有核心的释放锁的操作。Sync类是 ReentrantLock的内部类,继承自 AbstractQueuedSynchronizer类。可以看到,ReentrantLock都是把具体实现委托给内部类而不是直接继承自 AbstractQueuedSynchronizer,这样的好处是用户不会看到不需要的方法,也避免了用户错误地使用 AbstractQueuedSynchronizer的公开方法而导致错误。
ReentrantLock的重入计数是使用 AbstractQueuedSynchronizer的state属性的,state大于0表示锁被占用、等于0表示空闲,小于0则是重入次数太多导致溢出了。
Sync类是该锁的同步控制基础。Sync子类实现了公平与非公平两个版本。
其中:
• AbstractOwnableSynchronizer:保持和获取独占线程。
• AbstractQueuedSynchronizer:以虚拟队列的方式管理线程的锁获取与锁释放,以及各种情况下的线程中断。提供了默认的同步实现,但是获取锁和释放锁的实现定义为抽象方法,由子类实现。目的是使开发人员可以自由定义获取锁以及释放锁的方式。
• Sync:ReentrantLock的内部抽象类,实现了简单的获取锁和释放锁。
• NonfairSync和 FairSync:分别表示“非公平锁”和“公平锁”,都继承于 Sync,并且都是 ReentrantLock的内部类。
• ReentrantLock:实现了 Lock接口的 lock-unlock方法,根据 fair参数决定使用 NonfairSync还是FairSync。
Sync类中比较重要的实现方法有:nonfairTryAcquire,tryRelease等。
以下是 Sync的源代码:
static abstract class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* 执行Lock.lock()方法,留给子类根据其公平性实现 子类化的最主要原因是允许非公平的快速路径
*/
abstract void lock();
/**
* 非公平获取实现
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 如果锁是空闲的,进行加锁必须用CAS操作来确保即使有多个线程竞争锁也是安全的
if (compareAndSetState(0, acquires)) {
// 把当前线程设为锁的持有者,在获取前可用于判断是否是重入。
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 如果锁被占用且当前线程是锁的持有者,说明是重入。
int nextc = c + acquires;
if (nextc < 0)
// 溢出,加锁次数从0开始,加锁与释放操作是对称的,所以绝不会是小于0值,小于0只能是溢出。
throw new Error("Maximum lock count exceeded");
// 锁被持有的情况下,只有持有者才能更新锁保护的资源,所以这里不需要用CAS操作。
setState(nextc);
return true;
}
return false;
}
/**
* 尝试释放锁
*/
protected final boolean tryRelease(int releases) {
// 先读取state是为了获得一个读屏障,owner不是volatile的。
int c = getState() - releases;
// 只有锁的持有者才能释放锁
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {// 锁重入计数减到0,需要真正释放锁了。
free = true;
setExclusiveOwnerThread(null);
}
// 如果c为0,写操作完成后,其他线程就会看到锁被释放了,所以 setExclusiveOwnerThread必须在这个写之前完成。
setState(c);
return free;
}
/**
* 判断当前线程是否为锁的持有者
*/
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
/**
* 创建新的 Condition实例
*/
final ConditionObject newCondition() {
return new ConditionObject();
}
/**
* 获取锁持有者
*/
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
/**
* 获取加锁次数
*/
final int getHoldCount() {
// 以state属性作为加锁次数
return isHeldExclusively() ? getState() : 0;
}
/**
* 是否获取锁
*/
final boolean isLocked() {
// 加锁次数为0表示没有被拥有
return getState() != 0;
}
/**
*
* 从流中重新构建锁实例
*/
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // 重置为未锁定状态
}
}
其中 compareAndSetState方法的作用是:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。
setExclusiveOwnerThread方法的作用是:设置当前拥有独占访问的线程。null 参数表示没有线程拥有访问。
其他方法的作用已经在注释中体现了。
2.ReentrantLock.NonfairSync与 ReentrantLock.FairSync
NonfairSync与 FairSync均继承于 Sync类,两个类主要的区别是lock() 方法与 tryAcquire(int)的具体实现。
首先是 NonfairSync类:
/**
* 非公平锁 Sync实现
*/
final static class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* 执行lock,尝试立即进入,失败就退回常规流程
*/
final void lock() {
// 首先进行状态设置
if (compareAndSetState(0, 1))
// 如果状态设置成功,把当前线程设为锁持有者
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
/**
* 调用非公平版本获取
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
然后是 FairSync类,FairSync类提供公平性的锁实现。实现公平性的关键在于:如果锁被占用且当前线程不是持有者也不是等待队列的第一个,则进入等待队列。
/**
* 公平锁 Sync实现
*/
final static class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
/**
* 获取锁
*/
final void lock() {
// acquire方法会先调用 tryAcquire,所以公平策略的控制留给 tryAcquire
acquire(1);
}
/**
* 公平版本 tryAcquire,除非是递归调用或没有等待者或者是第一个,否则不授予访问
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 与非公平的不同就是要判断当前线程是否为首节点
if (isFirst(current) && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
// 如果为重入
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;// 重入次数增加
if (nextc < 0)// 溢出,重入次数太多,在改变状态之前抛出异常以确保锁的状态是正确的
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
NonfairSync与 FairSync不同在于:
lock方法中
NonfairSync是:如果原状态为0,则设置为新值1,如果设置成功,直接得到锁;如果设置失败则执行与FairSync流程相同的操作。
FairSync则是:先尝试去获取锁,如果得到了锁则设置状态值为1。
重入锁方面两个方法表现一样。
tryAcquire方法中
FairSync会比 NonfairSync多判断一个 isFirst(current)条件。
isFirst源代码为:
final boolean isFirst(Thread current) {
Node h, s;
return ((h = head) == null || ((s = h.next) != null && s.thread == current) || fullIsFirst(current));
}
线程为首结点需要满足以下条件:
1)等待队列为空。
2)等待队列 head的 next结点的 thread为当前线程(head.next.thread = currentThread),即线程为等待队列除哑结点外的第一个结点。
3)等待队列 head结点到某个结点(暂命名为结点s),之间的所有结点的thread变量为 null,且结点s的thread为当前线程。
四、ReentrantLock 与 synchronized的选择
1)比较 ReentrantLock 和 synchronized 的可伸缩性
引用自网络的测试结果:
两图总结了不同线程数量的结果。这个评测并不完美,而且只在两个系统上运行了(一个是双 Xeon 运行超线程 Linux,另一个是单处理器 Windows 系统),但是,应当足以表现 synchronized 与 ReentrantLock 相比所具有的伸缩性优势了。
两图中的图表以每秒调用数为单位显示了吞吐率,把不同的实现调整到 1 线程 synchronized 的情况。每个实现都相对迅速地集中在某个稳定状态的吞吐率上,该状态通常要求处理器得到充分利用,把大多数的处理器时间都花在处理实际工作(计算机随机数)上,只有小部分时间花在了线程调度开支上。我们注意到,synchronized 版本在处理任何类型的争用时,表现都相当差,而 Lock 版本在调度的开支上花的时间相当少,从而为更高的吞吐率留下空间,实现了更有效的 CPU 利用。
2)条件变量
类 Object 包含某些特殊的方法,用来在线程的 wait() 、 notify() 和 notifyAll() 之间进行通信。这些是高级的并发性特性,许多开发人员从来没有用过它们 —— 这可能是件好事,因为它们相当微妙,很容易使用不当。幸运的是,随着 JDK 5.0 中引入 java.util.concurrent ,开发人员几乎更加没有什么地方需要使用这些方法了。
通知与锁定之间有一个交互 —— 为了在对象上 wait 或 notify ,您必须持有该对象的锁。就像 Lock 是同步的概括一样, Lock 框架包含了对 wait 和 notify 的概括,这个概括叫作 条件(Condition) 。 Lock 对象则充当绑定到这个锁的条件变量的工厂对象,与标准的 wait 和 notify 方法不同,对于指定的 Lock ,可以有不止一个条件变量与它关联。这样就简化了许多并发算法的开发。例如, 条件(Condition) 的 Javadoc 显示了一个有界缓冲区实现的示例,该示例使用了两个条件变量,“not full”和“not empty”,它比每个 lock 只用一个 wait 设置的实现方式可读性要好一些(而且更有效)。 Condition 的方法与 wait 、 notify 和 notifyAll 方法类似,分别命名为 await 、 signal 和 signalAll ,因为它们不能覆盖 Object 上的对应方法。
3)公平与非公平
ReentrantLock 构造器的一个参数是 boolean fair值,它允许您选择想要一个 公平(fair)锁,还是一个 不公平(unfair)锁。公平锁使线程按照请求锁的顺序依次获得锁;而不公平锁则允许直接获取锁,在这种情况下,线程有时可以比先请求锁的其他线程先得到锁。注意 synchronized 是非公平锁。
为什么我们不让所有的锁都公平呢?毕竟,公平是好事,不公平是不好的,不是吗?(当孩子们想要一个决定时,总会叫嚷“这不公平”。我们认为公平非常重要,孩子们也知道。)在现实中,公平保证了锁是非常健壮的锁,有很大的性能成本。要确保公平所需要的记帐(bookkeeping)和同步,就意味着被争夺的公平锁要比不公平锁的吞吐率更低。作为默认设置,应当把公平设置为 false ,除非公平对您的算法至关重要,需要严格按照线程排队的顺序对其进行服务。
那么同步又如何呢?内置的监控器锁是公平的吗?答案令许多人感到大吃一惊,它们是不公平的,而且永远都是不公平的。但是没有人抱怨过线程饥渴,因为 JVM 保证了所有线程最终都会得到它们所等候的锁。确保统计上的公平性,对多数情况来说,这就已经足够了,而这花费的成本则要比绝对的公平保证的低得多。所以,默认情况下 ReentrantLock 是“不公平”的,这一事实只是把同步中一直是事件的东西表面化而已。如果您在同步的时候并不介意这一点,那么在 ReentrantLock 时也不必为它担心。
以上两图与之前两图数据相同,只是添加了一个数据集,用来进行随机数基准检测,这次检测使用了公平锁,而不是默认的协商锁。正如您能看到的,公平是有代价的。如果您需要公平,就必须付出代价,但是请不要把它作为您的默认选择。
4)synchronized已无用?
虽然 ReentrantLock 是个非常动人的实现,相对 synchronized 来说,它有一些重要的优势,但是我认为急于把 synchronized 视若敝屣,绝对是个严重的错误。 java.util.concurrent.lock 中的锁定类是用于高级用户和高级情况的工具 。一般来说,除非您对 Lock 的某个高级特性有明确的需要,或者有明确的证据(而不是仅仅是怀疑)表明在特定情况下,同步已经成为可伸缩性的瓶颈,否则还是应当继续使用 synchronized。
为什么我在一个显然“更好的”实现的使用上主张保守呢?因为对于 java.util.concurrent.lock 中的锁定类来说,synchronized 仍然有一些优势。比如,在使用 synchronized 的时候,不可能忘记释放锁;在退出 synchronized 块时,JVM 会为您做这件事。您很容易忘记用 finally 块释放锁,这对程序非常有害。您的程序能够通过测试,但会在实际工作中出现死锁,那时会很难指出原因(这也是为什么根本不让初级开发人员使用 Lock 的一个好理由。)
另一个原因是因为,当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。而且,几乎每个开发人员都熟悉 synchronized,它可以在 JVM 的所有版本中工作。在 JDK 5.0 成为标准(从现在开始可能需要两年)之前,使用 Lock 类将意味着要利用的特性不是每个 JVM 都有的,而且不是每个开发人员都熟悉的。
5)什么时候选择用 ReentrantLock 代替 synchronized
既然如此,我们什么时候才应该使用 ReentrantLock 呢?答案非常简单 —— 在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者轮询锁。ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock “性能会更好”。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。