Lock与Condition
0. 用锁的最佳实践
-
永远只在更新对象的成员变量时加锁
-
永远只在访问可变的成员变量时加锁
-
永远不在调用其他对象的方法时加锁
1. Lock与Synchronize区别
-
Lock是由代码实现,核心是CAS操作;synchronize则是关键字,通过修改对象头中的锁信息,由JVM实现调用。更详细的底层原理实现可见Java多线程——Lock和Synchronized底层原理比较及synchronized和lock的区别(底层实现)
-
由于Lock由代码实现,故需要在finally语句块中显示关闭锁,避免异常情况资源不释放。synchronize则会在线程崩溃时,由JVM自动释放锁资源。
-
synchronize在JDK1.5之前是重量级锁,每次都需要像操作系统申请资源,而JDK1.6后优化 synchronized 的性能给它的锁加入了四种状态,无锁状态 -> 偏向锁 -> 轻量级锁 -> 重量级锁(MarkWord储存状态中就可以看到),故后续性能和Lock差不多。
-
synchronize通常和object.wait()、object.notify()、object.notifyAll()搭配使用,没有显示的条件变量控制;Lock提供了更精细化的锁粒度,可在Lock.lock()及Lock.unlock()块中,通过Condition条件变量类搭配condition.await()、condition.signal()、condition.signalAll()使用。
-
Lock支持超时等待机制,synchronize不支持
2. Lock的引入
JDK1.5版本引入了java.util.concurrent.locks包,包含了Lock接口及其实现类。在Java已有管程实现的synchronize的基础下,作为引入Lock类的推动力,可以看看优化点:
- synchronize不支持超时等待,故在死锁原因层面,无法针对资源不可剥夺这个条件进行防范
- synchronize不支持显式的条件变量,无法更精细的控制并发粒度
- synchronize不能响应中断,Lock的lockInterruptibly()方法支持中断
- synchronize不支持获取锁失败时不进入阻塞状态,Lock的tryLock()方法支持
- 更好的性能(jdk1.6已对synchronize优化性能)
故,Java自JDK1.5版本后有了两种对于管程的实现。
3. Lock使用范例
class LockTest {
private final Lock lock = new ReentrantLock();
private int val;
public void add() {
lock.lock();
try {
val += 1;
} finally {
lock.unlock(); //必须在finally块中加unlock()操作
}
}
}
共享变量val的可见性保证分析如下。首先参考Lock相关类的类图
ReentrantLock继承自AbstractQueuedSynchronizer类,而AbstractQueuedSynchronizer类中有一个状态值state
// The synchronization status
private volatile int state;
可以看到state使用了volatile进行修饰,lock的时候会对state及unlock都会进行读写。利用JMM先行发生(Happens-Before)的规则(具体可参考Java内存模型:Java解决可见性和有序性问题的方案):
- 顺序性规则
针对线程A,val += 1
Happens-Before于lock.unlock()
- volatile变量规则
针对volatile变量的读,永远在写之后。那么线程A的lock.unlock()
Happens-Before于线程B的lock.lock()
操作 - 传递性规则
综合1和2的顺序,那么线程A的val += 1
Happens-Before于线程B的val += 1
当前volatile只是保证了可见性,不同线程若是同时读到最新值state值并写入,还是存在并发问题。互斥性就由lock()底层中的CAS自旋操作来保证。
4. 异步转同步
某些请求是异步的,可以考虑通过回调函数及条件变量,将异步转为同步。
模拟如下:
public class RequestAsyncToSync {
private final Lock lock = new ReentrantLock();
private final Condition done = lock.newCondition(); // 接收完成条件变量
private String resp; // 模拟响应对象
// 模拟请求,两秒后响应并将数据写入到resp对象中
public void request() {
CompletableFuture<Void> asyncTask = CompletableFuture.runAsync(() -> {
try {
Thread.sleep(2000); // 模拟请求耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
resp = "Hello World"; // 模拟请求回调数据写回resp对象
lock.lock();
try {
done.signalAll();
} finally {
lock.unlock();
}
});
}
// 判断是否完成
public boolean isDone() {
return !Objects.isNull(resp);
}
// 普通获取
public String getResp() {
return resp;
}
// 异步转同步等待获取
public String getSyncResp() {
lock.lock();
try {
while (!isDone()) {
done.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return resp;
}
public static void main(String[] args) {
RequestAsyncToSync requestAsyncToSync = new RequestAsyncToSync();
requestAsyncToSync.request();
System.out.println("Resp:" + requestAsyncToSync.getResp()); // 打印Resp:null
System.out.println("Resp:" + requestAsyncToSync.getSyncResp()); // 阻塞等待结果响应,并打印Resp:Hello World
}
}
更多的可以深入解读CompletableFuture源码与原理,学习CompletableFuture中的异步转同步思想(CompletableFuture底层玩的都是CAS更新状态和回调函数,没有使用管程。但是Lock互斥性的实现底层也是CAS,可以一起了解下)。