Синхронизация не конечного поля

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

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)
     {  
     }  
   }  
 }  

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

Ответ 1

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

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

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

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

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

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

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

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. Или начните понимать все о синхронизации и сделать это самостоятельно с помощью вложенных блокировок.;)

Для получения дополнительной информации о том, почему синхронизация по не последним полям ломается, просмотрите мой тестовый пример: fooobar.com/questions/32869/...

И для более подробной информации, почему вам требуется синхронизация вообще из-за RAM и кэшей, смотрите здесь: fooobar.com/questions/32855/...

Ответ 4

На самом деле я не вижу правильного ответа, т.е. Это прекрасно, что бы это сделать.

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

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

public volatile Object lock;

...

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

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

Ответ 5

EDIT: Таким образом, это решение (как предложил Джон Скит) может иметь проблему с атомарностью реализации "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();
    }

}

Ответ 6

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

Из java-документации о atomic package:

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

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?

Ответ 7

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

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

Ответ 8

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

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