跪拜 Guibai
← All articles
Java

A Semaphore-Based Flash Sale System Survives ByteDance's Third-Round Coding Gauntlet

By 鹤望兰675 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Flash sale problems are a staple of Chinese big-tech backend interviews, and the `AtomicInteger` check-then-act trap is a reliable filter for candidates who understand atomicity versus compound operations. The `Semaphore` approach generalizes to any resource-pool scenario where a fixed number of permits must be acquired with a deadline and released safely.

Summary

The core challenge is a classic overselling problem: 100 threads race to claim 30 items, each thread must acquire a lock within 3 seconds or bail, and any successful claim must be rolled back if payment takes longer than 10 seconds. The solution replaces the obvious-but-broken `AtomicInteger` check-then-decrement pattern with a `Semaphore` whose `tryAcquire` method bundles the check and decrement into a single atomic CAS operation, eliminating the race window entirely.

A wrapper class `SemaphoreAndStocks` pairs each semaphore with its initial stock count so that `undoDecrement` can guard against over-releasing permits — calling `release()` unconditionally would inflate available permits beyond the original 30. The payment timeout logic lives outside `decrementStock` in the main thread pool, where each `Callable` sleeps for a random duration up to 11 seconds and returns an "undo" signal if it exceeds 10 seconds, triggering a rollback.

The main loop runs rounds of `invokeAll` until stock hits zero, collecting per-thread success counts and undo events. The design keeps concerns separated: `decrementStock` only gates access, while the payment simulation and timeout enforcement happen in the caller.

Takeaways
`AtomicInteger.get()` followed by `decrementAndGet()` is two atomic operations, not one — a thread can pass the `> 0` check before another thread decrements, causing overselling.
`Semaphore.tryAcquire(timeout, unit)` bundles the check and decrement into a single CAS operation, eliminating the race window without manual spin loops.
Wrapping a `Semaphore` with its initial stock count prevents `release()` from inflating permits beyond the original ceiling.
Payment timeout simulation belongs in the caller, not inside the stock-decrement method, because payment processing runs on a separate timeline from lock acquisition.
The main loop uses `ExecutorService.invokeAll` to run all 100 callables per round, then inspects each `Future` for success, failure, or undo signals.
A `ConcurrentHashMap` keyed by product ID stores the semaphore wrapper, making the system trivially extensible to multiple products.
Conclusions

Interview coding rounds at ByteDance's payments division still demand raw API recall — no IDE autocomplete, no AI copilot — which punishes developers who have outsourced syntax memory to tools.

The `Semaphore` solution is cleaner than a `ReentrantLock` with `tryLock` because the permit count directly models inventory; a lock would require a separate counter and still need atomic check-and-decrement logic.

Running all 100 callables every round via `invokeAll` is deliberately wasteful and only works because `tryAcquire` fails fast when permits are exhausted; a production system would use a bounded queue or a countdown latch to avoid re-submitting losers.

The guard `semaphore.availablePermits() < initialStock` before `release()` is a defensive pattern that prevents silent state corruption from duplicate rollback calls.

Separating the lock-acquisition timeout (3s) from the payment timeout (10s) mirrors real payment gateway architecture, where the acquire phase is a fast mutex and the payment phase is an async external call with its own SLA.

Concepts & terms
Semaphore
A concurrency utility that maintains a set of permits. `acquire()` takes a permit (blocking if none available), `release()` returns one. `tryAcquire(timeout)` attempts to take a permit within a time limit, returning false on failure.
CAS (Compare-And-Swap)
An atomic instruction that updates a value only if it matches an expected current value. `Semaphore.tryAcquire` uses CAS internally to decrement the permit count without a separate check step.
Check-then-act race condition
A concurrency bug where a thread checks a condition (e.g., stock > 0) and acts on it (decrement) in two steps, allowing another thread to change the state between the check and the act.
invokeAll
An `ExecutorService` method that submits a collection of `Callable` tasks and blocks until all complete, returning a list of `Future` objects for result retrieval.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗