Синхронизация незавершенного поля

91

Предупреждение отображается каждый раз, когда я синхронизирую неокончательное поле класса. Вот код:

public class X  
{  
   private Object o;  

   public void setO(Object o)  
   {  
     this.o = o;  
   }  

   public void x()  
   {  
     synchronized (o) // synchronization on a non-final field  
     {  
     }  
   }  
 } 

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

 public class X  
 {  

   private final Object o;       
   public X()
   {  
     o = new Object();  
   }  

   public void x()  
   {  
     synchronized (o)
     {  
     }  
   }  
 }  

Я не уверен, что приведенный выше код является правильным способом синхронизации в поле неокончательного класса. Как мне синхронизировать не финальное поле?

divz
источник

Ответы:

127

Прежде всего, я рекомендую вам действительно постараться решить проблемы параллелизма на более высоком уровне абстракции, то есть решить их с помощью классов из java.util.concurrent, таких как ExecutorServices, Callables, Futures и т. Д.

При этом нет ничего плохого в синхронизации неоконечного поля как такового. Вам просто нужно иметь в виду, что при изменении ссылки на объект один и тот же фрагмент кода может выполняться параллельно . То есть, если один поток запускает код в синхронизированном блоке, а кто-то вызывает setO(...), другой поток может одновременно запускать тот же синхронизированный блок в том же экземпляре .

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

aioobe
источник
1
Я говорю, что, если вы синхронизируете неоконечное поле, вы должны знать о том факте, что фрагмент кода выполняется с монопольным доступом к объекту, на который имеется oссылка в момент достижения синхронизированного блока. Если объект, который oссылается на изменение, может прийти другой поток и выполнить блок синхронизированного кода.
aioobe 02
42
Я не согласен с вашим практическим правилом - я предпочитаю синхронизировать объект, единственная цель которого - охранять другое состояние. Если вы никогда ничего не делаете с объектом, кроме блокировки, вы точно знаете, что никакой другой код не может его заблокировать. Если вы заблокируете «настоящий» объект, методы которого вы затем вызываете, этот объект может синхронизироваться и сам с собой, что затрудняет рассуждение о блокировке.
Джон Скит
9
Как я сказал в своем ответе, я думаю, мне нужно было бы очень тщательно обосновать, почему вы хотите сделать это. И я бы также не рекомендовал синхронизацию this- я бы рекомендовал создать конечную переменную в классе исключительно для целей блокировки , которая не позволяет кому-либо другому блокировать тот же объект.
Джон Скит
1
Это еще один хороший момент, и я согласен; блокировка не конечной переменной определенно требует тщательного обоснования.
aioobe 02
Я не уверен в проблемах видимости памяти при изменении объекта, который используется для синхронизации. Я думаю, вы столкнетесь с большими проблемами, если измените объект, а затем полагаетесь на код, который правильно увидит это изменение, чтобы «один и тот же участок кода мог выполняться параллельно». Я не уверен, какие гарантии, если таковые имеются, расширяются моделью памяти на видимость в памяти полей, которые используются для блокировки, в отличие от переменных, к которым осуществляется доступ в блоке синхронизации. Мое практическое правило: если вы синхронизируете что-то, это должно быть окончательно.
Майк Кью,
47

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

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

  • Поток 1 входит в синхронизированный блок. Ура - он имеет эксклюзивный доступ к общим данным ...
  • Поток 2 вызывает setO ()
  • Поток 3 (или еще 2 ...) входит в синхронизированный блок. Ура! Он думает, что имеет эксклюзивный доступ к общим данным, но поток 1 все еще возится с ним ...

Почему вы хотите, чтобы это произошло? Может быть, есть какие- то очень специфические ситуации, в которых это имеет смысл ... но вам нужно будет представить мне конкретный вариант использования (вместе со способами смягчения того сценария, который я дал выше), прежде чем я буду доволен Это.

Джон Скит
источник
2
@aioobe: Но тогда поток 1 все еще может запускать какой-то код, который изменяет список (и часто ссылается на него o) - и на полпути его выполнения запускает мутацию другого списка. Как это было бы хорошей идеей? Я думаю, что мы принципиально не согласны с тем, стоит ли блокировать объекты, которых вы касаетесь другими способами. Я бы предпочел рассуждать о своем коде, не зная, что другой код делает с точки зрения блокировки.
Джон Скит
2
@Felype: Похоже, вам следует задать более подробный вопрос как отдельный вопрос, но да, я часто создавал отдельные объекты как блокировки.
Джон Скит
3
@VitBernatik: Нет. Если поток X начинает изменять конфигурацию, поток Y изменяет значение синхронизируемой переменной, затем поток Z начинает изменять конфигурацию, тогда как X, так и Z будут изменять конфигурацию одновременно, что плохо .
Джон Скит
1
Короче говоря, безопаснее, если мы всегда объявляем такие объекты блокировки окончательными, верно?
Сент-Антарио
2
@LinkTheProgrammer: «Синхронизированный метод синхронизирует каждый отдельный объект в экземпляре» - нет, это не так. Это просто неправда, и вам следует пересмотреть свое понимание синхронизации.
Джон Скит,
12

Я согласен с одним из комментариев Джона: вы всегда должны использовать фиктивную окончательную блокировку при доступе к не конечной переменной, чтобы предотвратить несоответствия в случае изменения ссылки на переменную. Поэтому в любом случае и в качестве первого практического правила:

Правило №1: Если поле не является окончательным, всегда используйте фиктивную (частную) окончательную блокировку.

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

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

Но при использовании макета финальной блокировки возникает другая проблема : вы можете получить неверные данные, потому что ваш неокончательный объект будет синхронизироваться с ОЗУ только при вызове synchronize (object). Итак, второе практическое правило:

Правило № 2: При блокировке неоконечного объекта вам всегда нужно делать и то, и другое: использовать фиктивную окончательную блокировку и блокировку неоконечного объекта для синхронизации ОЗУ. (Единственной альтернативой будет объявление всех полей объекта как изменчивых!)

Эти блокировки также называются «вложенными блокировками». Обратите внимание, что вы должны вызывать их всегда в одном и том же порядке, иначе вы получите тупик :

public class X {
    private final LOCK;
    private Object o;

    public void setO(Object o){
        this.o = o;  
    }  

    public void x() {
        synchronized (LOCK) {
        synchronized(o){
            //do something with o...
        }
        }  
    }  
} 

Как видите, я пишу две блокировки прямо в одной строке, потому что они всегда принадлежат друг другу. Таким образом, вы можете даже сделать 10 блокировок вложенности:

synchronized (LOCK1) {
synchronized (LOCK2) {
synchronized (LOCK3) {
synchronized (LOCK4) {
    //entering the locked space
}
}
}
}

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

synchronized (LOCK4) {
synchronized (LOCK1) {  //dead lock!
synchronized (LOCK3) {
synchronized (LOCK2) {
    //will never enter here...
}
}
}
}

Существует только один способ обхода таких вложенных блокировок при обработке неокончательных полей:

Правило № 2 - Альтернатива: объявите все поля объекта как изменчивые. (Я не буду здесь говорить о недостатках этого, например о предотвращении любого хранения в кешах уровня x даже для чтения, aso.)

Поэтому aioobe совершенно прав: просто используйте java.util.concurrent. Или начать разбираться в синхронизации и делать это самостоятельно с вложенными блокировками. ;)

Для получения дополнительной информации о том, почему синхронизация не конечных полей прерывается, ознакомьтесь с моим тестовым примером: https://stackoverflow.com/a/21460055/2012947

И для получения более подробной информации, почему вам вообще нужна синхронизация из-за ОЗУ и кешей, смотрите здесь: https://stackoverflow.com/a/21409975/2012947

Маркус
источник
1
Я думаю, что вам нужно обернуть установщик oсинхронизированным (LOCK), чтобы установить связь «происходит раньше» между установкой и объектом чтения o. Я обсуждаю это в своем аналогичном вопросе: stackoverflow.com/questions/32852464/…
Petrakeas
Я использую dataObject для синхронизации доступа к членам dataObject. Как это не так? Если объект dataObject начинает указывать где-то в другом месте, я хочу, чтобы он синхронизировался с новыми данными, чтобы предотвратить его изменение параллельными потоками. Есть проблемы с этим?
Harmen
2

Я не вижу здесь правильного ответа, то есть это совершенно нормально.

Я даже не уверен, почему это предупреждение, в этом нет ничего плохого. JVM гарантирует, что вы получите какой-то действительный объект обратно (или null), когда вы читаете значение, и вы можете синхронизировать на любом объектом.

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

public volatile Object lock;

...

synchronized (lock) {
    synchronized (newObject) {
        lock = newObject;
    }
}

Там. Это несложно, написать код с блокировками (мьютексами) на самом деле довольно просто. Написать код без них (код без блокировки) - вот что сложно.

Эван Дарк
источник
Это может не сработать. Скажем, o началось как ссылка на O1, затем поток T1 блокирует o (= O1) и O2 и устанавливает o в O2. В то же время поток T2 блокирует O1 и ждет, пока T1 разблокирует его. Когда он получит блокировку O1, он установит o на O3. В этом сценарии между T1, освобождающим O1, и T2, блокирующим O1, O1 стал недействительным для блокировки через o. В это время другой поток может использовать o (= O2) для блокировки и продолжать непрерывную гонку с T2.
GPS,
2

РЕДАКТИРОВАТЬ: Таким образом, это решение (как было предложено Джоном Скитом) может иметь проблему с атомарностью реализации «synchronized (object) {}» при изменении ссылки на объект. Я спросил отдельно, и, по словам г-на Эриксона, это не потокобезопасно - см. Является ли ввод синхронизированного блока атомарным?. Так что возьмите это как пример, как этого НЕ делать - со ссылками, почему;)

Посмотрите код, как бы он работал, если бы synchronized () был атомарным:

public class Main {
    static class Config{
        char a='0';
        char b='0';
        public void log(){
            synchronized(this){
                System.out.println(""+a+","+b);
            }
        }
    }

    static Config cfg = new Config();

    static class Doer extends Thread {
        char id;

        Doer(char id) {
            this.id = id;
        }

        public void mySleep(long ms){
            try{Thread.sleep(ms);}catch(Exception ex){ex.printStackTrace();}
        }

        public void run() {
            System.out.println("Doer "+id+" beg");
            if(id == 'X'){
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(1000);
                    // do not forget to put synchronize(cfg) over setting new cfg - otherwise following will happend
                    // here it would be modifying different cfg (cos Y will change it).
                    // Another problem would be that new cfg would be in parallel modified by Z cos synchronized is applied on new object
                    cfg.b=id;
                }
            }
            if(id == 'Y'){
                mySleep(333);
                synchronized(cfg) // comment this and you will see inconsistency in log - if you keep it I think all is ok
                {
                    cfg = new Config();  // introduce new configuration
                    // be aware - don't expect here to be synchronized on new cfg!
                    // Z might already get a lock
                }
            }
            if(id == 'Z'){
                mySleep(666);
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(100);
                    cfg.b=id;
                }
            }
            System.out.println("Doer "+id+" end");
            cfg.log();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Doer X = new Doer('X');
        Doer Y = new Doer('Y');
        Doer Z = new Doer('Z');
        X.start();
        Y.start();
        Z.start();
    }

}
Вит Бернатик
источник
1
Это может быть нормально, но я не знаю, есть ли в модели памяти гарантия, что значение, которое вы синхронизируете, является самым последним записанным - я не думаю, что есть гарантия атомарного «чтения и синхронизации». Лично я для простоты стараюсь избегать синхронизации на мониторах, которые в любом случае используются для других целей. (Имея отдельное поле, код становится явно правильным, вместо того чтобы тщательно его рассуждать.)
Джон Скит,
@Jon. Спасибо за ответ! Я слышу твое беспокойство. Я согласен, в этом случае внешняя блокировка позволит избежать вопроса о «синхронизированной атомарности». Таким образом было бы предпочтительнее. Хотя могут быть случаи, когда вы хотите ввести во время выполнения больше конфигурации и совместно использовать разные конфигурации для разных групп потоков (хотя это не мой случай). И тогда это решение может стать интересным. Я опубликовал вопрос stackoverflow.com/questions/29217266/… об атомарности synchronized () - так что мы увидим, можно ли его использовать (и есть ли кто-то ответ)
Вит Бернатик
2

AtomicReference подходит для ваших требований.

Из документации java об атомарном пакете:

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

boolean compareAndSet(expectedValue, updateValue);

Образец кода:

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

String newReference = "value 2";
boolean exchanged = someRef.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);

В приведенном выше примере вы заменяете Stringсвоим собственнымObject

Связанный вопрос SE:

Когда использовать AtomicReference в Java?

Равиндра бабу
источник
1

Если oза время существования экземпляра никогда не меняется X, вторая версия является лучшим стилем, независимо от того, задействована ли синхронизация.

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

NPE
источник
1

Просто добавляю свои два цента: у меня было это предупреждение, когда я использовал компонент, экземпляр которого создается дизайнером, поэтому это поле не может быть окончательным, потому что конструктор не может принимать параметры. Другими словами, у меня было квазифинальное поле без ключевого слова final.

Я думаю, поэтому это просто предупреждение: вы, вероятно, делаете что-то не так, но, возможно, это тоже правильно.

арахисовый орех
источник