Статьи

Страшная двойная проверка блокировки языка в Java

Проблема, обсуждаемая в этой статье, не нова, но все же сложна даже для опытных разработчиков. Паттерн синглтона — это распространенная идиома программирования. Тем не менее, при использовании с несколькими потоками, некоторый тип синхронизации должен быть выполнен, чтобы не нарушать код.

В соответствующей статье наш партнер 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 ;
  }
}
Листинг 1

Этот код не является потокобезопасным. Если 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 ;
  }
}
Перечисление 2

Теперь метод 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 ;
  }
}
Перечисление 3

Наш 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 ;
  }
}
Листинг 4

Добавив дополнительную проверку в синхронизированный блок, он обеспечил, чтобы только один поток когда-либо создавал экземпляр образца. Это шаблон двойной проверки. Друг нашего гуру, эксперт по 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 ;
  }
}
Листинг 5

Лучше враг добра!

Byron

Статьи по Теме: