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,可以一起了解下)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?