Частный конструктор, чтобы избежать состояния гонки

Я читаю книгу Java Concurrency in Practice session 4.3.5

  @ThreadSafe
  public class SafePoint{

       @GuardedBy("this") private int x,y;

       private SafePoint (int [] a) { this (a[0], a[1]); }

       public SafePoint(SafePoint p) { this (p.get()); }

       public SafePoint(int x, int y){
            this.x = x;
            this.y = y;
       }

       public synchronized int[] get(){
            return new int[] {x,y};
       }

       public synchronized void set(int x, int y){
            this.x = x;
            this.y = y;
       }

  }

Я не знаю, где Он говорит

Частный конструктор существует, чтобы избежать условия гонки, которое произойдет, если конструктор копирования был реализован как это (p.x, p.y); это пример идиомы захвата частного конструктора (Bloch and Gafter, 2005).

Я понимаю, что он предоставляет getter для извлечения как x, так и y сразу в массиве вместо отдельного getter для каждого, поэтому вызывающий будет видеть постоянное значение, но почему частный конструктор? что здесь трюк

Ответ 1

Здесь уже есть куча ответов, но я бы очень хотел погрузиться в некоторые подробности (насколько мне известно). Я настоятельно рекомендую вам запускать каждый образец, присутствующий здесь в ответе, чтобы убедиться в том, что происходит и почему.

Чтобы понять решение, вам нужно сначала понять проблему.

Предположим, что класс SafePoint выглядит следующим образом:

class SafePoint {
    private int x;
    private int y;

    public SafePoint(int x, int y){
        this.x = x;
        this.y = y;
    }

    public SafePoint(SafePoint safePoint){
        this(safePoint.x, safePoint.y);
    }

    public synchronized int[] getXY(){
        return new int[]{x,y};
    }

    public synchronized void setXY(int x, int y){
        this.x = x;
        //Simulate some resource intensive work that starts EXACTLY at this point, causing a small delay
        try {
            Thread.sleep(10 * 100);
        } catch (InterruptedException e) {
         e.printStackTrace();
        }
        this.y = y;
    }

    public String toString(){
      return Objects.toStringHelper(this.getClass()).add("X", x).add("Y", y).toString();
    }
}

Какие переменные создают состояние этого объекта? Только два из них: x, y. Защищены ли они каким-то механизмом синхронизации? Ну, они по встроенной блокировке, через синхронизированное ключевое слово - по крайней мере, в сеттерах и геттерах. Они "тронуты" где-нибудь еще? Конечно, здесь:

public SafePoint(SafePoint safePoint){
    this(safePoint.x, safePoint.y);
} 

Что вы здесь делаете, это чтение с вашего объекта. Чтобы класс был безопасным потоком, вам необходимо координировать доступ для чтения/записи к нему или синхронизировать с той же блокировкой. Но здесь ничего подобного не происходит. Метод setXY действительно синхронизирован, но конструктор клона не работает, поэтому вызов этих двух может быть выполнен небезопасным способом. Можем ли мы тормозить этот класс?

Попробуйте это:

public class SafePointMain {
public static void main(String[] args) throws Exception {
    final SafePoint originalSafePoint = new SafePoint(1,1);

    //One Thread is trying to change this SafePoint
    new Thread(new Runnable() {
        @Override
        public void run() {
            originalSafePoint.setXY(2, 2);
            System.out.println("Original : " + originalSafePoint.toString());
        }
    }).start();

    //The other Thread is trying to create a copy. The copy, depending on the JVM, MUST be either (1,1) or (2,2)
    //depending on which Thread starts first, but it can not be (1,2) or (2,1) for example.
    new Thread(new Runnable() {
        @Override
        public void run() {
            SafePoint copySafePoint = new SafePoint(originalSafePoint);
            System.out.println("Copy : " + copySafePoint.toString());
        }
    }).start();
}
}

Вывод легко следующий:

 Copy : SafePoint{X=2, Y=1}
 Original : SafePoint{X=2, Y=2} 

Это логично, потому что один поток обновляет = пишет наш объект, а другой читает его. Они не синхронизируются с некоторой общей блокировкой, поэтому вывод.

Решение

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

  • может использоваться другая блокировка, такая как блокировка реентера (если ключевое слово synchronized не может использоваться). Но это также не сработает, потому что первый оператор внутри конструктора должен быть вызовом этого/супер. Если мы реализуем другой замок, тогда первая строка должна быть примерно такой:

    lock.lock()//где lock - ReentrantLock, компилятор не допустит этого по указанной выше причине.

  • что, если мы сделаем конструктор методом? Конечно, это сработает!

Смотрите этот код, например

/*
 * this is a refactored method, instead of a constructor
 */
public SafePoint cloneSafePoint(SafePoint originalSafePoint){
     int [] xy = originalSafePoint.getXY();
     return new SafePoint(xy[0], xy[1]);    
}

И вызов будет выглядеть так:

 public void run() {
      SafePoint copySafePoint = originalSafePoint.cloneSafePoint(originalSafePoint);
      //SafePoint copySafePoint = new SafePoint(originalSafePoint);
      System.out.println("Copy : " + copySafePoint.toString());
 }

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

Нам нужно найти способ чтения и записи в SafePoint, синхронизированный с той же блокировкой.

В идеале мы хотели бы что-то вроде этого:

 public SafePoint(SafePoint safePoint){
     int [] xy = safePoint.getXY();
     this(xy[0], xy[1]);
 }

Но компилятор этого не допускает.

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

private SafePoint(int [] xy){
    this(xy[0], xy[1]);
}

И затем, фактическое invokation:

public  SafePoint (SafePoint safePoint){
    this(safePoint.getXY());
}

Обратите внимание, что конструктор является закрытым, потому что мы не хотим раскрывать еще один публичный конструктор и снова думать об инвариантах класса, поэтому мы делаем его закрытым - и только мы можем его вызывать.

Ответ 2

Частный конструктор является альтернативой:

public SafePoint(SafePoint p) {
    int[] a = p.get();
    this.x = a[0];
    this.y = a[1];
}

но позволяет цепочку конструкторов избегать дублирования инициализации.

Если SafePoint(int[]) были общедоступными, класс SafePoint не мог гарантировать безопасность потока, поскольку содержимое массива могло быть изменено другим потоком, содержащим ссылку на тот же массив, между значениями x и y считывается классом SafePoint.

Ответ 3

Конструкторы в Java не могут быть синхронизированы.

Мы не можем реализовать public SafePoint(SafePoint p) как { this (p.x, p.y); }, потому что

Поскольку мы не синхронизированы (и не можем, как мы находимся в конструкторе) во время выполнения конструктора кто-то может вызывать SafePoint.set() из другого потока

public synchronized void set(int x, int y){
        this.x = x; //this value was changed
-->     this.y = y; //this value is not changed yet
   }

поэтому мы будем читать объект в несогласованном состоянии.

Итак, вместо этого мы создаем моментальный снимок в потокобезопасном виде и передаем его частному конструктору. Конфайнмент стека защищает ссылку на массив, поэтому вам не о чем беспокоиться.

Обновление Ха! Что касается трюка, все просто - вы пропустили аннотацию @ThreadSafe из книги в вашем примере:

@ThreadSafe

открытый класс SafePoint {}

поэтому, если конструктор, который принимает массив int в качестве аргумента, будет общедоступным или защищенным, класс перестанет быть потокобезопасным, поскольку содержимое массива может измениться так же, как класс SafePoint (т.е. кто-то может измените его во время выполнения конструктора)!

Ответ 4

Я понимаю, что он предоставляет getter для извлечения как x, так и y сразу в массиве вместо отдельного getter для каждого, поэтому вызывающий будет видеть постоянное значение, но почему частный конструктор? какой трюк здесь?

Мы хотим здесь цепочки вызовов конструктора, чтобы избежать дублирования кода. В идеале что-то подобное нам нужно:

public SafePoint(SafePoint p) {
    int[] values = p.get();
    this(values[0], values[1]);
}

Но это не сработает, потому что мы получим ошибку компилятора:

call to this must be first statement in constructor

И мы не можем использовать это:

public SafePoint(SafePoint p) {
    this(p.get()[0], p.get()[1]); // alternatively this(p.x, p.y);
}

Потому что тогда у нас есть условие, в котором значения могли быть изменены между вызовом p.get().

Итак, мы хотим захватить значения из SafePoint и chain в другой конструктор. Вот почему мы будем использовать идиому захвата частного конструктора и фиксировать значения в частном конструкторе и цепочке в "реальном" конструкторе:

private SafePoint(int[] a) {
    this(a[0], a[1]);
}

Также обратите внимание, что

private SafePoint (int [] a) { this (a[0], a[1]); }

не имеет никакого смысла вне класса. 2-D точка имеет два значения, а не произвольные значения, подобные массиву. У него нет проверок длины массива или нет null. Он используется только в классе, и вызывающий абонент знает, что безопасно звонить с двумя значениями из массива.

Ответ 5

Цель использования SafePoint - всегда обеспечивать последовательный просмотр x и y.

Например, рассмотрим SafePoint (1,1). И один поток пытается прочитать этот SafePoint, в то время как другой поток пытается изменить его на (2,2). Если бы безопасная точка не была потокобезопасной, было бы возможно увидеть представления, в которых SafePoint будет (1,2) (или (2,1)), что является непоследовательным.

Первым шагом на пути к обеспечению последовательного представления потока является не обеспечение независимого доступа к x и y; но и предоставить способ доступа к ним одновременно. Аналогичный контракт применяется для методов модификатора.

В этот момент, если конструктор копирования не реализован внутри SafePoint, то он полностью. Но если мы его реализуем, мы должны быть осторожны. Конструкторы не могут быть синхронизированы. Реализации, такие как следующие, будут выставлять несогласованное состояние, поскольку к ним обращаются к файлам p.x и p.y независимо.

   public SafePoint(SafePoint p){
        this.x = p.x;
        this.y = p.y;
   }

Но следующее не будет нарушать безопасность потоков.

   public SafePoint(SafePoint p){
        int[] arr = p.get();
        this.x = arr[0];
        this.y = arr[1];
   }

Для повторного использования кода реализуется частный конструктор, который принимает массив int, который делегирует это (x, y). Конструктор массива int можно обнародовать, но в действительности он будет похож на этот (x, y).

Ответ 6

Конструктор не должен использоваться вне этого класса. Клиенты не должны создавать массив и передавать его этому конструктору.

Все остальные публичные конструкторы подразумевают, что будет вызван метод get SafePoint.

Частный конструктор позволит вам создать свой собственный, возможно, небезопасный путь Thread (т.е. путем получения x, y отдельно, построения массива и передачи его)

Ответ 7

Частный SafePoint (int [] a) предоставляет две функции:

Во-первых, запретите другим использовать следующий конструктор, потому что другие потоки могут получить ref в массиве и могут изменять массив при построении

int[] arr = new int[] {1, 2};
// arr maybe obtained by other threads, wrong constructor
SafePoint safepoint = new SafePoint(arr); 

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

Частный конструктор существует, чтобы избежать условия гонки, которое произойдет, если конструктор копирования был реализован как это (p.x, p.y)

//p may be obtined by other threads, wrong constructor
public SafePoint(SafePoint p) { this(p.x, p.y);}

См. авторскую реализацию: вам не нужно беспокоиться. p изменяется другими потоками, так как p.get() возвращает новую копию, также p.get() защищается p, поэтому p не будет изменен, даже полученные другими потоками!

public SafePoint(SafePoint p) {
    this(p.get());
}
public synchronized int[] get() {
    return new int[] {x, y};
}

Ответ 8

Что это значит, если у вас нет частного конструктора, и вы реализуете конструктор копирования следующим образом:

public SafePoint(SafePoint p) {
    this(p.x, p.y);
}

Теперь предположим, что поток A имеет доступ к SafePoint p выполняет над конструктором копии эту инструкцию (px, py) и в неудачный момент времени другой поток B также имеет доступ к SafePoint p выполняет setter set (int x, int y) в SafePoint p. Поскольку ваш конструктор копирования имеет доступ к переменной экземпляра p x и y без надлежащей блокировки, он может видеть несогласованное состояние SafePoint p.

Где, когда частный конструктор обращается к p переменным x и y через getter, который синхронизирован, поэтому вы гарантированно увидите постоянное состояние SafePoint p.