java并发编程9.显式锁

时间:2021-11-25 20:50:11

在协调共享对象的访问时可以使用的机制有synchronized和volatile。java 5.0新增了一种新的机制:ReentrankLock。

ReentrankLock并不是一种替代内置加锁的方法,而是当内置加锁机制不适用时,作为一种可选择的高级功能。

Lock与ReentrantLock

lock提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显示地。

在Lock的实现中必须提供与内部锁相同的内存可见性语义,但在加锁语义、调度算法、顺序保证以及性能特性等方面可以有所不同。

/**
* Lock接口
*/
public interface Lock{
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long timeout,TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}

ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。在获取ReentrantLock时,有着与进入同步代码块相同的内存语义,在释放ReentrantLock时,同样有着与退出同步代码块相同的内存语义。

ReentrantLock支持在Lock接口中定义的所有获取锁模式,并且为处理锁的不可用性问题提供了更高的灵活性。

Lock接口的标准使用形式:

    /**
* 必须在finally块中释放锁。
* 否则,如果在被保护的代码中抛出了异常,那么这个锁永远都无法释放。
* 当使用加锁时,还必须考虑在try块中抛出异常的情况
* 如果可能使对象处于某种不一致的状态,那么就需要更多的try-catch或try-finally代码块。
*
* 如果没有使用finally来释放Lock,那么相当于启动了一个定时炸弹。
* 当炸弹爆炸时,将很难追踪到最初发生错误的位置,因为没有记录应该释放锁的位置和时间。
* 这就是ReentrantLock不能完全替代synchronized的原因,它更加危险。在java6之后,改进了算法来管理内置锁。
*/
protected void terminated(){
Lock lock
= new ReentrantLock();
lock.lock();
try{
//更新对象状态
//捕获异常,并在必要时恢复不变性条件
}finally{
lock.unlock();
}
}

轮询锁与定时锁

可定时与可轮询的锁获取模式由tryLock方法实现,与无条件的锁获取模式相比,它具有更完善的错误恢复机制。

在内置锁中,死锁是一个严重的问题,恢复程序的唯一方法是重新启动程序,而防止死锁的唯一方法就是在构造程序时避免出现不一致的锁顺序。

可定时与可轮询的锁提供了另一种选择:避免死锁的发生。

/**
* 通过tryLock来避免锁顺序死锁。
* 使用tryLock来获取两个锁,如果不能同时获得,那么就回退并重新尝试。
* 在休眠时间中包括固定部分和随机部分,从而降低发生活锁的可能性。
* 如果在指定时间内不能获得所有需要的锁,那么transferMoney将返回一个失败的状态。
*/
public boolean transferMoney(Account fromAcct,Account toAcct,
DollarAmount amount,
long timeout,TimeUnit unit)throws InterruptedException{
long fixedDelay = getFixedDelayComponentNanos(timeout,unit);
long randMod = getRandomDelayModulusNanos(timeout,unit);
long stopTime = System.nanoTime() + unit.toNanos(timeout);
while(true){
if(fromAcct.lock.tryLock()){
try{
if(toAcct.lock.tryLock()){
try{
if(fromAcct.getBlance()compareTo(amount) < 0){
throw new InsufficientFundsException();
}
else{
fromAcct.debit(amount);
toAcct.credit(amount);
return true;
}
}
finally{
toAcct.lock.unlock();
}
}
}
finally{
fromAcct.lock.unlock();
}
}
if(System.nanoTime() < stopTime){
return false;
}
TimeUnit.NANOSECONDS.sleep(fixedDelay
+ rnd.nextLong() % randMod);
}
}
/**
* 带有时间限制的加锁
* 确保对资源进行串行访问的方法:一个单线程Executor;另一个方法是使用一个独占锁来保护对资源的访问。
* 试图在Lock保护的共享通信线路上发送一条消息,如果不能再指定时间内完成,代码就会失败。
* 定时的tryLock能够在这种带有时间限制的操作中实现独占的加锁行为。
*/
Lock lock
= new ReentrantLock();
public boolean trySendOnSharedLine(String meassage,long timeout,TimeUnit unit) throws InterruptedException{
long nanosToLock = unit.toNanos(timeout) - estimatedNanosToSend(meassage);
if(lock.tryLock(nanosToLock, TimeUnit.NANOSECONDS)){
return false;
}
try{
return sendOnSharedLine(meassage);
}
finally{
lock.unlock();
}
}
/**
* 可中断的锁获取操作
* lockInterruptibly方法能够在获得锁的同时保持对中断的响应,并且由于它包含在Lock中,
* 因此无须创建其他类型的不可中断阻塞机制。
*
*/
public boolean sendOnSharedLine(String message) throws InterruptedException{
lock.lockInterruptibly();
try{
return cancellableSendOnSharedLine(meassage);
}
finally{
lock.unlock();
}
}

private boolean cancellableSendOnSharedLine(String message) throws InterruptedException{
//...
return false;
}

公平性

ReentrantLock的构造函数中提供了两种公平性的选择:创建一个非公平的锁默认(或者一个公平的锁)。

在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许插队:

当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁,而在公平的锁中,如果另一个线程持有这个锁或者有其他线程在队列中等待这个锁,那么新发出请求的线程将被放入队列中。在非公平的锁中,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。因此当执行加锁操作时,公平性将由于在挂起线程和恢复线程时存在的开销而极大地降低性能。

在synchronized和ReentrantLock之间的选择

在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的、可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。

java5中,内置锁与ReentrantLock相比还有另一个优点:在线程转储中能给出在哪些调用帧中获得了哪些锁,并能检测和识别发生死锁的线程。而JVM并不知道哪些线程持有ReentrantLock,因此在调试使用ReentrantLock的线程的问题时,将起不到帮助作用。java6解决了这个问题,它提供了一个管理和调试接口,锁可以通过该接口进行注册,从而与ReentrantLock相关的加锁信息就能出现在线程转储中,并通过其他的管理接口和调试接口来访问。

synchronized是JVM的内置属性,它能执行一些优化,例如对线程封闭的锁对象的锁消除优化,通过增加锁的粒度来消除内置锁的同步,而如果通过基于类库的锁来实现这些功能,则可能性不大。

读 - 写锁

    /**
* ReadWriteLock接口
*/
public interface ReadWriteLock{
Lock readLock();
Lock WriteLock();
}

实现ReadWriteLock需要考虑的一些问题:

释放优先:当一个写入操作释放写入锁时,并且队列中同时存在读线程和写线程,那么应该优先选择读线程,写线程,还是最先发出请求的线程。

读线程插入:如果锁是由读线程持有,但有写线程正在等待,那么新到达的读线程能否立即获得访问权,还是应该在写线程后面等待?如果允许读线程插队到写线程之前,那么将提高并发性,但却可能造成写线程发生饥饿问题。

重入性:读取锁和写入锁是否是可重入的。

降级:如果一个线程持有写入锁,那么它能否在不释放该锁的情况下获得读取锁?这可能会使得写入锁被降级为读取锁,同时不允许其他写线程修改被保护的资源。

升级:读取锁能否优先于其他正在等待的读线程和写线程而升级为一个写入锁?在大多数的读写锁实现中并不支持升级,因为如果没有显示地升级操作,那么很容易造成死锁(如果两个读线程试图同时升级为写入锁,那么二者都不会释放读取锁)。

ReentrantReadWriteLock为这两种锁都提供了可重入的加锁语义,默认也是非公平锁。

当锁的持有时间较长并且大部分操作都不会修改被守护的资源时,那么读写锁能提高并发性。

/**
* ReadWriteMap中使用了ReentrantReadWriteLock来包装Map
* 从而使它能够在多个线程之间被安全的共享,并且能够避免读写和谢谢冲突。
* 实际上ConcurrentHashMap的性能已经很好了,如果需要对另一种Map实现如LinkedHashMap提供并发性更高的访问,可以考虑。
*/
public class ReadWriteMap<K,V> {
private final Map<K,V> map;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock r = lock.readLock();
private final Lock w = lock.writeLock();

public ReadWriteMap(Map<K,V> map){
this.map = map;
}

public V put(K key,V value){
w.lock();
try{
return map.put(key, value);
}
finally{
w.unlock();
}
}
//remove() putAll() clear()

public V get(Object key){
r.lock();
try{
return map.get(key);
}
finally{
r.unlock();
}
}
}

 

#笔记内容来自  《java并发编程实战