Перейти к основному содержимому

Как Spring Singleton Bean обслуживает параллельные запросы?

· 4 мин. чтения

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 .