Depends on how you have @Cacheable setup, I have two examples below of the two possible configs.
In this example if you have 10 threads all call getFoo at the same time and a service that getFoo interacts with is causing an exception to be thrown. Each thread, one by one, will proceed into getFoo and have the exception thrown.
@Cacheable(cacheName="data", selfPopulating=true)
public Foo getFoo(long id);
In this example if you have 10 threads all call getFoo at the same time
and a service that getFoo interacts with is causing an exception to be
throw. Only the first thread will call getFoo and when the exception is thrown the Exception object will be stored in the "exception" cache and each subsequent call to getFoo will re-throw that same exception object until it expires from the "exception" cache.
@Cacheable(cacheName="data", selfPopulating=true, exceptionCacheName="exception")
public Foo getFoo(long id);
In general the way I've used selfPopulating is to always define an exception cache so that when a problem occurs we don't beat on the broken service too much. Generally the exception cache TTL to a fairly low number so the service is retried regularly but even with a 30 second TTL that can save a lot of extra invocations.