1. Обзор
В этом руководстве мы узнаем, как bean-компоненты Spring, созданные с помощью области singleton
, работают за кулисами для обслуживания нескольких одновременных запросов. Кроме того, мы поймем, как Java хранит экземпляры bean-компонентов в памяти и как обрабатывает одновременный доступ к ним.
2. Spring Beans и память кучи Java
Куча Java , как мы знаем, представляет собой глобально разделяемую память, доступную для всех запущенных потоков внутри приложения. Когда контейнер Spring создает компонент с областью действия singleton, компонент сохраняется в куче. Таким образом, все параллельные потоки могут указывать на один и тот же экземпляр компонента.
Далее давайте разберемся, что такое стековая память потока и как она помогает обслуживать параллельные запросы.
3. Как обслуживаются одновременные запросы?
В качестве примера возьмем приложение Spring с одноэлементным компонентом с именем ProductService
:
@Service
public class ProductService {
private final static List<Product> productRepository = asList(
new Product(1, "Product 1", new Stock(100)),
new Product(2, "Product 2", new Stock(50))
);
public Optional<Product> getProductById(int id) {
Optional<Product> product = productRepository.stream()
.filter(p -> p.getId() == id)
.findFirst();
String productName = product.map(Product::getName)
.orElse(null);
System.out.printf("Thread: %s; bean instance: %s; product id: %s has the name: %s%n", currentThread().getName(), this, id, productName);
return product;
}
}
Этот bean-компонент имеет метод getProductById()
, который возвращает данные о продукте своим вызывающим объектам. Кроме того, данные, возвращаемые этим bean-компонентом, предоставляются клиентам в конечной точке /product/{id}
.
Далее давайте рассмотрим, что происходит во время выполнения, когда одновременные вызовы достигают конечной точки /product/{id}
. В частности, первый поток вызовет конечную точку /product/1
, а второй поток вызовет /product/2
.
Spring создает отдельный поток для каждого запроса. Как мы видим в выводе консоли ниже, оба потока используют один и тот же экземпляр ProductService
для возврата данных о продукте:
Thread: pool-2-thread-1; bean instance: com.foreach.concurrentrequest.ProductService@18333b93; product id: 1 has the name: Product 1
Thread: pool-2-thread-2; bean instance: com.foreach.concurrentrequest.ProductService@18333b93; product id: 2 has the name: Product 2
Spring может использовать один и тот же экземпляр компонента в нескольких потоках, во-первых, потому что для каждого потока Java создает частную память стека .
Память стека отвечает за хранение состояний локальных переменных, используемых внутри методов во время выполнения потока. Таким образом, Java гарантирует, что потоки, выполняющиеся параллельно, не перезаписывают переменные друг друга.
Во- вторых, поскольку bean-компонент ProductService
не устанавливает ограничений или блокировок на уровне кучи, программный счетчик каждого потока может указывать на одну и ту же ссылку экземпляра bean-компонента в памяти кучи. Следовательно, оба потока могут выполнять метод getProdcutById()
одновременно.
Далее мы поймем, почему для singleton bean-компонентов крайне важно не иметь состояния.
4. Singleton Beans без сохранения состояния и Singleton Beans с сохранением состояния
Чтобы понять, почему одноэлементные компоненты без сохранения состояния важны, давайте посмотрим, каковы побочные эффекты использования одноэлементных компонентов с состоянием.
Предположим, мы переместили переменную productName
на уровень класса:
@Service
public class ProductService {
private String productName = null;
// ...
public Optional getProductById(int id) {
// ...
productName = product.map(Product::getName).orElse(null);
// ...
}
}
Теперь давайте снова запустим сервис и посмотрим на результат:
Thread: pool-2-thread-2; bean instance: com.foreach.concurrentrequest.ProductService@7352a12e; product id: 2 has the name: Product 2
Thread: pool-2-thread-1; bean instance: com.foreach.concurrentrequest.ProductService@7352a12e; product id: 1 has the name: Product 2
Как мы видим, вызов productId
1 показывает productName
«Product 2» вместо «Product 1». Это происходит потому, что ProductService
имеет состояние и использует одну и ту же переменную productName
для всех запущенных потоков.
Чтобы избежать нежелательных побочных эффектов, подобных этому, очень важно, чтобы наши одноэлементные компоненты не сохраняли состояние.
5. Вывод
В этой статье мы увидели, как параллельный доступ к одноэлементным компонентам работает в Spring. Во-первых, мы рассмотрели, как Java хранит одноэлементные компоненты в куче памяти. Далее мы узнали, как разные потоки получают доступ к одному и тому же экземпляру синглтона из кучи. Наконец, мы обсудили, почему важно иметь бины без состояния, и рассмотрели пример того, что может произойти, если бины не являются апатридами.
Как всегда, код этих примеров доступен на GitHub .