Double-Check Locking — Thread-Safe Singleton
Definition
Section titled “Definition”The Double-Check Locking pattern optimizes concurrent access to a shared resource by checking the condition before and after acquiring a lock. The first check (without lock) serves as a fast-path for the nominal case (cache hit). The second check (after lock) protects against races.
Diagram
Section titled “Diagram”flowchart TD
REQ[GetOrAddAsync] --> C1{Check 1<br/>without lock}
C1 -->|hit| RET[Return value]
C1 -->|miss| ACQ[Acquire SemaphoreSlim]
ACQ --> C2{Check 2<br/>after lock}
C2 -->|hit| REL1[Release lock] --> RET
C2 -->|miss| FAC[Execute factory]
FAC --> SET[Store in cache]
SET --> REL2[Release lock] --> RET
style C1 fill:#2d5a27,color:#fff
style ACQ fill:#ff6b6b,color:#fff
style C2 fill:#4a9eff,color:#fff
Implementation in Granit
Section titled “Implementation in Granit”| Component | File | Role |
|---|---|---|
KeycloakAdminTokenService | src/Granit.Identity.Federated.Keycloak/Internal/KeycloakAdminTokenService.cs | Keycloak token acquisition with double-check locking |
Note: Cache stampede protection was previously implemented via manual double-check locking in
DistributedCacheService. With the migration to FusionCache (ADR-018), stampede protection is now handled natively by FusionCache. Double-check locking remains in use for non-cache scenarios such as token caching.
- Check 1: read cached token without lock — fast-path
- Acquire:
SemaphoreSlim.WaitAsync(cancellationToken) - Check 2: re-read cached token after lock
- Factory: request new token from Keycloak if still expired
- Store: cache the new token
- Release:
SemaphoreSlim.Release()in afinally
Anti-stampede
Section titled “Anti-stampede”If 100 simultaneous requests find an expired token:
- All 100 pass Check 1 (expired)
- 1 acquires the lock, 99 wait
- The first requests a new token and caches it
- The 99 pass Check 2 — valid token, no Keycloak call
Result: 1 single Keycloak request instead of 100.
Rationale
Section titled “Rationale”Double-check locking remains essential for non-cache shared resources in a high-concurrency environment. For cache stampede protection, FusionCache provides this natively via its built-in concurrency control.
Usage example
Section titled “Usage example”// Double-check locking for token caching// 100 simultaneous calls with an expired token:// -> 1 Keycloak request (the first one)// -> 99 served from cached token (after the lock)string token = await tokenService.GetAdminTokenAsync(cancellationToken);