What does the sync attribute on @Cacheable do, and what problem does it solve?
answer
- sync = one thread computes per key, others wait
- solves thundering herd / cache stampede
- routes via Cache.get(key, Callable)
- no unless, single cache name, no stacking
- provider-dependent atomicity
basics
~20 s@Cacheable(sync = true) makes concurrent misses for the same key wait so the method runs only once; the others get the value it produces. It prevents the 'thundering herd' where many threads all recompute the same missing entry at once.
solid answer
~50 sBy default @Cacheable has no locking: if many threads hit the same missing key simultaneously, they all execute the expensive method before any of them stores a result — the cache-stampede / thundering-herd problem. sync = true tells the cache provider to synchronize computation so that for a given key only ONE thread computes the value while the others block and then receive that computed value. It relies on the underlying Cache implementing atomic get-or-compute (e.g. Caffeine/ConcurrentMapCache support it; some providers don't). Important constraints: sync is mutually exclusive with several features — you can't combine it with unless, you can't list multiple cache names, and it's not usable together with the other cache operations on the same method. Use it for hot, expensive keys where duplicate computation is costly; skip it when the method is cheap or the provider doesn't support atomic loading.
code
java · 7 lines@Cacheable(cacheNames = "reports", key = "#id", sync = true)
public Report buildExpensiveReport(long id) {
// Under a burst of concurrent misses for the same id,
// only ONE thread runs this; the rest block and reuse the result.
return heavyAggregation(id);
}
// Illegal with sync=true: unless=..., cacheNames={"a","b"}, or stacking via @Cachinggo deeper
Aware that sync=true avoids running the method many times at once.
Explains the thundering-herd problem and that only one thread computes per key.
Knows the Cache.get(key, Callable) SPI routing, the mutual-exclusion constraints, and provider dependence.
Weighs local vs distributed stampede protection, blocking trade-offs, and when the constraints (no unless/multi-cache) force an alternative design.
## The problem: cache stampede Plain `@Cacheable` does a get, and on a miss it runs the method and puts the result — but there is **no lock between the get and the put**. Under load, imagine an entry expires and 200 request threads call the method within the same millisecond: all 200 see a miss, all 200 execute the expensive body (a DB query, a remote call), and all 200 write the same value. That burst of duplicated work is the **thundering herd / cache stampede**, and it can overload the backing resource exactly when the cache was supposed to protect it. ## The fix: `sync = true` `@Cacheable(cacheNames = "reports", key = "#id", sync = true)` asks the cache infrastructure to **serialize computation per key**: the first thread to miss computes the value; concurrent threads for the **same key** block until it's done and then receive the computed value instead of recomputing. Threads targeting *different* keys are not blocked relative to each other. This is implemented via the `Cache.get(Object key, Callable<T> valueLoader)` SPI method — an atomic get-or-load. When `sync = true`, Spring routes through that method, and the provider is responsible for the atomicity. Providers like **Caffeine** and the built-in `ConcurrentMapCache` support it; some (older/ simpler) caches may fall back to a coarse or no-op behavior, so it's provider-dependent. ## Constraints (these are exam favorites) When `sync = true`, several other features are **disallowed** and cause a configuration error: - **Only one cache name** — you cannot list multiple caches (`cacheNames = {"a", "b"}`). - **`unless` is not supported** — post-invocation veto can't combine with synchronized loading. - **Cannot be combined with other cache operations** on the same method (no stacking with a `@CachePut`/`@CacheEvict` via `@Caching`). - `condition` **is** still allowed. ## Gotchas - **Provider support varies.** `sync` is a hint to the provider; verify your `CacheManager`/`Cache` actually implements atomic loading, or the guarantee is weaker than you think. - **Blocking behavior.** Waiting threads block; if the loader is slow, they all wait for it. That's the intended trade — one slow computation instead of N. - **Not a distributed lock.** For a local cache it synchronizes within the JVM; across a cluster each node still computes once unless the provider offers cross-node coordination. - **Default is false** — you opt in only where duplicate computation actually hurts. ## When to use Turn it on for **hot keys with expensive loaders** (heavy aggregations, rate-limited upstream APIs) where paying for the same computation many times over is costly. Leave it off for cheap methods (the locking overhead isn't worth it) or when you specifically need `unless`/multi-cache/multiple operations that `sync` forbids.
- Which restrictions apply when sync = true?Only a single cache name is allowed, unless is not supported, and it cannot be combined with other cache operations on the same method (no @Caching stacking). condition remains allowed. Violating these produces a configuration/startup error.
- How does Spring implement the synchronized load under the hood?It uses the Cache SPI's get(Object key, Callable<T> valueLoader) method, delegating atomic get-or-compute to the provider. Providers like Caffeine and ConcurrentMapCache implement it; support and strength of the guarantee depend on the provider.
saying these in an interview costs you the question
- Thinking sync provides a distributed/cluster-wide lock
- Believing sync can be combined with unless or multiple cache names
- Assuming every CacheManager fully supports atomic sync loading
- Confusing sync (concurrency) with thread-safety of the cache store itself