显式锁
Java 5之前,在协调共享对象的访问时可以使用的机制只有
synchronized
和volatile
。Java 5增加了ReentrantLock
。ReentrantLock
并不是一种替代内置加锁的方法,而是当内置锁机制不适用时,作为一种可选择的高级功能。
Lock 与 ReentrantLock
Lock 提供了一中无条件的、可轮询的、定时的以及可中断的所获取操作,所有加锁和解锁的方法都是显式的。
在Lock的实现中必须提供与内部锁相同的内存可见性语义,但在加锁语义、调度算法、顺序保证以及性能特性等方面可以有所不同。
package java.util.concurrent.locks;
/**
* @see ReentrantLock
* @see Condition
* @see ReadWriteLock
*
* @since 1.5
* @author Doug Lea
*/
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
相同的互斥行和内存可见性。并且与synchronized
一样,ReentrantLock
还提供了可重入(可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。)
的加锁语义。
为什么要和创建一种与内存锁如此相似的新加锁机制?
==在大多数情况下,内置锁都能很好地工作,但在功能上存在一些局限性。例如,无法中断一个正在等待获取锁的线程,或者无法在请求一个锁时无限地等待下去。
内置锁必须在获取该锁的代码块中释放,这就简化了编码工作,并且与异常处理操作实现了很好的交互,但却无法实现非阻塞结构的加锁规则。这些都是使用synchronized
的原因,但是在某些情况下,一种更灵活的加锁机制通常能够提供更好的活跃性或性能。==
显式调用Lock,必须在finally中释放锁,虽然在finally中释放锁并不困难,但也可能忘记。
轮询锁与定时锁
可定时的与可轮询的锁获取模式是由
tryLock
方法实现的,与无条件的锁获取模式相比,它具有更完善的错误恢复机制。
可中断的锁获取操作
lockInterruptibly
方法能够在获得锁的同时保持对中断的响应。
代码在Log下面!!!!!
lock()方法打印:线程1获取不到锁后会一直等待锁的释放,并且不会响应中断,当线程0释放锁后,线程1才恢复对中断的响应。
Thread-0:start get lock
Thread-0:already get lock
Thread-1:start get lock
Thread-0:working num 0
Thread-0:working num 1
Thread-0:working num 2
Thread-0:working num 3
Thread-0:working num 4
Thread-0:working num 5
Thread-0: release unlock
Thread-1:already get lock
Thread-1:Interrupt
Thread-1: release unlock
lockInterruptibly()方法打印:线程1在获取不到锁后能够及时响应中断。
Thread-0:start get lock
Thread-0:already get lock
Thread-1:start get lock
Thread-1:Interrupt
Thread-1: unlock failed
Thread-1: failed desc:null
Thread-0:working num 0
Thread-0:working num 1
Thread-0:working num 2
Thread-0:working num 3
Thread-0:working num 4
Thread-0:working num 5
Thread-0: release unlock
跑一下示例代码就清楚lockInterruptibly
和lock
的区别了。
public static void main(String[] args) throws InterruptedException {
LockTest lockTest = new LockTest();
Thread t0 = new Thread(new Runnable(){
@Override
public void run() {
lockTest.doWork();
}
});
Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
lockTest.doWork();
}
});
// 启动线程t1
t0.start();
Thread.sleep(10);
// 启动线程t2
t1.start();
Thread.sleep(100);
// 线程t1没有得到锁,中断t1的等待
t1.interrupt();
}
class LockTest {
private Lock lock = new ReentrantLock();
public void doWork() {
String name = Thread.currentThread().getName();
try {
System.out.println(name + ":start get lock");
//lock.lock();
lock.lockInterruptibly();
System.out.println(name + ":already get lock");
for (int i = 0; i < 6; i++) {
Thread.sleep(1000);
System.out.println(name + ":working num "+ i);
}
} catch (InterruptedException e) {
System.out.println(name + ":Interrupt");
}finally{
try {
lock.unlock();
System.out.println(name + ": release unlock");
} catch (Exception e) {
System.out.println(name + ": unlock failed");
System.out.println(name + ": failed desc:" + e.getMessage());
}
}
}
}
性能考虑因素
在Java 5 新增ReentrantLock
时,它能比内置锁提供更好的竞争性能。Java 6 使用了改进后的算法来管理内置锁,使得内置锁与ReentrantLock
在吞吐量上相差无几,二者的可伸缩性基本相当。
公平性
在
ReentrantLock
的构造函数中提供了两种公平性选择:创建一个非公平的锁(默认)或者一个公平的锁。
- 公平的锁:线程按照它们发出请求的顺序来获得锁。
- 非公平的锁:如果线程在发出请求的同时该锁的状态变为可用,那么这个线程将跳过所有等待的线程并获得这个锁。
在公平锁中,如果有另一个线程正在持有锁或者有其他线程在队列中等待这个锁,那么新发出请求的线程将被放入队列中。在非公平的锁中,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。
我们为什么不希望所有的锁都是公平的?
==当执行加锁操作时,使用公平的锁在挂起线程和恢复线程时产生的开销会极大地降低性能。而且在实际情况中,统计上的公平性保证(确保被阻塞的线程能最终获得锁),通常已经够用了,并且产生的开销会小很多。有些依赖于公平的排队算法来保证业务的正确性,但这些算法并不常见。在大多数情况下,非公平锁的性能要高于公平锁的性能。==
在synchronized
和ReentrantLock
之间的选择
ReentrantLock
在加锁和内存上提供的语义与内置锁相同,此外它还提供了一些其他功能:定时的锁等待、可中断的锁等待、公平性、非块结构的加锁。
与显式锁相比,内置锁仍然具有很大的优势。内置锁更为开发者熟悉,并且简洁紧凑。ReentrantLock
的危险性比同步机制要高,如果忘记在finally
块中调用unlock()
,实际上已经埋下了一颗定时炸弹。
在一些内置锁无法满足需求的情况下,ReentrantLock
可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock
,这些功能包括:可定时的、可轮询的与可中断的所获取操作,公平队列,以及非块结构的锁。否则,还是优先使用synchronized
。
读-写锁
ReentrantLock
实现了一种标准的互斥锁:每次最多只有一个线程持有ReentrantLock
。但对于维护数据的完整性来说,互斥通常是一种过于强硬的加锁规则,因此也就限制了并发性。互斥是一种保守的加锁策略,虽然可以避免写/写
和写/读
冲突,但是也避免了读/读
冲突。因此如果放宽了读/读
情况的加锁需求,那么将提升程序的性能。在这种情况下就有了读-写锁
:一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
在读-写锁
的加锁策略中,允许多个读操作同时进行,但每次只允许一个写操作。
ReentrantReadWriteLock
为这两种锁都提供了可重人的加锁语义。与ReentrantLock
类似,
ReentrantReadWriteLock
在构造时也可以选择是一一个非公平的锁(默认)还是一个公平的锁。
- 在公平的锁中,等待时间最长的线程将优先获得锁。如果这个锁由读线程持有,而另一个线程请求写入锁,那么其他读线程都不能获得读取锁,直到写线程使用完并且释放了写人锁。
- 在非公平的锁中,线程获得访问许可的顺序是不确定的。写线程降级为读线程是可以的,但从读线程升级为写线程则是不可以的(这样做会导致死锁)。
读写锁代码示例:
public class ReadWriteMap{
private final Map map;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock read = lock.readLock();
private final Lock write = lock.writeLock();
public ReadWriteMap(Map map){
this.map = map;
}
public V put(K key,V value){
write.lock();
try {
return map.put(key, value);
}finally{
write.unlock();
}
}
public V get(Object key){
read.lock();
try {
return map.get(key);
}finally{
read.unlock();
}
}
}
与内置锁相比,显式的Lock提供了一些扩展功能,有着更高的灵活性。灵活性,并且对队列行有着更好的控制。但ReentrantLock
不能完全替代synchronized
,只有在synchronized
无法满足需求时,才应该使用它。
读-写锁
允许多个读线程并发地访问被保护的对象,当访问以读取操作为主的数据结构时,它能提高程序的可伸缩性。