Синхронизация не финального поля - PullRequest
75 голосов
/ 02 августа 2011

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

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

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

Ответы [ 8 ]

113 голосов
/ 02 августа 2011

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

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

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

41 голосов
/ 02 августа 2011

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

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

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

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

11 голосов
/ 30 января 2014

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

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

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

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

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

Правило № 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 - Альтернатива: Объявить все поля объекта как volatile. (Я не буду здесь говорить о недостатках этого, например, о предотвращении хранения в кеше уровня x даже для операций чтения, также.)

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

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

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

2 голосов
/ 23 июня 2016

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

Из документации Java о пакете atomic :

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

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?

2 голосов
/ 23 марта 2015

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

}
2 голосов
/ 07 октября 2014

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

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

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

public volatile Object lock;

...

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

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

1 голос
/ 22 июля 2012

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

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

1 голос
/ 02 августа 2011

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...