临界区的LockCount为何小于-1

临界区的LockCount为何小于-1

某日,在浙大国家实验室,与老方和小崔调试监控死锁问题。机柜里一溜架装服务器上出现死锁问题。用WinDbg看,发现其中导致死锁的临界区LockCount值是小于-1的数!!

多次重现该问题,发现LockCount经常是负的两三百。

我等本着不十分科学严谨,但又有一点科学严谨的态度,装模作样查了下资料,显示如下:

LockCount代表什么含义

ms-help://MS.MSDNQTR.v80.en/MS.MSDN.v80/dnmag03/html/CriticalSections1203default.htm

http://msdn.microsoft.com/zh-cn/magazine/cc164040(en-us).aspx

struct RTL_CRITICAL_SECTION

{

PRTL_CRITICAL_SECTION_DEBUG DebugInfo;

LONG LockCount;

LONG RecursionCount;

HANDLE OwningThread;

HANDLE LockSemaphore;

ULONG_PTR SpinCount;

};

LockCount

这是临界区里最重要的字段。其初始值为-1,而0或更大的值表示临界区被持有。当该值不等于-1,OwningThread字段(该字段在WinNT.h里定义错误的,应该用DWORD而不是HANDLE类型)存放了持有该临界区的线程ID。

LockCount - (RecursionCount - 1 ) 表示还有多少其他线程在等待获取该临界区。

(以下是英文原版)

LockCount

This is the most important field in a critical section. It is initialized to a value of -1; a value of 0 or greater indicates that the critical section is held or owned. When it's not equal to -1, the OwningThread field (this field is incorrectly defined in WINNT.H—it should be a DWORD instead of a HANDLE) contains the thread ID that owns this critical section. The delta between this field and the value of (RecursionCount -1) indicates how many additional threads are waiting to acquire the critical section.

LockCount的值是如何变化的。

网上有很多文章根据临界区的原理,总结了两个能使LockCount变换的函数的伪代码如下:

_RtlTryEnterCriticalSection

if(CriticalSection->LockCount == -1)

{

// 临界区可用

CriticalSection->LockCount = 0;

CriticalSection->OwningThread = TEB->ClientID;

CriticalSection->RecursionCount = 1;

return TRUE;

}

else

{

if(CriticalSection->OwningThread == TEB->ClientID)

{

// 临界区是当前线程获取

CriticalSection->LockCount++;

CriticalSection->RecursionCount++;

return TRUE;

}

else

{

// 临界区已被其它线程获取

return FALSE;

}

}

_RtlLeaveCriticalSection

if(--CriticalSection->RecursionCount == 0)

{

// 临界区已不再被使用

CriticalSection->OwningThread = 0;

if(--CriticalSection->LockCount)

{

// 仍有线程锁定在临界区上

_RtlpUnWaitCriticalSection(CriticalSection)

}

}

else

{

--CriticalSection->LockCount

}

上述文字中的含义可以比较清晰地推断出:

1. RecursionCount有可能由于LeaveCriticalSection的多余调用而小于初值0 (已经实证)

2. LockCount的值只可能大于或等于初值-1

理论似乎再一次与事实不符!

我们开始胡思乱想,猜测如下几种可能:

1. EnterCriticalSection执行到一半异常中止

这种机会很小,即使发生,也找不出什么道理让LockCount变成负两三百这么离谱。

2. 内存错乱导致RTL_CRITICAL_SECTION结构被写坏。

但几种推测都查证无果。

一个偶然的机会 -_-!!! ,我在自己的计算机上实验的时候,居然也发现了LockCount小于-1!而且屡试不爽!

我的计算机装的Windows Vista,我们自然就有如下猜想:

在某个操作系统版本下,LockCount的机制本来就有所不同!!

这个猜想比较靠谱,立刻着手验证。实验室里发生这个问题的电脑都是Windows2003+SP1。我们马上在Windows2003+SP1系统做了测试,写了个非常简单的测试,创建一个临界区,然后调用EnterCriticalSection,果然发现LockCount编程了-2!而多线程下测试,也确实会出现负两三百的情况。

看来LockCount的含义在不同版本的Win下确实不一样。

其后我们多次尝试上网搜索关于LockCount含义在Windows不同版本中的变迁,却不得要领。

又一个偶然的机会 -_-!!! ,老方在WinDbg的帮助文档里发现了一段关于LockCount变迁的说明,全文如下(真是踏破铁鞋无觅处,得来全不费工夫)

Interpreting Critical Section Fields in Windows Server 2003 SP1 and Later

In Microsoft Windows Server 2003 Service Pack 1 and later versions of Windows, the LockCount field is parsed as follows:

The lowest bit shows the lock status. If this bit is 0, the critical section is locked; if it is 1, the critical section is not locked.

The next bit shows whether a thread has been woken for this lock. If this bit is 0, then a thread has been woken for this lock; if it is 1, no thread has been woken.

The remaining bits are the ones-complement of the number of threads waiting for the lock.

As an example, suppose the LockCount is -22. The lowest bit can be determined in this way:

0:009> ? 0x1 & (-0n22)

uate expression: 0 = 00000000

The next-lowest bit can be determined in this way:

0:009> ? (0x2 & (-0n22)) >> 1

uate expression: 1 = 00000001

The ones-complement of the remaining bits can be determined in this way:

0:009> ? ((-1) - (-0n22)) >> 2

uate expression: 5 = 00000005

In this example, the first bit is 0 and therefore the critical section is locked. The second bit is 1, and so no thread has been woken for this lock. The complement of the remaining bits is 5, and so there are five threads waiting for this lock.

事情至此总算水落石出!

jetcache这部分操作是什么意思 default AutoReleaseLock tryLock(K key, long expire, TimeUnit timeUnit) { if (key == null) { return null; } else { String uuid = UUID.randomUUID().toString(); long expireTimestamp = System.currentTimeMillis() + timeUnit.toMillis(expire); CacheConfig config = this.config(); AutoReleaseLock lock = () -> { int unlockCount = 0; while(unlockCount++ < config.getTryLockUnlockCount()) { if (System.currentTimeMillis() >= expireTimestamp) { logger.info("[tryLock] [{} of {}] [{}] lock already expired: Key={}", new Object[]{unlockCount, config.getTryLockUnlockCount(), uuid, key}); return; } CacheResult unlockResult = this.REMOVE(key); if (unlockResult.getResultCode() != CacheResultCode.FAIL && unlockResult.getResultCode() != CacheResultCode.PART_SUCCESS) { if (unlockResult.isSuccess()) { logger.debug("[tryLock] [{} of {}] [{}] successfully release the lock. Key={}", new Object[]{unlockCount, config.getTryLockUnlockCount(), uuid, key}); return; } logger.warn("[tryLock] [{} of {}] [{}] unexpected unlock result: Key={}, result={}", new Object[]{unlockCount, config.getTryLockUnlockCount(), uuid, key, unlockResult.getResultCode()}); return; } logger.info("[tryLock] [{} of {}] [{}] unlock failed. Key={}, msg = {}", new Object[]{unlockCount, config.getTryLockUnlockCount(), uuid, key, unlockResult.getMessage()}); } }; int lockCount = 0; Cache cache = this; while(lockCount++ < config.getTryLockLockCount()) { CacheResult lockResult = cache.PUT_IF_ABSENT(key, uuid, expire, timeUnit); if (lockResult.isSuccess()) { logger.debug("[tryLock] [{} of {}] [{}] successfully get a lock. Key={}", new Object[]{lockCount, config.getTryLockLockCount(), uuid, key}); return lock; } if (lockResult.getResultCode() != CacheResultCode.FAIL && lockResult.getResultCode() != CacheResultCode.PART_SUCCESS) { logger.debug("[tryLock] [{} of {}] [{}] others holds the lock, return null. Key={}", new Object[]{lockCount, config.getTryLockLockCount(), uuid, key}); return null; } logger.info("[tryLock] [{} of {}] [{}] cache access failed during get lock, will inquiry {} times. Key={}, msg={}", new Object[]{lockCount, config.getTryLockLockCount(), uuid, config.getTryLockInquiryCount(), key, lockResult.getMessage()}); int inquiryCount = 0; while(inquiryCount++ < config.getTryLockInquiryCount()) { CacheGetResult inquiryResult = cache.GET(key); if (inquiryResult.isSuccess()) { if (uuid.equals(inquiryResult.getValue())) { logger.debug("[tryLock] [{} of {}] [{}] successfully get a lock after inquiry. Key={}", new Object[]{inquiryCount, config.getTryLockInquiryCount(), uuid, key}); return lock; } logger.debug("[tryLock] [{} of {}] [{}] not the owner of the lock, return null. Key={}", new Object[]{inquiryCount, config.getTryLockInquiryCount(), uuid, key}); return null; } logger.info("[tryLock] [{} of {}] [{}] inquiry failed. Key={}, msg={}", new Object[]{inquiryCount, config.getTryLockInquiryCount(), uuid, key, inquiryResult.getMessage()}); } } logger.debug("[tryLock] [{}] return null after {} attempts. Key={}", new Object[]{uuid, config.getTryLockLockCount(), key}); return null; } }
最新发布
07-24
<think>我们分析这段代码:这是JetCache中实现分布式锁的一个方法。方法名为tryLock,尝试获取一个分布式锁。 参数:key(锁的键),expire(锁的过期时间),timeUnit(时间单位) 返回:AutoReleaseLock对象(一个函数式接口,用于释放锁),如果获取锁失败则返回null。 实现步骤: 1. 如果key为null,直接返回null。 2. 生成一个唯一的uuid,作为锁的值(用于标识锁的持有者,避免误删其他客户端的锁)。 3. 计算锁的过期时间戳(当前时间+过期时间)。 4. 从配置中获取尝试获取锁的次数(tryLockLockCount)、尝试释放锁的次数(tryLockUnlockCount)和查询锁状态的次数(tryLockInquiryCount)。 5. 创建一个AutoReleaseLock对象(实际上是一个lambda表达式,用于在需要时释放锁)。这个lambda表达式会在被调用时执行释放锁的操作。 6. 尝试获取锁:循环最多tryLockLockCount次。 a. 执行PUT_IF_ABSENT操作(如果key不存在则设置值,存在则不设置),将key的值设置为uuid,并设置过期时间。 b. 如果操作成功(lockResult.isSuccess()),说明获取锁成功,返回之前创建的AutoReleaseLock对象。 c. 如果操作失败,但失败原因不是缓存访问失败(即CacheResultCode不是FAIL或PART_SUCCESS),说明锁已经被其他客户端持有,直接返回null。 d. 如果是缓存访问失败(例如网络问题),则进行锁状态查询(inquiry): 循环最多tryLockInquiryCount次,执行GET操作获取当前锁的值。 - 如果查询成功,并且当前锁的值就是自己生成的uuid(说明虽然之前PUT_IF_ABSENT失败了,但实际上锁已经被自己持有,可能是之前操作超时但实际上成功了),则返回锁。 - 如果查询成功,但值不是自己的uuid,说明锁被其他客户端持有,返回null。 - 如果查询失败,记录日志并继续下一次查询。 7. 如果尝试了指定次数后仍未获取锁,返回null。 关于AutoReleaseLock的实现(释放锁的部分): 当调用AutoReleaseLock的run方法(因为AutoReleaseLock是一个函数式接口,实际上就是执行lambda表达式)时,会尝试释放锁。 释放锁的过程: a. 循环最多tryLockUnlockCount次。 b. 检查锁是否已经过期(当前时间>=expireTimestamp),如果过期则直接返回(因为锁已经自动过期,无需释放)。 c. 执行REMOVE操作删除key(即释放锁),如果删除成功(unlockResult.isSuccess())则返回。 d. 如果删除失败(CacheResultCode为FAIL或PART_SUCCESS),则记录日志并继续尝试,直到达到最大尝试次数。 注意:释放锁的操作中,只有删除成功或者锁已经过期才会停止尝试。如果删除失败,会重试多次。 为什么需要这样设计? 分布式锁在分布式系统中非常重要,用于控制多个节点对共享资源的访问。这个实现具有以下特点: - 使用唯一uuid作为锁的值,避免误删其他客户端的锁(只有值匹配时才删除,但这里释放锁时并没有检查值,而是直接删除,这是因为在获取锁时已经确保只有自己能够知道这个uuid,并且在释放锁时通过删除key来释放。但是,这里存在一个风险:如果锁已经过期并且被其他客户端获取,那么此时删除可能会误删别人的锁。所以,释放锁时应该先检查锁的值是否还是自己的uuid,然后再删除。然而,在这个实现中,并没有在释放时检查值,而是直接删除。这是因为在释放锁时,锁可能已经过期,并且被其他客户端获取,所以直接删除可能会导致误删。因此,更安全的做法是使用原子操作(比较并删除)来释放锁。但是,在这个代码中,释放锁使用的是REMOVE(key),并没有检查值。所以我们需要看REMOVE操作的具体实现,如果REMOVE操作只是简单的删除,那么这里就有问题。实际上,在分布式锁的实现中,释放锁时应该使用lua脚本来保证原子性:先比较锁的值是否等于当前uuid,再决定是否删除。所以,这里可能是一个潜在的问题点。 但是,我们注意到在获取锁时,使用PUT_IF_ABSENT(相当于set if not exist)来设置锁,并且设置了一个过期时间。在释放锁时,由于锁有一个过期时间,所以即使没有立即释放,锁也会在过期后自动释放。但是,为了避免在锁过期之前被误删,应该使用原子操作来释放锁(比较值再删除)。然而,在这个代码中,REMOVE操作只是简单的删除,没有比较值。因此,这个锁实现并不是绝对安全的。 然而,我们再看代码中释放锁的部分,它首先检查了锁是否已经过期(expireTimestamp),如果已经过期,它就不执行删除操作。这可以避免在锁过期后去删除别人的锁。但是,在锁未过期的情况下,它直接执行了删除操作,这可能会删除别人的锁吗?有可能!因为锁的过期时间是固定的,但是多个客户端的时间可能不一致(时间漂移),所以判断是否过期可能不准确。另外,在锁过期后,其他客户端可能已经获取了锁,此时删除就会误删。 因此,正确的做法是:释放锁时,使用一个原子操作(比较当前锁的值是否等于自己设置的uuid,然后删除)。在Redis中,可以使用Lua脚本来实现。但是,在这个代码中,我们没有看到比较值再删除的操作,所以这个锁的实现可能不是完全安全的。 但是,我们注意到在获取锁后,返回的AutoReleaseLock对象中,释放锁的操作是使用REMOVE(key)来删除整个key。这会导致无论锁的值是什么,都会删除。所以,如果锁已经过期并且被其他客户端获取,那么这里就会误删。因此,这个锁的实现存在缺陷。 改进建议:在释放锁时,应该使用原子操作来比较并删除。例如,在Redis中,可以使用如下脚本: if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end 但是,在JetCache中,REMOVE操作是否支持原子操作取决于底层的缓存实现。如果底层缓存是Redis,那么JetCache的REMOVE操作就是简单的del命令,没有比较值。所以,这里需要修改。 总结:这段代码是尝试实现一个分布式锁,但是释放锁的部分存在安全隐患,可能会误删其他客户端的锁。正确的做法是在释放锁时检查锁的值是否等于自己设置的值,然后再删除。 然而,我们再看代码中释放锁的部分,它有一个循环,并且每次循环都会检查锁是否已经过期(通过本地时间判断)。这在一定程度上可以降低误删的概率,因为如果锁已经过期,它就不会删除。但是,由于分布式系统中各节点时间可能不一致,所以不能完全依赖本地时间。另外,在锁过期后,其他客户端可能已经获取了锁,此时删除就会导致问题。 因此,这个锁的实现适用于对锁的安全性要求不高的场景。如果要求强一致性,建议使用Redisson等成熟的分布式锁框架。 现在,我们回答用户的问题:这段代码是JetCache中实现分布式锁的tryLock方法,它尝试获取一个分布式锁,并返回一个用于释放锁的AutoReleaseLock对象。但是,释放锁的部分存在缺陷,可能会误删其他客户端的锁。 注意:在JetCache的后续版本中,可能已经修复了这个问题。我们分析的代码可能来自某个特定版本。 由于用户要求提供Java代码,我们这里不提供代码,因为用户是询问这段代码的意思。但是,我们可以给出一个改进的释放锁的示例(假设使用Redis): 在释放锁时,应该使用脚本来保证原子性。但是,在JetCache中,我们可以通过扩展Cache接口来实现。不过,这已经超出了这段代码的范围。 所以,我们主要解释这段代码的含义和潜在问题。 最后,按照要求,提出相关问题: §§相关问题§§: 1. 分布式锁在实现时需要注意哪些问题? 2. 为什么在释放锁时需要检查锁的值? 3. 在分布式系统中,如何解决时间不一致问题? 4. JetCache的分布式锁实现与Redisson的分布式锁实现有何不同?</think>这段代码是 JetCache 框架中实现**分布式锁**的核心逻辑,用于在分布式环境下安全地获取和释放锁。以下是对操作的详细解析: --- ### **核心功能:分布式锁的实现** #### **1. 获取锁(`tryLock` 方法)** - **目的**:尝试获取一个基于 Key 的分布式锁。 - **流程**: 1. **生成唯一标识**:使用 `UUID.randomUUID().toString()` 生成锁的唯一标识(`uuid`),避免不同客户端的锁冲突。 2. **计算锁过期时间**:`expireTimestamp = 当前时间 + expire`(锁的自动过期时间)。 3. **循环尝试获取锁**: - 通过 `PUT_IF_ABSENT(key, uuid, expire, timeUnit)` 尝试设置锁(等价于 Redis 的 `SETNX key uuid EX expire`)。 - **成功**:返回 `AutoReleaseLock` 对象(用于后续释放锁)。 - **失败(锁已被占用)**: - 检查锁是否属于自己(通过 `GET(key)` 查询锁的值是否等于 `uuid`)。 - 若属于自己(可能因网络重试导致误判),仍视为获取成功。 - **失败(缓存访问异常)**:重试查询锁状态(`config.getTryLockInquiryCount()` 次)。 4. **超时放弃**:达到最大重试次数(`config.getTryLockLockCount()`)后返回 `null`。 #### **2. 释放锁(`AutoReleaseLock` 实现)** - **目的**:安全释放锁,避免误删其他客户端的锁。 - **流程**: 1. **检查锁是否已过期**:若 `System.currentTimeMillis() >= expireTimestamp`,跳过释放(锁已自动失效)。 2. **循环尝试释放**: - 调用 `REMOVE(key)` 删除锁(需原子操作)。 - 成功删除或锁已过期时退出。 - 失败时按配置重试(`config.getTryLockUnlockCount()` 次)。 3. **日志记录**:详细记录释放成功/失败的原因。 --- ### **关键设计解析** #### **1. 避免锁误删** - **唯一标识(`uuid`)**:每个客户端生成独立的 `uuid` 作为锁的值,确保只有锁的持有者才能释放。 - **释放前检查**:在 `AutoReleaseLock` 中检查锁的过期时间,避免释放已过期的锁(此时可能被其他客户端持有)。 #### **2. 容错机制** - **网络抖动处理**: - 获取锁时重试(`tryLockLockCount`)。 - 释放锁时重试(`tryLockUnlockCount`)。 - **缓存访问失败处理**:查询锁状态时重试(`tryLockInquiryCount`)。 #### **3. 锁的自动过期** - **过期时间(`expire`)**:即使锁未被显式释放,也会在过期后自动失效,防止死锁。 - **兜底逻辑**:释放锁时若发现已过期,直接跳过删除操作。 --- ### **潜在风险与改进** 1. **释放锁的非原子性**: - 当前 `REMOVE(key)` 直接删除锁,未校验 `uuid` 可能导致误删(如锁过期后被其他客户端获取)。 - **改进方案**:使用 Lua 脚本原子化校验值并删除(如 Redis 的 `EVAL`): ```java String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; cache.eval(script, key, uuid); ``` 2. **时钟漂移问题**: - 依赖本地时间判断锁过期,若服务器时间不同步可能导致锁提前释放。 - **改进方案**:使用缓存服务的绝对时间(如 Redis 的 `TIME` 命令)。 --- ### **典型应用场景** ```java AutoReleaseLock lock = cache.tryLock("order_lock", 10, TimeUnit.SECONDS); if (lock != null) { try { // 执行业务逻辑(如处理订单) } finally { lock.run(); // 确保锁被释放 } } else { // 获取锁失败 } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值