显示锁
为了保证共享对象的安全性,常用的机制有:
- volatile 关键字
- synchronized
- ReentrantLock 显示锁
1.1 ReentrantLock
ReentrantLock实现了Lock接口。Lock接口定义一组抽象的加锁操作。
Lock提供了一种无条件的、可轮询的、定时的、以及可中断的锁获取操作。
所有的加锁、解锁操作都是显示的。在Lock的实现中必须提供与内部锁相同的内存可见语义性,但是在加锁语义、调度算法、顺序保证以及性能特性方面可以不同。
ReentrantLock和synchronized的共同点:都支持可重入锁。
1.2 为什么需要ReentrantLock锁?
- 无法中断一个正在等待获取锁的线程、
- 无法再请求获取一个锁时无限地等待下去。
- 内置锁必须在获取该锁的代码块中释放,无法实现非租塞结构的加锁规则。
ReentrantLock的标准用法:
public static void main(String[] args) {
Lock lock = new ReentrantLock();
lock.lock();
try {
// 这里是你的逻辑... 捕获并处理异常
} finally {
// 如果没有使用finally释放锁,会相当危险,不像synchronize,程序离开控制块时候,会自动释放锁。
lock.unlock();
}
}
1.3 可定时锁和轮询锁
在内置锁中,防止死锁的唯一方式是在构建程序时避免出现不一样的锁顺序。
但是,可定时和可轮询的锁提供了另一种防止死锁的机制:
如果不能获得全部的锁,那么可以使用可以定时的或者是可轮询的锁获取方式。它会释放已经获得的锁,然后重新尝试获取其它锁。
定时锁:在带有时间限制的操作中调用一个阻塞方法,它能根据剩余时间提供一个时限,如果操作不能在指定时间内给出结果,那么程序会提前结束。然后使用内置锁时,开始请求锁操作后,这个操作无法取消。
定时锁的API:
//尝试非阻塞的获取锁,调用该方法后立刻返回,如果获得锁返回true,否则返回false
boolean tryLock();
//超时的获取锁,当前线程在三种情况下会返回:
//1、线程在超时时间内获得了锁。
//2、超时时间结束,返回false。
//3、线程在超时时间内被中断。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
public class ReentrantLockTest {
static Lock lock = new ReentrantLock();
/**
* 测试非阻塞的获取锁
* 避免死锁 ,如果在规定的时间内不能获得锁,自动释放已经获得的其它锁。
* @param time
* @param unit
* @throws InterruptedException
*/
public static void testTryLock(long time ,TimeUnit unit) throws InterruptedException {
boolean tryLock = lock.tryLock(time, unit);
System.out.println(Thread.currentThread().getName()+":"+tryLock);
try {
Thread.currentThread().sleep(10000);
} finally {
// 如果获取了锁 释放锁 ,如果没有获得锁 ,就释放锁 会报错。
if (tryLock) {
lock.unlock();
}
}
}
//两个线程同时去争抢一个锁,一个会获得锁,另一个会超时返回flase。
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
try {
testTryLock(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread b = new Thread(new Runnable() {
@Override
public void run() {
try {
testTryLock(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
a.setName("a");
b.setName("b");
a.start();
b.start();
}
}
在我的机器上输出的结果是:
b:true
a:false
1.4 可中断的锁获取操作
对应的java API操作是:
// 可中断的获取锁,与lock方法不同的是该方法可以响应中断,在获取锁的过程中可以中断当前线程。
void lockInterruptibly() throws InterruptedException;
为什么需要可中断的所获取操作 ?
内置锁,拥有不可中断的阻塞机制使得实现可取消的任务变得十分复杂。lockInterruptibly()方法能够在获取锁的同时保持对中断的响应,并且它包含于Lock中,因此不需要创建其它类型的不可中断机制。
1.5 非块结构的加锁
在内置锁中,锁的获取操作和释放操作都是基于代码块的。
链式加锁(又称锁耦合)当遍历或修改链表时,我们必须持有该节点上的这个锁,直到获取了下一个节点的锁,只有这样才能释放前一个节点的锁。
1.6 吞吐量
在java5中ReentrantLock比内置锁的吞吐量要高出许多,但在java6中二者很接近。
1.7 公平性
ReentrantLock支持公平性的锁和非公平性的锁。
- 公平锁 :线程按照它们的请求顺序来获得锁。
- 非公平锁:当一个线程请求锁的时候,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程二获取锁。
大多数情况下,非公平性的锁性能好于公平性锁的性能。
为什么非公平性锁的性能要优于公平性锁呢?
在恢复一个被挂起的线程时与该线程真正开始运行之间存在严重的延迟。(说白了就是存在上下文切换的开销,非公平锁从另一个思路避免这种开销。同样我们也能想到偏向锁,也是这样一个思路:偏向进程会一直持有这个锁,直到发生竞争才释放掉。这个思路不也是为了减少上下文切换的开销吗?)
对于公平锁而言,可轮询的tryLock依然会“插队”。
1.8 如何选择synchronized 和 ReentrantLock ?
synchronized 是JVM的内置属性,随着jdk版本的提高可能会不断被优化,而ReentrantLock是基于java类库实现的,被优化的可能性不高。
ReentrantLock 可以作为一种高级工具,当synchronized无法满足需求时使用ReentrantLock。例如:可定时的、可轮询的、可中断的锁获取操作;公平队列以及非块结构的锁。