Как избежать синхронизации в не конечном поле?

Если у нас есть 2 класса, которые работают на одном и том же объекте под разными потоками, и мы хотим избежать условий гонки, нам придется использовать синхронизированные блоки с тем же монитором, что и в следующем примере:

class A {
    private DataObject mData; // will be used as monitor

    // thread 3
    public setObject(DataObject object) {
       mData = object;
    }

    // thread 1
    void operateOnData() {
        synchronized(mData) {
            mData.doSomething();
            .....
            mData.doSomethingElse();
        }
    }
}

  class B {
    private DataObject mData;  // will be used as monitor

    // thread 3
    public setObject(DataObject object) {
       mData = object;
    }

    // thread 2
    void processData() {
        synchronized(mData) {
            mData.foo();
            ....
            mData.bar();
        }
    }
}

Объект, над которым мы будем работать, будет установлен путем вызова setObject(), после чего он не изменится. Мы будем использовать объект в качестве монитора. Тем не менее, intelliJ будет предупреждать о синхронизации в поле, отличном от конечного.

В этом конкретном сценарии, является ли нелокальное поле приемлемым решением?

Еще одна проблема с вышеприведенным подходом заключается в том, что не гарантируется, что монитор (mData) будет отмечен по потоку 1 или потоку 2 после того, как он задан потоком 3, потому что " до того, как между установкой и чтением монитора не было установлено взаимосвязи. Его можно было бы наблюдать как null по потоку 1, например. Является ли мое предположение правильным?

Что касается возможных решений, создание поточной защиты DataObject не является вариантом. Установка монитора в конструкторе классов и объявление его final может работать.

ИЗМЕНИТЬ Семантически, необходимое взаимное исключение связано с DataObject. Именно по этой причине я не хочу иметь дополнительный монитор. Одним из решений было бы добавить методы lock() и unlock() на DataObject, которые необходимо вызвать перед началом работы над ним. Внутри они будут использовать объект Lock. Таким образом, метод operateOnData() становится следующим:

 void operateOnData() {
     mData.lock()
     mData.doSomething();
     .....
     mData.doSomethingElse();
     mData.unlock();
 }

Ответ 1

Вы можете создать обертку

class Wrapper
{
    DataObject mData;

    synchronized public setObject(DataObject mData)
    {
        if(this.mData!=null) throw ..."already set"
        this.mData = mData;
    }

    synchronized public void doSomething()
    {
        if(mData==null) throw ..."not set"

        mData.doSomething();
    }

Объект-оболочка создается и передается в и B

class A 
{
    private Wrapper wrapper; // set by constructor

    // thread 1
    operateOnData() 
    {
        wrapper.doSomething();
    }

В потоке 3 также содержится ссылка на обертку; он вызывает setObject(), когда он доступен.

Ответ 2

Простое решение - просто определить открытый статический конечный объект для использования в качестве блокировки. Объявите это следующим образом:

/**Used to sync access to the {@link #mData} field*/
public static final Object mDataLock = new Object();

Затем в программе синхронизируется по mDataLock вместо mData.

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

Этот метод синхронизации устраняет эту возможность. Это также очень низкая стоимость.

Кроме того, статическая блокировка означает, что все экземпляры класса имеют один замок. В этом случае это похоже на то, что вы хотите.

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

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

Это особенно актуально, если у вас есть несколько параллельных экземпляров этих классов.

Ответ 3

Некоторые платформы предоставляют явные примитивы памяти-барьера, которые гарантируют, что если один поток будет записывать в поле, а затем создает барьер записи, любой поток, который никогда не исследовал объект, о котором идет речь, может быть гарантированно увидеть эффект от этой записи. К сожалению, по состоянию на последний раз, когда я задавал такой вопрос, Самый дешевый способ установления происходит раньше, чем в не финальном поле, единственный раз, когда Java может предлагать какие-либо гарантии потоковой передачи семантикой, не требуя каких-либо специальных действий от имени потока чтения, было использование полей final. Java гарантирует, что любые ссылки, сделанные объекту через конечное поле, будут видеть любые магазины, которые были выполнены для конечных или не-полей этого объекта до того, как ссылка была сохранена в конечном поле, но это отношение не является транзитивным. Таким образом, учитывая

class c1 { public final c2 f; 
           public c1(c2 ff) { f=ff; } 
         }
class c2 { public int[] arr; }
class c3 { public static c1 r; public static c2 f; }

Если единственное, что когда-либо записывает на c3, это поток, который выполняет код:

c2 cc = new c2();
cc.arr = new int[1];
cc.arr[0] = 1234;
c3.r = new c1(cc);
c3.f = c3.r.f;

выполняется второй поток:

int i1=-1;
if (c3.r != null) i1=c3.r.f.arr[0];

а третий поток выполняет:

int i2=-1;
if (c3.f != null) i2=c3.f.arr[0];

Стандарт Java гарантирует, что второй поток будет, если условие if дает true, установите i1 в 1234. Однако третий поток может увидеть ненулевое значение для c3.f и но см. значение null для c3.arr или см. ноль в c3.f.arr[0]. Несмотря на то, что значение, хранящееся в c3.f, было прочитано из c3.r.f, и все, что читает ссылку final c3.r.f, требуется для просмотра любых изменений, внесенных в этот объект, идентифицированных таким образом, до того, как была написана ссылка c3.r.f, ничего в стандарте Java запретит JIT перестраивать первый код потока как:

c2 cc = new c2();
c3.f = cc;
cc.arr = new int[1];
cc.arr[0] = 1234;
c3.r = new c1(cc);

Такой переписывание не повлияет на второй поток, но может нанести ущерб третьим.