Java并发之显式锁
一.概述
在Java 5.0之前,在协调对共享对象的访问时可以使用的机制只有synchronized和volatile,我们的选择不多。
但在Java5.0 增加了一种新的机制:ReentratLock ,显式锁。 它并不是一种替代内置加锁的方法,而是当内置加锁机制不适用时,作为一种可选择的高级功能。
二.具体学习
1.Lock与ReentrantLock
1)Lock是个接口,其中定义了一组抽象的加锁操作:
public interface Lock{
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long timeout , TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
从上面代码我们可以看出:与内置加锁机制不同的是,Lock提供了一种无条件的,可轮询的,定时的以及可中断的锁获取操作,所有的加锁和解锁都是显式的。
注意:在Lock的实现中必须提供与内部锁相同的内存可见性语义,但在加锁语义,调度算法,顺序保证以及性能特性等方面可以有所不同。
2)ReentrantLock是个类,它实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。
在获取ReentrantLock时,有着与进入同步代码块相同的内存语义,在释放ReentrantLock时,有着与退出同步代码块相同的内存语义。
此外,与synchronized一样,ReentrantLock提供了可重入的加锁语义,ReentrantLock支持在Lock接口中定义的所有获取锁模式,与synchronized相比,它还为处理锁的不可用性问题提供了更高的灵活性。
3)有些人可能会想:我觉得内置锁就完全够用了,干嘛还要加个显式锁?
是的,在大多数情况下,内置锁能很好的满足我们。但是它在功能上存在一些的局限,例如:内置锁无法中断一个正在等待获取锁的线程,或者无法在请求获取一个锁时无限地等待下去。
我们知道内置锁必须在获取该锁地代码块中释放,这虽然简化了编码工作并且与异常处理操作是实现了很好的交互,但却无法实现非阻塞结构的加锁规则。
在某些情况下,使用一种更灵活的加锁机制(显式锁)通常能提供更好的活跃性或性能。
4)如何使用显式锁?
下面代码展示了如何使用显式锁来保护对象状态:
Lock lock = new ReentrantLock();
...
lock.lock();
try{
//更新对象状态
//捕获异常,并在必要时恢复不变性条件
}finally{
lock.unlock();
}
注意:显式锁使用的关键时在最后在 finally 中释放锁。如果没有释放,将会造成严重的后果。
2.轮询锁和定时锁
可定时的与可轮询的锁获取模式是由tryLock方法实现的,与无条件的锁获取模式相比,它具有更加完善的错误恢复机制。
在内置锁中,死锁是一个严重的问题,恢复程序的唯一方法是重新启动程序,而防止死锁的唯一方法就是在构造程序的过程中避免出现不一致的锁顺序。 而可定时的与可轮询的锁提供了另一种可以避免死锁发生的选择。
1)如果一个线程不能获得所需要的锁,那么可以使用可定时的或可轮询的锁获取方式,从而使它重新获得控制权,它会释放已经获得的锁,然后重新尝试获取所有的锁。
下面我们利用这个策略来解决前面的动态顺序死锁的问题(可以参看动态顺序死锁问题):
下面代码无需抠细节,只需知道大概的思路即可
public boolean transferMoney(Account fromAcct,
Account toAcct,
DollarAmount amount,
long timeout,
TiemUnit unit) throws InsufficientFundsException , InterruptedException{
long fixedDelay = getFixedDelayComponentNanos(timeout,unit);
long randMod = getRandomDelayModulusNanos(timeout,unit);
long stopTime = System.nanoTime() + unit.toNanos(timeout);
while(true){
if(fromAcct.lock.tryLock()){//尝试获得fromAcct锁
try{
if(toAcct.lock.tryLock()){//尝试获得toAcct锁
try{
if(fromAcct.getBalance().compareTo(amount)<0)
throw new InsufficientFundsException();
else{
fromAcct.debit(amount);
toAcct.credit(amount);
return true;
}
}finally{
toAcct.lock.unlock();//释放toAcct锁
}
}
}finally{
fromAcct.lock.unlock();//释放fromAcct锁
}
}
if(System.nanoTime()<stopTime)//如果超出了时间限制
return false;//返回失败
NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);//睡眠随机的时间
}
}
上面代码使用tryLock来获取两个锁,如果不能同时获得,那么就回退并重新尝试,在休眠时间中包括固定部分和随机部分,从而降低活锁的可能性。如果在指定时间内不能获得所需要的锁,那么返回失败状态。
2)在实现具有时间限制的操作时,定时锁同样非常有用。 当在带有时间限制的操作中调用了一个阻塞方法时,它能根据剩余时间来提供一个时限,如果操作不能在指定的时间内给出结果,那么就会让程序提前结束。 当使用内置锁时,在开始请求锁后,这个操作将无法取消,因此内置锁很难实现带有时间限制的操作。
下面代码试图在Lock保护的共享通信线路上发送一条消息,如果不能在指定时间内完成,代码就会失败。定时的tryLock能够在这种带有时间限制的操作中实现独占加锁行为:
public boolean trySendOnSharedLine(String message,long timeout,TimeUnit unit)
throws InterruptedException{
long nanosToLock = unit.toNanos(timeout)- estimatedNanosToSend(message);
if(!lock.tryLock(nanosToLock,NANOSECONDS))
return false;
try{
return sendOnSharedLine(message);
}finally{
lock.unlock();
}
}
3.可中断的显式锁获取操作
如下:
public boolean sendOnSharedLine(String message) throws InterruptedException{
lock.lockInterruptibly();//获取可中断的显式锁
try{
return candellableSendOnSharedLine(message);
}finally{
lock.unlock();
}
}
private boolean cancellableSendOnSharedLine(String messgae) throws InterruptedException{...}
正如定时的锁获取操作能在带有时间限制的操作中使用独占锁,可中断的锁获取操作同样能在可取消的操作中使用加锁。
lockInterruptibly方法能够在获得锁的同时保持对中断的响应,并且由于它包含在Lock中,因此无须创建其他类型的不可中断阻塞机制。
4.非块结构的加锁
在内置锁中,锁的获取和释放等操作都是基于代码块的(释放锁的操作总是与获取锁的操作处于同一个代码块,而不考虑控制权如何退出该代码块)。内置锁的这种锁释放操作简化了对程序的分析,避免了可能的编码错误,但有时候我们需要更灵活的加锁规则来实现更加复杂的功能,这就是显式锁的非块式结构的加锁。
5.性能考虑因素
当时,把ReentrantLock添加到Java5.0时,它能比内置锁提供更好的竞争性能。
对于同步原语来说,竞争性能是可伸缩性的关键要素:如果有越多的资源被耗费在锁的管理和调度上,那么应用程序得到的资源就越少。 锁的实现方式越好,将需要越少的系统调用和上下文切换,并且在共享内存总线上的内存同步通信量也越少,而一些耗时的操作将占用应用程序的计算资源。
但Java 6 使用了改进后的算法来管理内置锁,与在ReentrantLock中使用的算法类似,该算法有效地提高了可伸缩性。并且达到了与显式锁几乎相同的竞争性能。
所以说:性能是一个不断变化的指标,如果在昨天的测试基准中发现X比Y更快,那么在今天就可能已经过时了。
6.公平性
在ReentrantLock的构造函数中提供了两种公平性选择:创建一个非公平的锁或者一个公平的锁。
1)在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许 “插队” :当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有等待线程并获得这个锁。
2)非公平的ReentrantLock并不提倡 “插队行为 ” ,但它无法防止某个线程在合适的时候进行 “插队”。
3)在公平的锁中,如果有另一个线程持有这个锁或者有其他线程在队列中等待这个锁,那么新发出请求的线程将被放入队列。在非公平的锁中,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。
注意:在大多数情况下,非公平锁的性能要高于公平锁的性能。所以不必要的话,不要为公平性付出代价
在竞争激烈的情况下,非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。 假设线程A持有一个锁,并且线程B请求这个锁。由于这个锁已被线程A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此会再此尝试获取锁,与此同时,如果C也请求这个锁,那么C很可能会在B完全唤醒之前获得,使用以及释放这个锁。这样的情况是一种 “双赢” 的局面:B获得锁的时刻并没有推迟,C更早地获得了锁,并且吞吐量也获得了提高。
注意:当持有锁的时间相对较长,或者请求锁的平均时间间隔较长,那么应该使用公平锁。
与默认的ReentrantLock一样,内置加锁并不会提供确定的公平性保证,但在大多数情况下,在锁实现上实现统计上的公平性保证已经足够了。Java语言规范并没有要求JVM以公平的方式来实现内置锁,而在各种JVM中也没有这样做。ReentrantLock并没有进一步降低锁的公平性,而只是使一些已经存在的内容更明显。
7.如何看待synchronized和ReentrantLock
与显式锁相比,内置锁仍然具有很大的优势。内置锁为许多开发人员所熟悉,并且简洁紧凑,而且在许多现有的程序中都已经使用了内置锁(如果将这两种机制混合使用,那么不仅容易令人困惑,也容易发生错误)。
ReentrantLock的危险性比同步机制要高,如果忘记在finally块中调用unlock,那么虽然代码表面上能正常运行,但实际上非常危险。
所以:仅当内置锁不能满足需求时,才考虑使用显式锁。
注意:在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具,当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的,可轮询的,可中断的锁获取操作,公平队列,非块结构的锁。
否则,还是应该使用synchronized

浙公网安备 33010602011771号