Недавно я читал информативный пост о различиях между synchronized
и ReentrantLock
от ReentrantLock
Paul 1 . Он подчеркивает преимущества последнего, но не скрывает некоторые недостатки, которые связаны с громоздким блоком try-finally, необходимым для правильного использования.
Соглашаясь с его заявлениями, я размышлял о мысли, которая всегда беспокоит меня, когда дело доходит до синхронизации. Оба подхода смешивают разные проблемы — синхронизацию и функциональность синхронизированного контента — что затрудняет проверку этих проблем по одному.
Будучи исследовательским типом, я выбрал решение для этой проблемы, которое я уже пробовал в прошлом. Однако в то время мне не очень нравился шаблон программирования. Это было из-за его многословности из-за анонимного класса. Но имея под рукой Java 8 и лямбда-выражения, я подумал, что стоит пересмотреть. Поэтому я скопировал часть «counter» примера Javin Paul, написал простой тестовый пример и начал рефакторинг. Это была начальная ситуация:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
class Counter { private final Lock lock; private int count; Counter() { lock = new ReentrantLock(); } int next() { lock.lock(); try { return count++; } finally { lock.unlock(); } } } |
Хорошо видно уродливый блок try-finally, который создает много шума вокруг реальной функциональности 2 . Идея состоит в том, чтобы переместить этот блок в его собственный класс, который служит аспектом синхронизации, для некоторой операции, которая выполняет добавочную работу. Следующий фрагмент показывает, как может выглядеть такой вновь созданный интерфейс Operation
и как он может использоваться лямбда-выражением 3 :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class Counter { private final Lock lock; private int count; interface Operation<T> { T execute(); } Counter() { lock = new ReentrantLock(); } int next() { lock.lock(); try { Operation<Integer> operation = () -> { return count++; }; return operation.execute(); } finally { lock.unlock(); } } } |
На следующем этапе извлечения класса вводится тип Synchronizer
который служит исполнителем, который обеспечивает выполнение данной Operation
в надлежащих границах синхронизации:
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
33
34
35
36
|
class Counter { private final Synchronizer synchronizer; private int count; interface Operation<T> { T execute(); } static class Synchronizer { private final Lock lock; Synchronizer() { lock = new ReentrantLock(); } private int execute( Operation<Integer> operation ) { lock.lock(); try { return operation.execute(); } finally { lock.unlock(); } } } Counter() { synchronizer = new Synchronizer(); } int next() { return synchronizer.execute( () -> { return count++; } ); } } |
Если я не полностью ошибаюсь, это должно быть сделано так же, как в начальном классе. Ну, тесты были зелеными, но простые тесты JUnit обычно мало помогают в отношении параллелизма. Но с последним изменением по крайней мере возможно проверить правильную последовательность вызовов модульным тестом для обеспечения синхронизации:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
public class Counter { final Synchronizer<Integer> synchronizer; final Operation<Integer> incrementer; private int count; public Counter( Synchronizer<Integer> synchronizer ) { this .synchronizer = synchronizer; this .incrementer = () -> { return count++; }; } public int next() { return synchronizer.execute( incrementer ); } } |
Как вы можете видеть, Operation
и Synchronizer
были перемещены в свои собственные файлы. Таким образом обеспечивается аспект синхронизации, который можно протестировать как отдельный модуль. Класс Counter
теперь использует конструктор для внедрения экземпляра синхронизатора 4 . Кроме того, операция приращения была назначена полю с именем «incrementer». Чтобы облегчить тестирование, видимость финальных полей была открыта по умолчанию. Тест с использованием Mockito, например, для слежки за синхронизатором, теперь может обеспечить правильный вызов синхронизации, например:
1
2
3
4
5
6
7
8
9
|
@Test public void synchronization() { Synchronizer<Integer> synchronizer = spy( new Synchronizer<>() ); Counter counter = new Counter( synchronizer ); counter.next(); verify( synchronizer ).execute( counter.incrementer ); } |
Обычно я не слишком рад использованию проверки вызова метода, так как это создает очень тесную связь между модулем и контрольным примером. Но, учитывая вышеизложенные обстоятельства, это не выглядит для меня слишком плохим компромиссом. Однако я просто делаю первые разминки с Java 8 и лямбда-выражениями, и, возможно, я тоже что-то упускаю из-за параллелизма — так что вы думаете?
- Пример ReentrantLock в Java, разница между синхронизированным и ReentrantLock , Джавин Пол, 7 марта 2013 г.
- Очевидно, достаточно шума, чтобы сбить меня с толку, потому что моя первая тестовая версия не удалась … ↩
- Я решил пойти с возвращаемым значением параметра типа вместо
int
. Таким образом, полученный механизм синхронизации может быть лучше использован повторно. Но я не уверен, что, например, автобокс здесь не критичен из-за производительности или по каким-либо причинам. Таким образом, для общего подхода, вероятно, есть еще несколько вещей, которые следует рассмотреть, которые выходят за рамки этого поста, хотя ↩ - Если по какой-либо причине изменение конструктора невозможно, можно ввести делегирующий конструктор по умолчанию, который внедряет новый экземпляр
Synchronizer
в параметризованный, например:this( new Synchronizer() );
, Этот подход может быть приемлемым для целей тестирования ↩
Ссылка: | Чистая синхронизация с использованием ReentrantLock и Lambdas от нашего партнера по JCG Фрэнка Аппеля в блоге Code Affine . |