双重检查锁定(DCL,Double-Checked Locking)
双重检查锁定(Double-Checked Locking)是一种并发设计模式,该模式减少了同步的开销,提高了执行效率。该模式通过两次检查锁定,确保被检查的代码的线程安全性。在第一次检查中,如果发现变量不满足条件,才进行加锁操作。然后在锁定的区块内再进行一次检查,如果仍不满足条件,才进行相关操作。
高并发环境下DCL的应用和优势
在高并发环境下,DCL可以显著提高性能。在使用单例模式时,如果没有并发考虑,可能每次访问单例对象时都需要获取同步锁,这会大大影响程序的执行效率。而DCL模式可以避免这个问题,它只在第一次实例化时加锁,之后的访问都不需要获取锁,这大大降低了锁的开销,提高了程序的执行效率。
————————————————上面内容来源:作者冰点(icepip.blog.csdn.com)原文链接:https://blog.csdn.net/wangshuai6707/article/details/132989903
本文主要讲DCL在各种应用场景中的代码实现。
单例模式的DCL
这里指的是懒汉式单例。懒汉式单例类似于懒加载,即只有在首次访问单例对象时,才把对象加载到内存中,进行对象的实例化创建。
public class Singleton { // 类级别的实例 private volatile static Singleton instance; private Singleton() {} // 获取实例(含实例初始化) public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
新增数据库记录
void insertData(data) // data,要插入的数据 { if(!exist(data)) { synchronized { if(!exist(data)) { repo.insert(data); } } }
🍀问题:锁实际是在哪里释放的? ---->看下面两种代码示意,哪个正确?
void insertData(data) // data,要插入的数据 { if(!exist(data)){ synchronized { try{ if(!exist(data)){ repo.insert(data); } } finally{ releaseLock; } } } | void insertData(data) // data,要插入的数据 { if(!exist(data)){ synchronized { if(!exist(data)){ try{ repo.insert(data); } finally{ releaseLock; } } } } |
答:前者是正确的。
分布式锁的在新增数据库记录中的应用
这里,我们讨论的场景是:同步插入数据库记录。就是说,在并发请求新增数据的情况下,实现按顺序逐条插入数据库。(这种场景很少见)
void insertData(data) // data,要插入的数据 { if(!exist(data)){ locked = redis.synchronized(..) if(locked){ try { if(!exist(data)){ repo.insert(data); } } finally{ releaseLock; } } else{ // 新增数据未拿到锁,程序异常。 throw Exception; } }
分布式锁在累计交易金额中的应用
这里,我们讨论的场景是:按日累加交易的金额。就是说,将当天的每笔交易累加求和,持久化为数据库里的一条记录。其中有一个细节是,当天的第一笔交易时,要先新增记录,后面的交易则在这个记录上做金额的累加。见下方图示
当天交易数据 | 动作 | 数据记录变动情况 |
---|---|---|
第1笔交易 ORD001,金额5 | 新增记录 | yyyyMMdd,累计金额=5 |
第2笔交易 ORD002,金额8 | 修改记录 | yyyyMMdd,累计金额=13 |
第3笔交易 ORD003,金额10 | 修改记录 | yyyyMMdd,累计金额=23 |
并发情况下,要对新增记录做并发控制。代码实现如下。
void summarize(transData) // 入参,交易数据 { var date = convert2Date(transData.transTime);// 获得交易日 var record = repo.getByDate(date); if(record==null) { // 不存在数据记录,加锁新增 bool locked = redis.synchronized(key:date, ttl:3s, retry:30ms) record = repo.getByDate(date); if(!locked && record==null) { // 新增数据未拿到锁,数据库里也不存在数据,则抛出异常。 throw Exception; } try { if(record==null){ repo.insert(date, transData.amount); } else { // 并发同步等待后,发现已经存在数据记录,进行数据库累加 repo.summarizeDelta(record.id, transData.amount); } } finally{ releaseLock(key:date); } } else { // 存在数据记录,进行数据库累加 repo.summarizeDelta(record.id, transData.amount); } }
怎么拿没拿到锁都查库了呢? 看下面这种直观的实现方式你就明白了。
void summarize(transData) // 入参,交易数据 { var date = convert2Date(transData.transTime);// 获得交易日 var record = repo.getByDate(date); if(record==null) { // 不存在数据记录,加锁新增 bool locked = redis.synchronized(key:date, ttl:3s, retry:30ms) if(!locked) { // 新增数据未拿到锁,这时,要看其他线程是不是已经保存数据了。 record = repo.getByDate(date); if(record==null){ //如果数据库里也不存在数据,则抛出异常。 throw Exception; } else { // 发现已经存在数据记录,进行数据库累加 repo.summarizeDelta(record.id, transData.amount); } } else { try { record = repo.getByDate(date); if(record==null){ repo.insert(date, transData.amount); } else { // 并发同步等待后,发现已经存在数据记录,进行数据库累加 repo.summarizeDelta(record.id, transData.amount); } } finally{ releaseLock(key:date); } } } else { // 存在数据记录,进行数据库累加 repo.summarizeDelta(record.id, transData.amount); } }
添加评论