Статьи

Java EE Подводные камни # 1: Игнорировать блокировку по умолчанию @Singleton

EJB Singleton Beans были введены спецификацией EJB 3.1 и часто используются для хранения кэшированных данных. Это означает, что мы пытаемся улучшить производительность нашего приложения с помощью Singleton. В целом, это работает довольно хорошо. Особенно если параллельно не слишком много звонков. Но это изменится, если мы проигнорируем блокировку по умолчанию и количество параллельных вызовов увеличится.

Разумные значения по умолчанию

Давайте начнем с некоторого кода Java и посмотрим, как работает разумное значение по умолчанию блокировки. В следующем фрагменте показан простой EJB Singleton со счетчиком и двумя методами. method1 записывает текущее значение счетчика в журнал, а method2 считает от 0 до 100.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
@Singleton
@Remote(SingletonRemote.class)
public class DefaultLock implements SingletonRemote {
    Logger logger = Logger.getLogger(DefaultLock.class.getName());
 
    private int counter = 0;
 
    @Override
    public void method1() {
        this.logger.info("method1: " + counter);
    }
 
    @Override
    public void method2() throws Exception {
        this.logger.info("start method2");
        for (int i = 0; i < 100; i++) {
            counter++;
            logger.info("" + counter);
        }
        this.logger.info("end method2");
    }
}

Как видите, блокировка не определена. Что вы ожидаете увидеть в файле журнала, если мы вызовем оба метода параллельно?

01
02
03
04
05
06
07
08
09
10
11
12
13
2014-06-24 21:18:51,948 INFO  [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 5) method1: 0
2014-06-24 21:18:51,949 INFO  [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 4) start method2
2014-06-24 21:18:51,949 INFO  [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 4) 1
2014-06-24 21:18:51,949 INFO  [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 4) 2
2014-06-24 21:18:51,950 INFO  [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 4) 3
  ...
2014-06-24 21:18:51,977 INFO  [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 4) 99
2014-06-24 21:18:51,977 INFO  [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 4) 100
2014-06-24 21:18:51,978 INFO  [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 4) end method2
2014-06-24 21:18:51,978 INFO  [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 6) method1: 100
2014-06-24 21:18:51,981 INFO  [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 7) method1: 100
2014-06-24 21:18:51,985 INFO  [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 8) method1: 100
2014-06-24 21:18:51,988 INFO  [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 9) method1: 100

Хорошо, это может быть немного неожиданно, по умолчанию используется блокировка записи, управляемая контейнером, для всего Singleton. Это хороший вариант по умолчанию, чтобы избежать одновременного изменения атрибутов. Но это плохое значение по умолчанию, если мы хотим выполнять операции только для чтения. В этом случае сериализация вызовов методов приведет к снижению масштабируемости и снижению производительности при высокой нагрузке.

Как этого избежать?

Ответ на этот вопрос очевиден, нам нужно позаботиться об управлении параллелизмом. Как обычно в Java EE, есть два способа справиться с этим. Мы можем сделать это сами или попросить контейнер сделать это.

Бин Управляемый Параллелизм

Я не хочу вдаваться в подробности, касающиеся Бина Управляемого Параллелизма. Это наиболее гибкий способ управления одновременным доступом. Контейнер обеспечивает одновременный доступ ко всем методам Синглтона, и вы должны защищать его состояние по мере необходимости. Это можно сделать с помощью синхронизированных и энергозависимых . Но будьте осторожны, довольно часто это не так просто, как кажется.

Управляемый контейнером параллелизм

Управляемый контейнером параллелизм намного проще в использовании, но не такой гибкий, как подход, управляемый компонентом. Но по моему опыту, это достаточно хорошо для общих случаев использования.

Как мы видели в журнале, управляемый контейнером параллелизм является значением по умолчанию для EJB Singleton. Контейнер устанавливает блокировку записи для всего Singleton и сериализует все вызовы методов.

Мы можем изменить это поведение и определить блокировки чтения и записи на уровне метода и / или класса. Это можно сделать, аннотируя класс Singleton или методы с помощью @ javax.ejb.Lock (javax.ejb.LockType) . Перечисление LockType предоставляет значения WRITE и READ для определения исключительной блокировки записи или блокировки чтения.

В следующем фрагменте показано, как установить Lock для method1 и method2 в LockType.READ .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Singleton
@Remote(SingletonRemote.class)
public class ReadLock implements SingletonRemote {
    Logger logger = Logger.getLogger(ReadLock.class.getName());
 
    private int counter = 0;
 
    @Override
    @Lock(LockType.READ)
    public void method1() {
        this.logger.info("method1: " + counter);
    }
 
    @Override
    @Lock(LockType.READ)
    public void method2() throws Exception {
        this.logger.info("start method2");
        for (int i = 0; i < 100; i++) {
            counter++;
            logger.info("" + counter);
        }
        this.logger.info("end method2");
    }
}

Как уже упоминалось, мы могли бы добиться того же, аннотируя класс с помощью @Lock (LockType.READ) вместо аннотирования обоих методов.

Хорошо, если все работает так, как ожидалось, оба метода должны быть доступны в paralel. Итак, давайте посмотрим на файл журнала.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2014-06-24 21:47:13,290 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 10) method1: 0
2014-06-24 21:47:13,291 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) start method2
2014-06-24 21:47:13,291 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 1
2014-06-24 21:47:13,291 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 2
2014-06-24 21:47:13,291 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 3
   ...
2014-06-24 21:47:13,306 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 68
2014-06-24 21:47:13,307 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 69
2014-06-24 21:47:13,308 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 3) method1: 69
2014-06-24 21:47:13,310 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 70
2014-06-24 21:47:13,310 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 71
   ...
2014-06-24 21:47:13,311 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 76
2014-06-24 21:47:13,311 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 77
2014-06-24 21:47:13,312 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 2) method1: 77
2014-06-24 21:47:13,312 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 78
2014-06-24 21:47:13,312 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 79
   ...
2014-06-24 21:47:13,313 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 83
2014-06-24 21:47:13,313 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 84
2014-06-24 21:47:13,314 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 5) method1: 84
2014-06-24 21:47:13,316 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 85
2014-06-24 21:47:13,316 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 86
2014-06-24 21:47:13,317 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 87
2014-06-24 21:47:13,318 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 88
2014-06-24 21:47:13,318 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 6) method1: 89
2014-06-24 21:47:13,318 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 89
2014-06-24 21:47:13,319 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 90
   ...
2014-06-24 21:47:13,321 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 99
2014-06-24 21:47:13,321 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 100
2014-06-24 21:47:13,321 INFO  [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) end method2

Вывод

В начале этой статьи мы узнали, что Java EE по умолчанию использует управляемую контейнером блокировку записи. Это приводит к сериализованной обработке всех вызовов методов и снижает масштабируемость и производительность приложения. Это то, что мы должны иметь в виду при реализации EJB Singleton.

Мы рассмотрели два существующих варианта управления параллелизмом: параллельный управляемый компонент и управляемый контейнером параллелизм.

Мы использовали управляемый контейнером подход, чтобы определить блокировку чтения для обоих методов нашего синглтона. Это не так гибко, как подход, управляемый bean-компонентом, но его гораздо проще использовать и в большинстве случаев достаточно. Нам просто нужно предоставить аннотацию, а контейнер обработает все остальное.