В соответствующей статье наш партнер JCG Манодж Хангаонкар из отчета «Хангаонкар» подробно рассматривает дважды проверенную идиому блокировки, чтобы понять, где она выходит из строя, и представляет все возможные решения:
Не посмотрим, что он скажет:
Проблема с двойной проверкой блокировки в Java хорошо документирована. Тем не менее, даже опытный программист может слишком усердно пытаться оптимизировать синхронизацию кода, который создает одиночные игры и становится жертвой ловушки.
Рассмотрим код
1
2
3
4
5
6
7
8
9
|
public class Sample { private static Sample s = null ; public static Sample getSample() { if (s == null ) { s = new Sample() ; } return s ; } } |
Этот код не является потокобезопасным. Если 2 потока t1 и t2 одновременно входят в метод getSample (), они могут получить разные экземпляры выборки. Это можно легко исправить, добавив ключевое слово synchronized в метод getSample ().
1
2
3
4
5
6
7
8
9
|
public class Sample { private static Sample s = null ; public static synchronized Sample getSample() { if (s == null ) { s = new Sample() ; } return s ; } } |
Теперь метод getSample работает правильно. Перед входом в метод getSample поток t1 получает блокировку. Любой другой поток t2, которому нужно войти в метод, будет блокироваться, пока t1 не выйдет из метода и не снимет блокировку. Код работает. Жизнь хороша. Это где умный программист, если не быть осторожным, может перехитрить себя. Он заметит, что в действительности только первый вызов getSample, который создает экземпляр, должен быть синхронизирован, а последующие вызовы, которые просто возвращают s, платят ненужный штраф. Он решает оптимизировать код для
01
02
03
04
05
06
07
08
09
10
11
|
public class Sample { private static Sample s = null ; public static Sample getSample() { if (s == null ) { synchronized (Sample. class ) { s = new Sample() ; } } return s ; } } |
Наш Java-гуру быстро понимает, что этот код имеет ту же проблему, что и листинг 1. Так что он прекрасно настраивает его дальше.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
public class Sample { private static Sample s = null ; public static Sample getSample() { if (s == null ) { synchronized (Sample. class ) { if (s == null ) { s = new Sample() ; } } } return s ; } } |
Добавив дополнительную проверку в синхронизированный блок, он обеспечил, чтобы только один поток когда-либо создавал экземпляр образца. Это шаблон двойной проверки. Друг нашего гуру, эксперт по Java, приятель просматривает код. Код зарегистрирован и товар отправлен. Жизнь хороша, верно?
Неправильно !! Допустим, поток t1 входит в getSample. s является нулем. Это получает блокировку. В синхронизированном блоке он проверяет, что s по-прежнему равно null, а затем выполняет конструктор для Sample. Перед завершением выполнения конструктора t1 заменяется и t2 получает контроль. Поскольку конструктор не завершился, s частично инициализирован. Это не нуль, но имеет некоторую поврежденную или неполную ценность. Когда t2 входит в getSample, он видит, что s не равен нулю, и возвращает искаженное значение.
Таким образом, шаблон двойной проверки не работает. Варианты: синхронизировать на уровне метода, как в листинге 2, или отказаться от синхронизации и использовать статическое поле, как показано ниже.
1
2
3
4
5
6
7
|
public class Sample { private static Sample INSTANCE = new Sample(); public static Sample getSample() { return INSTANCE ; } } |
Лучше враг добра!
Byron
Статьи по Теме: