在Java5.0增加新的机制:ReentrantLock。ReentrantLock并不是一种替代内置加锁的方法,而是内置加锁不适用时,作为一种可选择的高级功能。
13.1 Lock与ReentrantLock
Lock提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显示的。在Lock的实现中,必须提供与内部锁相同的内存可见性语义,但在加锁语义,调度算法,顺序保证以及性能特性等方面可以有所不同。
https://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/locks/Lock.html#method_summary
ReentrantLock与Synchronized相同点:
- 两者相同的互斥性和内存可见性
- 有相同的内存语义,同样有着与退出同步代码块相同的内存语义(3.1节和16章介绍内存可见性)
- 都提供了可重入的加锁语义(2.3.2节)
不同点:
- ReentrantLock为处理锁的不可用性提供了更高的支持。
内置锁的局限性:
- 无法中断一个正在等待获取锁的线程或者无法在请求获取一个锁时无限地等待下去。
- 无法实现非阻塞的加锁规则
Lock lock = new ReentrantLock();
...
lock.lock();
try {
// 更新对象状态
// 捕获异常,并在必要时恢复不变性条件
} finally {
lock.unlock();
}
程序清单 13-2 使用ReentrantLock来保护对象状态
13-2给出了Lock接口的标准使用形式:必须在finally块中释放锁。否则,如果在被保护的代码中抛出了异常,那么这个锁永远都无法释放。
当使用这种形式的加锁时,包括内置锁,都应该考虑在出现异常时的情况。
13.1.1 轮询锁和定时锁 - DeadLockAvoidance,TimedLocking
通过tryLock方法可实现轮询锁和定时锁。
13.1.2 可中断的锁获取操作 - InterruptibleLocking
13.1.3 非块结构的加锁
在内置锁中,锁的获取和释放等操作都是基于代码块的—释放锁的操作总是与获取锁的操作处于同一个代码块,而不考虑控制权如何退出该代码块。自动的锁释放操作简化了对程序的分析,避免了可能的编码错误,但有时候需要更灵活的加锁规则。
在第11章中,我们看到了通过降低锁的粒度可以提高代码的可伸缩性。锁分段技术在基于散列的容器中实现了不同的散列链,以便使用不同的锁。我们可以通过采用类似的原则来降低链表中锁的粒度,即为每个链表节点使用一个独立的锁,使不同的线程能独立地对链表的不同部分进行操作。每个节点的锁将保护链接指针以及在该节点中存储的数据,因此当遍历或修改链表时,我们必须持有该节点上的这个锁,直到获得了下一个节点的锁,只有这样,才能释放前一个节点上的锁。在[CPJ 2.5.1 4]中介绍了使用这项技术的一个示例,并称之为连锁式加锁(Hand-Over-Hand Locking)或者锁耦合(Lock Coupling)。
13.2 性能考虑因素
性能是一个不断变化的指标,如果在昨天的测试基准中发现X比Y更快,那么在今天就可能已经过时。
13.3 公平性
ReentrantLock的构造函数提供两种公平性选择:
- 创建非公平的锁(默认)
- 创建公平的锁
公平锁:
- 在公平的锁上,线程将按照它们发出请求的顺序来获得锁
非公平锁:
- 允许“插队”:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁。
其他公平相关的锁:
- 在Semaphore中同样可以选择采用公平的或非公平的获取顺序。
我们为什么不希望所有的锁都是公平的?毕竟,公平是一种好的行为,而不公平则是一种不好的行为,对不对?当执行加锁操作时,公平性将由于在挂起线程和恢复线程时存在的开销而极大地降低性能。在实际情况中,统计上的公平性保证一确保被阻塞的线程能最终获得锁,通常已经够用了,并且实际开销也小得多。有些算法依赖于公平的排队算法以确保它们的正确性,但这些算法并不常见。在大多数情况下,非公平锁的性能要高于公平锁的性能。
—— 个人对该段总结 非公平锁的性能要高于公平锁的性能。
13.4 在 synchronized 和 ReentrantLock 之间选择
在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的、可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。
synchronized是JVM的内置属性,而ReentrantLock只是基于类库的锁
13.5 读-写锁
读写锁文档
https://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/locks/ReadWriteLock.html#method_summary
但对于维护数据的完整性来说,互斥通常是一种过于强硬的加锁规则,因此也就不必要地限制了并发性。互斥是一种保守的加锁策略,虽然可以避免“写/写”冲突和“写/读”冲突,但同样也避免了“读/读”冲突。在许多情况下,数据结构上的操作都是“读操作”—虽然它们也是可变的并且在某些情况下被修改,但其中大多数访问操作都是读操作。
—- 个人对该段总结 读写锁的使用场景,对于读/读操作偏多的场景中,可用读-写锁
在读取锁和写入锁之间的交互可以采用多种实现方式。ReadWriteLock中的一些可选实现
包括:
- 释放优先。当一个写人操作释放写人锁时,并且队列中同时存在读线程和写线程,那么应该优先选择读线程,写线程,还是最先发出请求的线程?
- 读线程插队。如果锁是由读线程持有,但有写线程正在等待,那么新到达的读线程能否立即获得访问权,还是应该在写线程后面等待?如果允许读线程插队到写线程之前,那么将提高并发性,但却可能造成写线程发生饥饿问题。
- 重入性。读取锁和写入锁是否是可重人的?
- 降级。如果一个线程持有写人锁,那么它能否在不释放该锁的情况下获得读取锁?这可能会使得写入锁被“降级”为读取锁,同时不允许其他写线程修改被保护的资源。
- 升级。读取锁能否优先于其他正在等待的读线程和写线程而升级为一个写入锁?在大多数的读-写锁实现中并不支持升级,因为如果没有显式的升级操作,那么很容易造成死锁。(如果两个读线程试图同时升级为写人锁,那么二者都不会释放读取锁。)
小节:
与内置锁相比,显式的Lock提供了一些扩展功能,在处理锁的不可用性方面有着更高的灵活性,并且对队列行有着更好的控制。但ReentrantLock不能完全替代synchronized,只有在synchronized无法满足需求时,才应该使用它。
读一写锁允许多个读线程并发地访问被保护的对象,当访问以读取操作为主的数据结构时,它能提高程序的可伸缩性。