第13章 显示锁
终于看到了这本书的最后一本分,呼呼呼,真不容易。其实说实在的,我不喜欢半途而废,有其开始,就一定要有结束,否则的话就感觉哪里乖乖的。
java5.0之前,在协调对共享对象的访问时可以使用的机制只有synchronized和volatile。java5.0增加了一种新的机制:ReentrantLock。与之前提到过的机制相反,ReentrantLock并不是一种替代内置锁的方法,而是当内置锁机制不适用时,作为一种可选择的高级功能。
13.1 Lock与ReentrantLock(p227)
程序清单13-1中给出的Lock接口定义了一组抽象的加锁操作。与内置锁机制不同的是,Lock提供了一种无条件的,可轮询的,定时的以及可中断的锁获取操作,所有加锁和解锁操作都是显示的。
ReentrantLock实现了Lock接口(Reentrant:再进去,凹角,再进去的,凹角的),并提供了与synchronized相同的互斥性和内存可见性。ReentrantLock支持在Lock接口中定义的所有获取锁模式,并且与syncronized相比,他为处理锁的不可用性提供了更高的灵活性。
书上一段话就截了这部分,感觉这段有点翻译错误。程序清单13-2给出了Lock接口的标准使用形式。这种形式比使用内置锁复杂一些,必须在finally块中释放锁,否则如果早被保护的代码中抛出了异常,那么这个锁永远都无法释放。
ReentrantLock不能完全替代syncronized的原因:它更加危险,因为当程序的执行控制离开被保护的代码块时,不会自动清楚。虽然在finally块中释放锁并不困难,但也可能忘记。
13.1.1 轮询锁与定时锁(p228)
可定时的与可轮询的锁获取模式是由tryLock方法实现的,与无条件的锁获取模式相比,它具有更完善的错误恢复机制。如果不能获得所有需要的锁,那么可以使用可定时的锁或可轮询的锁获取方式,从而使你重新获得控制权,它会释放已经获得的锁,然后重新尝试获取所有锁。程序清单13-3给出了另一种方法来解决10.1.2节中动态顺序死锁的问题:使用tryLock来获取两个锁,如果不能同时获得,那么就回退并重新尝试。
在实现具有时间限制的操作时,定时锁同样非常有用。当在带时间限制的操作中调用了一个阻塞方法时,它能根据剩余时间来提供一个时限。如果操作不能在指定时间内给出结果,那么使程序提前结束。当使用内置锁时,在开始请求锁后,这个锁操作将无法取消,因此内置锁很难实现带有时间限制的操作。
程序清单6-17的旅游门户网站示例中,为询价的每个汽车租赁公司都创建另一个独立的任务。询价操作包含某种基于网络的请求机制,例如web请求。但在询价操作中同样可能需要实现对紧缺资源的独占访问,例如通向公司的直连线路。9.5节介绍了确保对资源进行串行访问的方法:一个单线程的Executor。另一种方法是使用一个独占锁来保护对资源的访问。程序清单13-4试图在Lock保护的共享通信线路上发送一条消息,如果不能在指定时间完成,代码就会失败。定时的tryLock能够在这种带有时间限制的操作中实现独占行为。
13.1.2 可中断的锁获取操作(p230)
可中断的锁获取操作能在可取消的操作中使用加锁。7..6节给出了几种不能响应中断的机制,例如请求内置锁。这些不可中断的阻塞机制将使得实现可取消的任务变得复杂。lockInterruptibly方法能够在获得锁的同时保持对中断的响应,并且由于它包含在Lock中,因此无须创建其它类型的不可中断阻塞机制。来个例子:
13.1.3 非块结构加锁(p231)
采用11章中的锁分段技术来降低链表中锁的粒度,为每个链表节点使用一个独立的锁,使不同的线程能独立地对链表的不同部分进行操作。每个节点的锁将保护链接指针以及在该节点中存储的数据,只有这样,才能释放前一个节点上的锁。(连锁式加锁(Hand-Over-Hand Locking)||锁耦合(Lock Coupling))
13.2 性能考虑因素
上图的原因是:java 6使用了改进后的算法来管理内置锁,使得内置锁和ReentrantLock在java6上的性能差异不是很大,而在java 5上就差很多了。
13.3 公平性
在ReentrantLock的构造函数中提供了两种公平性的选择:创建一个非公平的锁(默认)或者一个公平的锁。在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有等待线程并获得这个锁。(在Semaphore中同样可以选择采用公平或非公平的获取顺序)。在公平的锁中,如果有另一个线程持有这个锁或者有其他线程在队列中等待这个锁,那么新发出请求的线程将被放入队列中。在非公平的锁中,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。(即使对于公平锁而言,可轮询的tryLock任然会“插队”)
图13-2给出了Map的性能测试,并比较由公平的以及非公平的ReentrantLock包装的HashMap性能。从图中可以看出,公平性把性能降低了越两个数量级。不必要的话,不要为公平性付出代价。
13.4 在synchronized和ReentrantLock之间进行选择(p234)
ReentrantLock在性能上似乎优于内置锁,但与显示锁相比,内置锁仍然具有很大的优势。内置锁为许多开发人员所熟悉,并且简介紧凑,而且许多现有的程序都已经使用了内置锁,如果将这两种机制混合使用,那么不仅容易令人困惑,也容易发生错误。
13.5 读 — 写锁
ReentrantLock实现了一种标准的互斥锁:每次只能有一个线程能持有ReentrantLock。但对于维护数据完整性来说,互斥锁通常是一种过于强硬的加锁规则,因此也就不必要地限制了并发性。
意思就是这个锁允许的情况只有两种:要么全是读操作,要么就是一个写操作,两者不能同时进行。
在程序清单13-6的ReadWriteLock中暴露了两个Lock对象,其中一个用于读操作,而另一个用于写操作。要读取由ReadWriteLock保护的数据,必须首先获得读取锁,当需要修改ReadWriteLock保护的数据时,必须首先获得写入锁。尽管这两个锁看上去是彼此独立的,但读取锁和写入锁知识 读—写 锁对象的不同视图。
在 读 — 写 锁实现的加锁策略中,允许多个读操作同时进行,但每次只允许一个写操作。与Lock一样,ReadWriteLock可以采用多种不同的实现方式,这些方式在性能,调度保证,获取优先性,公平性以及加锁语义等方面可能有所不同。
在读取锁和写入锁之间的交互可以采用多种实现方式。ReadWriteLock中的一些可选实现包括:
当锁的持有是将较长并且大部分操作都不会修改被守护的资源时,那么读—写锁能提高并发性。在程序清单13-7的ReadWriteMap中使用了ReentrantReadWriteLock来包装Map,从而使它能在多个读线程之间被安全地共享,并且仍然能避免“读-写”或“写-写”冲突。在实现中,ConcurrentHashMap的性能已经很好了,一次如果只需要一个并发的基于散列映射,那么就可以使用ConcurrentHashMap来代替这种方法,但如果需要多另一种Map实现提供并发性更高的访问,那么可是使用这项技术。
图13-3给出了分别用ReentrantLock和ReadWriteLock来封装ArrayList的吞吐量比较,每个操作随机地选择一个值并在容器中查找这个值,并且只有少量的操作会修改这个容器中的内容。
小结: