Потокобезопасный класс в Java с помощью синхронизированных блоков - PullRequest
17 голосов
/ 08 марта 2012

Допустим, у нас очень простой класс Java MyClass.

public class MyClass {
   private int number;

    public MyClass(int number) {
        this.number = number;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }
}

Существует три способа создания поточно-ориентированного Java-класса с некоторым состоянием:

  1. Сделайте его действительно неизменным

    public class MyClass {
       private final int number;
    
       public MyClass(int number) {
        this.number = number;
       }
    
       public int getNumber() {
        return number;
       }
    
    }
    
  2. Сделать поле number volatile.

    public class MyClass {
       private volatile int number;
    
       public MyClass(int number) {
        this.number = number;
       }
    
       public int getNumber() {
           return number;
       }
    
       public void setNumber(int number) {
           this.number = number;
       }
    }
    
  3. Используйте блок synchronized. Классическая версия этого подхода описана в главе 4.3.5 Java Concurrency на практике. И самое забавное в том, что в примере с ошибками, упомянутыми в этой книге, есть ошибка.

    public class MyClass {
       private int number;
    
       public MyClass(int number) {
           setNumber(number);
       }
    
       public synchronized int getNumber() {
           return number;
       }
    
       public synchronized void setNumber(int number) {
           this.number = number;
       }
    }
    

Есть еще один факт, который следует добавить в контекст обсуждения. В многопоточной среде JVM может свободно переупорядочивать инструкции вне блока synchronized, сохраняя логическую последовательность и отношения случай-до , заданные JVM. Это может привести к публикации объекта, который еще не создан должным образом в другом потоке.

У меня есть пара вопросов относительно третьего случая.

  1. Будет ли это эквивалентно следующему коду:

    public class MyClass {
       private int number;
    
       public MyClass(int number) {
           synchronized (this){
               this.number = number;
           }
       }
    
       public synchronized int getNumber() {
           return number;
       }
    
       public synchronized void setNumber(int number) {
           this.number = number;
       }
    }
    
  2. Будет ли предотвращен переупорядочение в третьем случае или JVM сможет переупорядочить вмешательства и, следовательно, опубликовать объект со значением по умолчанию в поле number?

  3. Если ответ на второй вопрос положительный, у меня есть еще один вопрос.

     public class MyClass {
       private int number;
    
       public MyClass(int number) {
           synchronized (new Object()){
               this.number = number;
           }
       }
    
       public synchronized int getNumber() {
           return number;
       }
    
       public synchronized void setNumber(int number) {
           this.number = number;
       }
    }
    

Этот странно выглядящий synchronized (new Object()) должен предотвратить эффект переупорядочения. Будет ли это работать?

Просто чтобы прояснить, все эти примеры не имеют практического применения. Мне просто любопытны нюансы многопоточности.

Ответы [ 2 ]

8 голосов
/ 08 марта 2012

synchronized(new Object()) ничего не будет делать, поскольку синхронизация происходит только на объекте, с которым вы синхронизируете. Поэтому, если поток A синхронизируется на oneObject, а поток B синхронизируется на anotherObject, между ними не происходит никаких событий. Поскольку мы можем точно знать, что ни один другой поток никогда не будет синхронизироваться с new Object(), который вы там создадите, это не установит предшествующее событие между любым другим потоком.

Что касается вашего synchronzied в конструкторе, если ваш объект безопасно опубликован в другом потоке, он вам не нужен; и если это не так, вы, вероятно, в беспорядке, как это. Я задал этот вопрос в списке интересов параллелизма немного назад, и интересная тема привела . В частности, посмотрите это письмо , в котором указано, что даже при синхронизации вашего конструктора другой поток при отсутствии безопасной публикации может видеть значения по умолчанию в ваших полях, а это письмо , которое (imho ) связывает все это вместе.

2 голосов
/ 08 марта 2012

В вопросе № 3, synchronized(new Object()) не является опцией и ничего не помешает. Компилятор может определить, что никакие другие потоки не могут синхронизироваться с этим объектом (поскольку ничто другое не может получить доступ к объекту.) Это явный пример из статьи Брайана Гетца " Теория и практика Java: оптимизация синхронизации в Mustang " .

Даже если вам нужно было выполнить синхронизацию в конструкторе, и даже если ваш synchronized(new Object()) блок был полезен, т. Е. Вы выполняли синхронизацию с другим долгоживущим объектом, поскольку другие методы синхронизируются на this возникают проблемы с видимостью, если вы не синхронизируете одну и ту же переменную. То есть вы действительно хотите, чтобы ваш конструктор также использовал synchronized(this).

В сторону:

Синхронизация на this считается плохой формой. Вместо этого синхронизируйте на каком-нибудь закрытом финальном поле. Абоненты могут синхронизироваться на вашем объекте, что может привести к тупику. Учтите следующее:

public class Foo
{
    private int value;
    public synchronized int getValue() { return value; }
    public synchronized void setValue(int value) { this.value = value; }
}

public class Bar
{
    public static void deadlock()
    {
        final Foo foo = new Foo();
        synchronized(foo)
        {
            Thread t = new Thread() { public void run() { foo.setValue(1); } };
            t.start();
            t.join();
        }
    }
}

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

...