Потокобезопасный класс в Java с помощью синхронизированных блоков

Скажем, у нас очень простой 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-класса, который имеет некоторое состояние:

  • Сделайте это по-настоящему неизменным

    public class MyClass {
       private final int number;
    
       public MyClass(int number) {
        this.number = number;
       }
    
       public int getNumber() {
        return number;
       }
    
    }
    
  • Сделать поле 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;
       }
    }
    
  • Используйте блок 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;
       }
    }
    

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

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

  • Будет ли это эквивалентно следующему фрагменту кода:

    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;
       }
    }
    
  • Будет ли предотвращено переупорядочение в третьем случае или возможно, чтобы JVM изменила порядок надстроек и, следовательно, опубликовала объект со значением по умолчанию в поле number?

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

     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()) должен предотвращать эффект переупорядочения. Будет ли это работать?

Чтобы быть ясными, все эти примеры не имеют практических приложений. Мне просто интересно узнать о нюансах многопоточности.

Ответы

Ответ 1

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

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

Ответ 2

В вопросе №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, что это затормозит. Лучше всего держать свою семантику блокировки внутренней и частной для вашего класса.