显式锁(第十三章)

显式锁

在Java5.0之前,在协调对共享对象的访问时可以使用的机制只有synchronized和volatile。Java5.0增加了一种新的机制:ReentrantLock。ReentrantLock并不是一种替代内置加锁的方法,而是当内置加锁机制不适用时,作为一种可选择的高级功能。

1. Lock接口与ReentrantLock

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相同的互斥性和内存可见性。

Lock接口的使用形式:

Lock lock = new ReentrantLock();
...
lock.lock();
try {
    //更新对象状态
    //捕获异常,并在必要时恢复不变性条件
} finally {
    lock.unlock();
}
    使用Lock时,必须在finally块中释放锁,否则,如果被保护的代码中抛出了异常,那么这个锁永远都无法释放。这就是ReentrantLock不能完全替代synchronized的原因:它更加危险,因为程序的执行控制离开被保护的代码块时,不会自动清除锁。
    

2. 轮询锁与定时锁

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

    可定时的与可轮询的锁能够避免死锁的发生。如果不能获得所有需要的锁,那么可以使用可定时的或可轮询的锁获取方式,从而使你重新获得控制权,它会释放已经获得的锁,然后重新尝试获取所有锁。
    

在实现具有时间限制的操作时,定时锁能够提供一个时限,如果操作不能在指定的时间内给出结果,那么就会使程序提前结束。

3. 可中断的锁获取操作

Java中,请求内置锁时不能响应中断。Lock的lockInterruptibly方法能够在锁的同时保持对中断的响应,且由于它包含在Lock中,因此无须创建其他类型的不可中断阻塞机制。

...
lock.lockInterruptibly();
...

定时的tryLock同样能响应中断,因此当需要实现一个定时的和可中断的锁获取操作时,可是使用tryLock方法。

4. 公平性

在ReentrantLock的构造函数中提供了两种公平性选择:非公平(默认)、公平
在公平的锁上,线程按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁。

    在大多数情况下,非公平锁的性能要高于公平锁的性能。
    

当持有锁的时间相对较长,或者请求锁的平均时间间隔较长,那么应该使用公平锁。在这些情况下,“插队”带来的吞吐量提升(当锁处于可用状态时,线程却还处于被唤醒的过程中)则可能不会出现。

与默认的ReentrantLock一样,内置锁并不会提供确定的公平性保证,但在大多数情况下,在锁实现上实现统计上的公平性保证已经足够了。Java语言规范并没有要求JVM以公平的方式来实现内置锁,而在各种JVM中也没有这样做。ReentrantLock并没有进一步降低锁的公平性,而只是使一些已经存在的内容更明显。

5.在synchronized和ReentrantLock之间进行选择

  • ReentrantLock的优点:
    ReentrantLock在加锁和内存上提供的语义与内置锁相同,此外它还提供了一些其他功能,包括定时的锁等待、可中断的锁等待、公平性,以及实现非块结构的加锁
  • ReentrantLock的缺点:
    ReentrantLock的危险性比同步机制要高,如果忘记在finally块中调用unlock,那么就有可能出现问题。
    两者之间的选择:
    当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的、可轮训的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。
    

6. 读-写锁

一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。
要读取由ReadWriteLock保护的数据,必须首先获得读取锁,当需要修改ReadWriteLock保护的数据时,必须首先获得写入锁。尽管这两个锁看上去是彼此独立的,但读取锁和写入锁只是读-写锁对象的不同视图。

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

ReentranReadWriteLock为这两种锁都提供了可重入的加锁语义。与ReentrantLock类似,ReentrantReadWriteLock在构造时也可以选择是一个非公平的锁(默认)还是一个公平的锁。
在公平的锁中,等待事件最长的线程将优先获得锁。如果这个锁由读线程持有,而另一个线程请求写入锁,那么其他读线程都不能获得读取锁,直到写线程使用完并且释放了写入锁。
在非公平的锁中,线程获得访问许可的顺序是不确定的。写线程降级为读线程是可以的,但从读线程升级为写线程则是不可以的(这样做会导致死锁)。

点赞