显示锁
为了保证共享对象的安全性,常用的机制有:
- 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。例如:可定时的、可轮询的、可中断的锁获取操作;公平队列以及非块结构的锁。
1.9 读写锁
ReentrantLock和内置锁都是标准的互斥锁,但是互斥锁规则太过于强硬。互斥锁虽然避免了“写/写”操作,“写/读操作”,但是也避免了“读/读操作”。
为了提高并发操作,允许多个线程同时访问共享数据,就可以使用"读/写"锁(一个资源可以被多个读操作访问,或者被一个写操作访问,但是两者不能同时进行)。
读写锁除了保证对读线程的可见性之外以及提升并发性之外,还能简化读写场景。
例如:
应用程序中存在一个共享的数据结构,大多数情况下,读操作多于写操作。之前我们会使用java的等待通知机制,即写操作开始时,其它操作会进入等待状态,只有写状态结束后才会被唤醒。
读写锁的使用场景适合于读操作需求远大于写操作需求时。
1.9.1 读写锁的实现
java提供的读写锁实现是ReentrantReadWriteLock。
- ReentrantReadWriteLock支持公平锁和非公平锁
- 写锁可以降级为读锁,但是读锁不能升级为写锁。
- ReentrantReadWriteLock支持可重入。读锁获取了读锁后可再次获取读锁,同理,写锁获取了写锁后也可以再次获取。
1.9.2 使用读写锁的demo
使用读写锁保证HashMap线程安全,提高读操作的并发性,保证可见性。
public class ReadWriteMap <K ,V> {
private final Map<K, V> map;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock rLock = lock.readLock();
private final Lock wLock = lock.writeLock();
public ReadWriteMap (Map<K, V> map) {
this.map = map;
}
public V put (K key ,V value) {
wLock.lock();
try {
return map.put(key, value);
} finally {
wLock.unlock();
}
}
public V get (Object key) {
rLock.lock();
try {
return map.get(key);
} finally {
rLock.unlock();
}
}
public void show () {
for (Map.Entry<K, V> entry : map.entrySet()) {
System.out.println(entry.getKey() + " "+entry.getValue());
}
System.out.println(map.size());
}
public static void main(String[] args) throws InterruptedException {
final CountDownLatch begin = new CountDownLatch(1);
final CountDownLatch end = new CountDownLatch(1000);
final Map map = new HashMap();
final ReadWriteMap readWriteMap = new ReadWriteMap(map);
for (int i =0 ;i<1000;i ++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
begin.await();
Random rand = new Random();
int nextInt = rand.nextInt(1000);
readWriteMap.put("key"+nextInt, "value"+nextInt);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
end.countDown();
}
}
});
thread.start();
}
begin.countDown();
end.await();//阻塞主线程
readWriteMap.show();
}
}