在java5之前,在协调对共享对象的访问时可以使用的机制只有synchronized和volatile。5新增了一种机制,ReentrantLock。与之前的机制不同的是,ReentrantLock不是一种替代内置锁的机制,而是当内置锁机制不适用时,作为一种可选择的高级功能。
Lock
Lock是显式锁的抽象接口,提供了加锁、解锁等功能。它提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作。
ReentrantLock
它是Lock的实现,提供了与synchronized相同的互斥性和内存可见性。它也提供了可重入的加锁语义。但它比synchronized更加灵活。
使用显式锁的时候一定要记住,在finally块中释放锁。
轮询锁和定时锁
可定时和可轮询的锁模式是通过tryLock方法来实现。于无条件的锁获取机制,它具有更完善的错误恢复机制。
在内置锁中,死锁是一个严重的问题,恢复程序的唯一方法就是重新启动程序,而防止死锁的唯一方法就是在构造程序时避免出现不一致的锁顺序。可定时与可轮询的锁提供了另一种选择:避免死锁的发生。
可中断的锁获取操作
Lock中提供的lockInterruptibly方法能够在获得锁的同时保持对中断的响应。
可中断的锁获取操作的标准结构比普通的锁获取操作略微复杂一些,因为需要两个try块,但是如果在可中断的锁获取操作中抛出了InterruptedException,那么可以使用标准的try-finally加锁模式。
定时的tryLock同样也支持中断操作。
公平性
ReentrantLock的构造器提供了两种公平性选择:创建非公平的锁(默认)或者一个公平锁。
在公平锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”:当一个线程请求非公平锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁。
其实非公平的ReentrantLock并不提倡“插队”。在公平锁中,如果有另一个线程持有这个锁或者有其他线程在队列中等待这个锁,那么新发出的请求的线程将被放入队列中。非公平的锁中,只有当锁被某个线程持有时,新发出的请求的线程才会被放入队列中。
多数情况下,非公平锁的性能要高于公平锁。原因就在于,在激烈竞争的情况下,在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。
当持有锁的时间相对较长,或者请求锁的平均时间间隔较长,那么应该使用公平锁。
在synchronized和ReentrantLock之间进行选择
仅当内置锁不能满足需求时,才可以考虑使用ReentrantLock。一般在使用:可定时、可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁时才可以考虑使用ReentrantLock,其他情况还是使用synchronized。
而且synchronized是JVM的内置属性,它能执行一些优化。ReentrantLock是通过基于类库的锁来实现这些功能,以后性能提升的可能性不大。
读-写锁
ReentrantLock是一种标准的独占锁,每次最多只能有一个线程持有ReentrantLock。它可以很好的保护“写-写”、“写-读”和“读-读”的情况,但实际情况中,多数操作都是读操作,独占的控制显然影响了效率。
所以提出了读-写锁,一个资源可以同时被多个读操作访问或者被一个写操作访问,但是读或写不能同时进行。
ReadWriteLock同时提供了两个Lock,一个用于读操作,一个用于写操作。
ReentrantReadWriteLock实现了ReadWriteLock,同样可以选择是非公平锁(默认)还是公平锁。
对于公平锁,等待时间最长的线程将优先获得锁。如果这个锁由读线程持有,而另一个线程请求写入锁,那么其他读线程都不能获得读取锁,直到写线程使用完并释放了写入锁。
非公平锁,线程获得访问许可的顺序是不确定的。
当锁的持有时间较长并且大部分操作都不会修改被守护的资源时,读-写锁能提高并发性。