显式锁

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

Lock与ReentrantLock

与内置加锁机制不同的是,Lock提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的。在Lock的实现中必须提供与内部锁相同的内存可见性语义,但在加锁语义、调度算法、顺序保证以及性能特性等方面可以有所不同。

public interface Lock {
  void lock();
  void lockInterruptibly() throws InterruptedException;
  boolean tryLock();
  boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  void unlock();
  Condition newCondition();
}

ReentrantLock实现了Lock接口,并提供了与synchronized相同的排斥性和内存可见性。在获取ReentrantLock时,有着与进入同步代码块相同的内存语义,在释放ReentrantLock时,同样有着与退出同步代码块相同的内存语义。此外,与synchronized一样,ReentrantLock还提供了可重入的加锁语义。ReentrantLock支持在Lock接口中定义的多有获取锁模式,并且与synchronized相比,它还为处理锁的不可用性问题提供了更高的灵活性。

为什么要创建一种与内置锁如此相似的新加锁机制?在大多数情况下,内置锁都能很好地工作,但在功能上存在一些局限性,例如,无法中断一个正在等待获取锁的线程,或者无法在请求获取一个锁时无限地等待下去。内置锁必须在获取该锁的代码块中释放,这就简化了编码工作,并且与异常处理操作实现了很好的交互,但却无法实现非阻塞结构的加锁规则。这些都是使用synchronized的原因,但在某些情况下,一种更灵活的加锁机制通常能提供更好的活跃性或性能。

Lock锁的使用比使用内置锁复杂一些:必须在finally块中释放锁。否则,如果在被保护的代码中抛出了异常,那么这个锁永远都无法释放。

轮询锁与定时锁

可定时的与可轮询的锁获取模式是由tryLock方法实现的,与无条件的锁获取模式相比,它具有更完善的错误恢复机制。在内置锁中,死锁是一个严重的问题,恢复程序的唯一方法是重新启动程序,而防止死锁的唯一方法就是在构造过程时避免不一致的锁顺序。可定时的与可轮询的锁提供了另一种选择:避免死锁的发生。

如果不能获得所需要的锁,那么可以使用可定时的或可轮询的锁获取方式,从而使你重新获得控制权,它会释放已经获得的锁,然后重新尝试获取所有锁(失败最好记下日志)。

可中断的锁获取操作

lockInterruptible方法能够在获得锁的同时保持对中断的响应,并且由于它包含在Lock中,因此无需创建其他类型的不可中断阻塞机制。

非块结构的加锁

在内置锁中,锁的获取和释放等操作都是基于代码块的——释放锁的操作总是与获取锁的操作处于同一个代码块,而不考虑控制权如何退出该代码块。自动的锁释放操作简化了对程序的分析,避免了可能的编码错误,但有时候需要更灵活的加锁规则。

性能考虑因素

ReentrantLock能比内置锁提供更好的竞争性能。对于同步原语来说,竞争性能是可伸缩性的关键要素:如果有越多的资源被耗费在锁的管理和调度上,那么应用程序得到的资源就越少。锁的实现方式越好,将需要越少的系统调用和上下文切换,并且在共享内存总线上的内存同步通信量也越少,而一些耗时的操作将占用应用程序的计算资源。

Java6使用了改进的算法来管理内置锁,与在ReentrantLock中使用的算法类似,该算法有效地提高了可伸缩性。

公平性

ReentrantLock的构造函数中提供了两种公平性选择:创建一个非公平的锁(默认)或者一个公平的锁。在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”:当一个线程请求非公平锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁(在Semaphore中同样可以选择采用公平的还是非公平的获取顺序)。非公平的ReentrantLock并不提倡“插队”行为,但无法防止某个线程在合适的时候进行“插队”。在公平的锁中,如果有另一个线程等待持有这个锁或者有其他线程在队列中等待这个锁,那么新发出请求的线程将被放入队列中。在非公平的锁中,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。

在大多数情况下,非公平锁的性能要高于公平锁的性能。但是要确保被阻塞的线程最终能够获得锁,也就可以了。

在synchronized和ReentrantLock之间进行选择

ReentrantLock在加锁和内存上提供的语义与内置锁相同,此外它还停供了一些其他的功能,包括定时的锁等待、可中断的锁等待、公平性,以及实现非块结构的加锁。ReentrantLock在性能上似乎优于内置锁。但内置锁简单,不易出错。

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

读—写锁

ReentrantLock实现了一种标准的互斥锁:每次最多只有一个线程能够持有ReentrantLock。互斥是一种保守的加锁策略,虽然说可以避免“写/写”冲突和“写/读”冲突,但同样避免了“读/读”冲突。

ReadWriteLock中暴露了两个Lock对象,其中一个用于读操作,而另一个用于写操作。要读取由ReadWriteLock保护的数据,必须首先获得读取锁,当需要修改ReadWriteLock保护的数据时,必须首先获得写入锁。尽管这两个锁看上去是彼此独立的,但读取锁和写入锁只是读—写锁对象的不同视图。

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

在读——写锁实现的加锁策略中,允许多个读操作同时进行,但每次只允许一个写操作。与Lock一样,ReadWriteLock可以采用多种不同的实现方式,这些方式在性能、调度保证、获取优先性、公平性以及加锁语义等方面可能有所不同。

读——写锁是一种性能优化措施,在一些特定情况下能实现更高的并发性。在实际情况中,对于在多处理器系统上被频繁读取的数据结构,读——写锁能够提高性能。而在其他情况下,读——写锁的性能比独占锁的性能略差一些,这是因为它们的复杂度更高。如果要判断在某种情况下使用读——写锁是否会带来性能提升,最好对程序进行分析。由于ReadWriteLock使用Lock来实现锁的读——写部分,因此如果分析结果表明读-写锁没有提高性能,那么可以很容易地将读-写锁换成独占锁。

在读取锁和写入锁之间的交互可以采用多种实现方式。ReadWriteLock中的一些可选实现包括:

  • 释放优先
  • 读线程插队
  • 重入性
  • 降级
  • 升级

ReentrantReadWriteLock为这两种锁都提供了可重入的加锁语义。与ReentrantLock类似,ReentrantReadWriteLock在构造时也可以选择是一个公平还是非公平锁。

ReentrantReadWriteLock中的写入锁只能有唯一的所有者,并且只能由获得该锁的线程来释放。

results matching ""

    No results matching ""