Синхронізація нефінального поля


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

Я не впевнений, що наведений вище код є правильним способом синхронізації в нефінальному полі класу. Як я можу синхронізувати не остаточне поле?

Відповіді:


127

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

З огляду на це, немає нічого поганого в синхронізації в нефінальному полі як такому. Потрібно лише мати на увазі, що якщо посилання на об’єкт змінюється, той самий розділ коду може виконуватися паралельно . Тобто, якщо один потік запускає код у синхронізованому блоці і хтось дзвонить setO(...), інший потік може одночасно запускати той самий синхронізований блок на тому самому екземплярі .

Синхронізуйте об’єкт, до якого вам потрібен ексклюзивний доступ (або, ще краще, об’єкт, призначений для його охорони).


1
Я кажу, що, якщо ви синхронізуєтесь у нефінальному полі, ви повинні знати той факт, що фрагмент коду працює з ексклюзивним доступом до об’єкта, на який oпосилався на момент досягнення синхронізованого блоку. Якщо об'єкт, що oпосилається на зміни, може змінитися інший потік і виконати синхронізований блок коду.
aioobe

42
Я не погоджуюсь з вашим емпіричним правилом - я волію синхронізувати об'єкт, єдиною метою якого є охорона іншого стану. Якщо ви ніколи нічого не робите з об’єктом, крім блокування, ви точно знаєте, що жоден інший код не може його заблокувати. Якщо ви зафіксуєте "реальний" об'єкт, методи якого ви потім викликаєте, цей об'єкт може синхронізуватись і сам із собою, що ускладнює міркування щодо блокування.
Джон Скіт,

9
Як я кажу у своїй відповіді, я думаю, що мені потрібно було б дуже ретельно це обґрунтувати, чому ви хочете зробити таке. І я б також не рекомендував синхронізувати this- я б рекомендував створити остаточну змінну в класі виключно для цілей блокування , що зупиняє будь-кого іншого від блокування того самого об'єкта.
Джон Скіт,

1
Це ще один хороший момент, і я погоджуюсь; блокування для не остаточної змінної, безумовно, потребує ретельного обґрунтування.
aioobe

Я не впевнений у проблемах видимості пам'яті, пов’язаних із зміною об’єкта, який використовується для синхронізації. Я думаю, ви зіткнетеся з великими проблемами, змінивши об'єкт, а потім покладаючись на код, який бачить цю зміну правильно, щоб "той самий розділ коду можна було запускати паралельно". Я не впевнений, що, якщо такі є, гарантії поширюються моделлю пам'яті на видимість пам'яті полів, які використовуються для блокування, на відміну від змінних, доступ до яких здійснюється в блоці синхронізації. Моє головне правило було б, якщо ви синхронізуєте щось, воно повинно бути остаточним.
Mike Q

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: Ви тримаєте замок, а інший потік змінює посилання на змінну. Результат той самий: інший потік може потрапити в захищений блок.

Але при використанні фінального фіктивного фіксатора виникає ще одна проблема : ви можете отримати неправильні дані, оскільки ваш нефінальний об'єкт буде синхронізований лише з оперативною пам'яттю під час виклику синхронізації (об'єкта). Отже, як друге правило:

Правило №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 починає вказувати кудись інакше, я хочу, щоб він синхронізувався з новими даними, щоб запобігти паралельним потокам модифікувати його. Якісь проблеми з цим?
Хармен

2

Я насправді не бачу правильної відповіді тут, тобто, цілком нормально робити.

Я навіть не впевнений, чому це попередження, у ньому немає нічого поганого. JVM гарантує, що ви отримаєте якийсь дійсний об'єкт назад (або нульовий) при читанні значення, і ви можете синхронізувати з будь-яким об'єктом.

Якщо ви плануєте фактично змінити замок під час його використання (на відміну від, наприклад, зміни його з методу init, перед тим, як почати його використовувати), вам слід зробити змінну, яку ви плануєте змінити 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

РЕДАКТУВАТИ: Отже, це рішення (як запропонував Джон Скіт) може мати проблему з атомністю реалізації "синхронізованого (об’єкта) {}", коли посилання на об’єкт змінюється. Я запитав окремо, і, за словами містера Еріксона, це не є безпечним для потоку - див .: Чи є введення синхронізованого блоку атомним? . Тож візьміть це як приклад, як цього НЕ робити - із посиланнями чому;)

Подивіться, як би це працювало, якби синхронізація () була атомною:

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. Thx за відповідь! Я чую ваше занепокоєння. Я згоден, що у цьому випадку зовнішній замок дозволить уникнути питання про "синхронізовану атомність". Таким чином було б кращим. Хоча можуть бути випадки, коли ви хочете ввести в середовищі виконання більше конфігурації та поділитися різною конфігурацією для різних груп потоків (хоча це не мій випадок). І тоді це рішення може стати цікавим. Я опублікував запитання stackoverflow.com/questions/29217266/… про синхронізовану () атомність - тож ми побачимо, чи можна його використовувати (і хтось відповість)
Віт Бернатік

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, друга версія є кращим стилем незалежно від того, чи задіяна синхронізація.

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


1

Просто додавши свої два центи: у мене було це попередження, коли я використовував компонент, який створюється за допомогою конструктора, тому це поле насправді не може бути остаточним, оскільки конструктор не може приймати параметри. Іншими словами, у мене було квазіфінальне поле без остаточного ключового слова.

Думаю, саме тому це лише попередження: ви, мабуть, робите щось не так, але це може бути і правильно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.