java中的显示锁Lock
java中的显示锁Lock
什么是显示锁
在Java 1.5之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile两种。Java1.5增加了一种新的机制,Lock,Lock是一个接口,提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有的加锁和解锁操作方法都是显示的,因而称为显示锁,Lock并不是替代内置加锁的方法,而是当内置加锁机制不适用时,作为一种可选择的高级功能。
Lock和synchronized的比较
- synchronized代码更简洁
- Lock可以在获取锁可以被中断,超时获取锁,尝试获取锁
- synchronized在1.8以前是性能低下的,到了1.8之后经过改良,性能基本行和Lock相持平,如果不是特殊场景推荐使用synchronized。
Lock 的实现类
- ReentrantLock
- ReentrantReadWriteLock
Lock的API
public interface Lock {
//获取锁
void lock();
//可中断锁,在获取锁的过程中可以中断线程
void lockInterruptibly() throws InterruptedException;
//尝试获取锁,如果成功返回true,否则返回false
boolean tryLock();
//超时获取锁,
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁
void unlock();
//获取Condition 对象
Condition newCondition();
}
Lock 使用的标准范式
//加锁
lock.lock();
try {
//todo 需要加锁的代码
} finally {
//释放锁
lock.unlock();
}
Condition接口
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。
public interface Condition {
//使当前线程在接收到信号前或被中断前一直保持等待状态.
void await() throws InterruptedException;
//使当前线程在接收到信号前或被中断前或达到指定时间前一直保持等待状态(TimeUnit为时间单位).
boolean await(long time, TimeUnit unit) throws InterruptedException;
//使当前线程在接收到信号前或被中断前或达到指定时间前一直保持等待状态(单位为毫秒).
long awaitNanos(long nanosTimeout) throws InterruptedException;
//使当前线程在接收到信号前或被中断前或达到最后日期期限前一直保持等待状态.
boolean awaitUntil(Date deadline) throws InterruptedException;
//是当前线程进入等待状态,对中断不敏感
void awaitUninterruptibly();
//唤醒一个在该Condition实例等待的线程.
void signal();
//唤醒所有在该Condition实例等待的线程.
void signalAll();
}
Condition 使用的标准范式
/**
* 创建一个显示锁对象
*/
private Lock lock = new ReentrantLock();
/**
* 创建 Condition 对象
*/
private Condition condition = lock.newCondition();
/**
* 等待方法
*
* @throws InterruptedException
*/
public void conditionWait() throws InterruptedException {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
/**
* 唤醒方法
*
* @throws InterruptedException
*/
public void conditionSignal() throws InterruptedException {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
ReentrantLock
锁可重入
简单地讲就是:“同一个线程对于已经获得到的锁,可以多次继续申请到该锁的使用权”。而synchronized关键字隐式的支持重进入,比如一个synchronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁。ReentrantLock在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。
公平和非公平锁
如果在时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。 ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。事实上,公平的锁机制往往没有非公平的效率高。
在激烈竞争的情况下,非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程A持有一个锁,并且线程B请求这个锁。由于这个锁已被线程A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此会再次尝试获取锁。与此同时,如果C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样的情况是一种“双赢”的局面:B获得锁的时刻并没有推迟,C更早地获得了锁,并且吞吐量也获得了提高。
使用示例
/**
* 创建线程池
*/
private static ExecutorService executorService = Executors.newCachedThreadPool();
/**
* 创建一个显示锁对象
*/
private static Lock lock = new ReentrantLock();
/**
* 创建 Condition 对象
*/
private static Condition condition = lock.newCondition();
/**
* 生产者
*/
private static void handel() {
System.out.println("尝试获取锁,线程:" + Thread.currentThread().getId());
lock.lock();
System.out.println("获取锁成功,线程:" + Thread.currentThread().getId());
try {
System.out.println("使线程进入等待状态,线程:" + Thread.currentThread().getId());
//进入等待状态并释放所资源
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("释放锁,线程:" + Thread.currentThread().getId());
}
}
/**
* 通知全部
*/
private static void latch() {
lock.lock();
try {
condition.signalAll();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
executorService.submit(() -> {
handel();
});
}
Thread.sleep(5000);
latch();
executorService.shutdown();
}
输出
尝试获取锁,线程:13
尝试获取锁,线程:16
尝试获取锁,线程:15
尝试获取锁,线程:14
尝试获取锁,线程:12
获取锁成功,线程:13
使线程进入等待状态,线程:13
获取锁成功,线程:16
使线程进入等待状态,线程:16
获取锁成功,线程:15
使线程进入等待状态,线程:15
获取锁成功,线程:14
使线程进入等待状态,线程:14
获取锁成功,线程:12
使线程进入等待状态,线程:12
释放锁,线程:13
释放锁,线程:16
释放锁,线程:12
释放锁,线程:14
释放锁,线程:15