Description
Affects: 5.1.9.RELEASE
During singleton creation, DefaultSingletonBeanRegistry
synchronises on this.singletonObjects
:
While synchronized, it then uses the singletonFactory
to create the singleton:
This call into user code while holding a lock can result in deadlock. We've seen one example reported in this Spring Boot issue where Micrometer is also involved. I've also reproduced a very similar problem without Micrometer and with no synchronization in user code:
package example;
import javax.annotation.PostConstruct;
import javax.validation.Validator;
import javax.validation.constraints.Max;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
public class SingletonCreationDeadlockTests {
@Test
public void create() {
new AnnotationConfigApplicationContext(Config.class).close();;
}
private static final class Registry {
private final ConfigProperties properties;
Registry(ConfigProperties properties) {
this.properties = properties;
}
void register() {
this.properties.getSetting();
}
}
@Validated
static class ConfigProperties {
@Max(10)
private int setting = 5;
public int getSetting() {
return this.setting;
}
public void setSetting(int setting) {
this.setting = setting;
}
}
@Configuration
static class Config {
@Bean
public Registry registry(ConfigProperties properties) {
return new Registry(properties);
}
@Bean
public ConfigProperties properties() {
return new ConfigProperties();
}
@Bean
public LocalValidatorFactoryBean localValidatorFactoryBean() {
return new LocalValidatorFactoryBean();
}
@Bean
public static MethodValidationPostProcessor methodValidationPostProcessor(@Lazy Validator validator) {
MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
postProcessor.setValidator(validator);
return postProcessor;
}
@Bean
public Registrar registrar(Registry registry) {
return new Registrar(registry);
}
}
static class Registrar {
private final Registry registry;
Registrar(Registry registry) {
this.registry = registry;
}
@PostConstruct
void register() {
Thread thread = new Thread(() -> {
registry.register();
});
thread.start();
try {
thread.join();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}
Here's a zip of a complete project containing the above test: singleton-creation-deadlock.zip
The deadlock occurs because the main thread has locked singletonObjects
and then waits for the thread created by Registrar
to complete. The thread created by Registrar
ends up waiting to lock singletonObjects
due to ConfigProperties
being @Validated
and the resolution of the @Lazy
Validator
requiring a call to DefaultListableBeanFactory.doResolveDependency
which results in a call to DefaultSingletonBeanRegistry.getSingleton
where the attempt to lock singletonObjects
is made.