转至元数据结尾
转至元数据起始

双重检查锁定(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);
	}
}
编写评论...