Java 5.0之前,在协调线程对共享数据的访问时可以使用的机制只有synchronized和volatile两种,java 5.0增加了一种新的机制:ReentrantLock;但是ReentrantLock并不是代替内置锁,而是作为一种可选择的高级功能;
1:Lock与ReentrantLock
Lock接口定义了一组抽象的加锁操作,与内置加锁机制不同,Lock提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁方法都是显示的。
Lock的实现中必须提供与内置加锁相同的内存可见性语义,但是在加锁语义、顺序保证以及性能特性方面有所不同;
Method Summary | |
---|---|
void | lock() Acquires the lock. |
void | lockInterruptibly() Acquires the lock unless the current thread is interrupted. |
Condition | newCondition() Returns a new Condition instance that is bound to this Lock instance. |
boolean | tryLock() Acquires the lock only if it is free at the time of invocation. |
boolean | tryLock(long time, TimeUnit unit) Acquires the lock if it is free within the given waiting time and the current thread has not been interrupted. |
void | unlock() Releases the lock. |
ReentrantLock实现了Lock接口,并且提供与synchronized相同的互斥性和内存可见性,与synchronized提供了可重入的加锁语义;
ReentrantLock还提供了Lock接口定义的获取锁的模式,与synchronized相比,为处理锁不可用性问题提供了更高的灵活性;
为什么要创建一种与内置锁如此相似的新的加锁机制?在大多数情况下,内置锁都能很好的工作,但是在功能上存在一些局限性,例如,无法中断一个正在等待获取锁的线程、或者无法在请求获取一个锁时无限的等待下去。内置锁必须在获取该锁的代码块中释放,这就简化了编码工作,并且与异常处理操作实现了很好的交互,但却无法实现非阻塞结构的加锁规则;
1.1 轮询锁与定时锁
可定时的与可轮询的锁获取模式是由tryLock方法实现的,与无条件的锁获取方式相比,它具有跟完善的错误恢复机制。在内置锁中,死锁是一个严重的问题,恢复程序的唯一方法是重启程序,防止死锁的唯一方法是在构造程序时避免出现不一致的锁顺序。可定时与可轮询的锁提供了另外一种选择:避免死锁发生;
如果不能获得所需要的锁,那么可以使用可定时的活可轮询的锁获取方式,从而重新获得控制权,它会释放已经获得的锁,然后重试获取所有的锁。
1.2可中断的锁获取操作
内置锁不能响应中断,lockInterruptibly方法能够在获取锁的同时保持对中断的响应;
2:性能因素考虑
当把ReentrantLock加入到java 5.0时,他能比内置锁提供更好的竞争性能。对于同步原语来说,竞争性能是可伸缩性的关键因素:如果越多的资源消耗在锁的管理和调度上,那么应用程序得到的资源就越少。锁的实现方式越好,那么系统调用和上下文切换越少,并且在共享内存上内存同步通信量越少。
java6使用了改进后的算法来管理内置锁,与ReentrantLock中使用算法类似,有效的提高了性能。两者性能差距不是很大。
3:公平性
在ReentrantLock的构造函数中提供了两种公平性选择:创建一个非公平的锁(默认)或者一个公平的锁。公平锁,线程将按照请求顺序获得锁;非公平锁允许插队:当一个线程请求非公平锁时,如果发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有等待线程获得这个锁;
为什么不希望所有的锁都是公平的?当执行加锁操作时,公平性将由于挂起线程和恢复线程时存在的开销而极大的降低性能。
内置锁与ReentrantLock一样,默认不提供确定的公平性保证;
4:在synchronized和ReentrantLock之间进行选择
ReentrantLock与synchronized在加锁和内存上提供的语义与内置锁相同,此外还提供了一些其他功能,包括定时的锁等待、可中断的所等待、公平性。
ReentrantLock的性能在Java6.0中略有胜出,Java5.0中远远胜出;
内置锁其实也有优势:
现有许多程序都使用了内置锁;不需要显示的释放锁降低风险;此外,在线程转储中能给出在哪些调用帧中获得了哪些锁,并且能够检测和识别发生死锁的线程.
JVM并不知道哪些线程持有ReentrantLock,因此在调试使用ReentrantLock的线程问题时,将起不到帮助作用;
未来可能提升的时内置锁的性能,而不是ReentrantLock。synchronzed是JVM的内置属性,他能执行一些优化,例如对线程封闭的锁对象的锁消除优化,通过增加锁的粒度来消除内置锁的同步。
5:读-写锁
ReentrantLock实现了一种标准的互斥锁:每次最多一个线程持有ReentrantLock。但是对于维护数据的完整性来说,互斥通常是一种过于强硬的加锁规则,也会限制了并发性。
在许多情况下,数据结构上的操作大多都是“读操作”,如果能运行多个读操作的线程能同时访问数据结构,那么将大大提升程序性能;此时,多个读操作可以同时进行,或者只有一个写操作进行,但是读写操作不能够同时进行;
public interface ReadWriteLock{
<span style="white-space:pre"> </span>Lock readLock();
<span style="white-space:pre"> </span>Lock writeLock();
}
例子:
public class ReadWriteMap<K,V>{
<span style="white-space:pre"> </span>private final Map<K,V> map;
<span style="white-space:pre"> </span>private finalReentrantReadWriteLock lock=new ReentrantReadWriteLock();
<span style="white-space:pre"> </span>private final Lock r=lock.readLock();
<span style="white-space:pre"> </span>private final Lock w=lock.writeLock();
<span style="white-space:pre"> </span>public V put(K key,V value){
<span style="white-space:pre"> </span>w.lock();
<span style="white-space:pre"> </span>try{
<span style="white-space:pre"> </span>return map.put(key,value);
<span style="white-space:pre"> </span>}finally{
<span style="white-space:pre"> </span>w.unlock();
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>public V get(K key){
<span style="white-space:pre"> </span>r.lock();
<span style="white-space:pre"> </span>try{
<span style="white-space:pre"> </span>return map.get(key);
<span style="white-space:pre"> </span>}finally{
<span style="white-space:pre"> </span>r.unlock();
<span style="white-space:pre"> </span>}<span style="white-space:pre"> </span>
<span style="white-space:pre"> </span>}
}
6:总结
与内置锁相比,显式的Lock提供了一些扩展功能,在处理锁的不可用性方面有着更高的灵活性,并且对队列行有着更好的控制(意思应该是请求线程队列)。但是ReentantLock不能完全代替synchronized,只有在无法满足需求时才应该使用它;
读-写锁允许多个线程并发的访问被保护的对象,当访问以读取操作为主的数据结构时,他能提供程序的可伸缩性