Уникайте синхронізації (цього) на Java?


381

Кожного разу, коли виникає запитання щодо синхронізації Java, деякі люди дуже хочуть зазначити, чого synchronized(this)слід уникати. Натомість, вони стверджують, слід віддати перевагу замок на приватній довідці.

Деякі з наведених причин:

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

Мені цікаво побачити приклади реального світу (без речей Foobar), де thisкраще уникати блокування , коли synchronized(this)також робити цю роботу.

Тому: чи слід завжди уникати synchronized(this)та замінювати його замком на приватній довідці?


Деякі додаткові відомості (оновлені у вигляді відповідей):

  • ми говоримо про синхронізацію екземплярів
  • розглядаються як неявні ( synchronizedметоди), так і явні формиsynchronized(this)
  • якщо ви цитуєте Bloch чи інші повноваження з цього приводу, не залишайте частин, які вам не подобаються (наприклад, Ефективна Java, пункт про безпеку теми: Зазвичай це блокування самого екземпляра, але є винятки.)
  • якщо вам потрібна деталізація у вашому блокуванні, окрім synchronized(this)передбачених, то synchronized(this)це не застосовується, тому це не проблема

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

За відсутності внутрішнього синхронізації, і коли потрібен зовнішній синхронізацію, блокування часто є самим екземпляром, в основному говорить Блох. То чому б це не стосувалося і внутрішньої синхронізації з блокуванням "це"? (Важливість документації - це інше питання.)
eljenso

Існує компроміс між розширеною деталізацією та додатковим кешем процесора та запитами на шину, оскільки для блокування зовнішнього Об'єкта, швидше за все, буде потрібна зміна окремої кешової лінії та обмін між кешами процесора (пор. MESIF та MOESI).
АртемГр

1
Я думаю, що у світі оборонного програмування ви запобігаєте помилкам не за ідіомою, а за кодом. Коли хтось задає мені запитання: "Наскільки оптимізована ваша синхронізація?", Я хочу сказати "Дуже" замість "Дуже, якщо тільки хтось не дотримується ідіоми".
Sgene9

Відповіді:


132

Я висвітлю кожну точку окремо.

  1. Якийсь злий код може вкрасти ваш замок (дуже популярний цей, також є "випадково" варіант)

    Мене більше хвилює випадково . Це означає, що це використання thisє частиною відкритого інтерфейсу вашого класу і має бути задокументовано. Іноді потрібна можливість іншого коду використовувати ваш замок. Це стосується таких речей Collections.synchronizedMap(див. Javadoc).

  2. Усі синхронізовані методи в межах одного класу використовують точно такий же замок, що зменшує пропускну здатність

    Це надмірно спрощене мислення; просто позбутися synchronized(this)не вирішить проблему. Правильна синхронізація для пропускної здатності займе більше роздумів.

  3. Ви (зайво) виставляєте занадто багато інформації

    Це варіант №1. Використання - synchronized(this)це частина вашого інтерфейсу. Якщо ви не хочете / потребуєте цього викриття, не робіть цього.


1. "синхронізований" не є частиною відкритого інтерфейсу вашого класу. 2. згоден 3. див. 1.
eljenso

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

15
Подібні. Див. Javadoc for Collections.synchronizedMap () - повернутий об'єкт використовує синхронізовану (це) внутрішньо, і вони сподіваються, що споживач скористається цим, щоб використовувати той самий замок для масштабних атомних операцій, як ітерація.
Даррон

3
Насправді Collections.synchronizedMap () НЕ використовує внутрішньо синхронізований (це), він використовує приватний кінцевий об'єкт блокування.
Bas Leijdekkers

1
@Bas Leijdekkers: документація чітко вказує, що синхронізація відбувається на поверненому екземплярі карти. Що цікаво, це те, що погляди, які повертаються keySet()та values()не фіксуються (їх) this, але екземпляр карти, що важливо для послідовної поведінки для всіх операцій з картою. Причина, що об'єкт блокування перетворюється на змінну, полягає в тому, що підклас SynchronizedSortedMapпотребує його для реалізації підкарт, які блокуються в оригінальному екземплярі карти.
Холгер

86

Ну, по-перше, слід зазначити, що:

public void blah() {
  synchronized (this) {
    // do stuff
  }
}

семантично еквівалентний:

public synchronized void blah() {
  // do stuff
}

що є однією з причин не використовувати synchronized(this). Ви можете стверджувати, що ви можете робити речі навколо synchronized(this)блоку. Звичайна причина - намагатися уникати синхронізованої перевірки взагалі, що призводить до різноманітних проблем одночасності, зокрема до подвійної перевіреної блокування , яка просто показує, наскільки складно зробити порівняно просту перевірку нитки безпечні.

Приватний замок - це захисний механізм, який ніколи не є поганою ідеєю.

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

synchronized(this) просто насправді нічого не дає тобі.


4
"синхронізований (це) просто нічого не дає вам." Гаразд, я замінюю його на синхронізацію (myPrivateFinalLock). Що це мені дає? Ви говорите про те, що це оборонний механізм. Від чого я захищений?
eljenso

14
Ви захищені від випадкового (або зловмисного) блокування "цього" зовнішніми об'єктами.
клент

14
Я зовсім не згоден з цією відповіддю: блокування завжди повинно утримуватися на найкоротший термін, і саме це є причиною, чому ви хочете "робити речі" навколо синхронізованого блоку, а не синхронізувати весь метод .
Олів'є

5
Робити речі поза синхронізованим блоком завжди цілеспрямовано. Справа в тому, що люди трапляються в цьому неправильно багато часу і навіть не усвідомлюють цього, як у проблемі із замкненою перевіркою. Дорога до пекла вимощена добрими намірами.
клент

16
Я взагалі не згоден із тим, що "X - це захисний механізм, який ніколи не є поганою ідеєю". Через таке ставлення багато непотрібно роздутого коду.
finnw

54

Під час використання синхронізованого (це) ви використовуєте екземпляр класу як замок. Це означає, що поки замок набувається за допомогою потоку 1 , нитка 2 повинна зачекати.

Припустимо наступний код:

public void method1() {
    // do something ...
    synchronized(this) {
        a ++;      
    }
    // ................
}


public void method2() {
    // do something ...
    synchronized(this) {
        b ++;      
    }
    // ................
}

Спосіб 1 модифікація змінної a та метод 2 модифікації змінної b , слід уникати одночасної модифікації однієї і тієї ж змінної двома потоками, і це так. АЛЕ під час потоку1 модифікація a та потока2 модифікація b це може бути виконано без будь-яких умов перегону.

На жаль, наведений вище код не дозволить цього, оскільки ми використовуємо ту саму посилання для блокування; Це означає, що нитки, навіть якщо вони не перебувають у гоночному стані, повинні чекати, і, очевидно, код жертвує паралельністю програми.

Рішення полягає у використанні двох різних замків для двох різних змінних:

public class Test {

    private Object lockA = new Object();
    private Object lockB = new Object();

    public void method1() {
        // do something ...
        synchronized(lockA) {
            a ++;      
        }
        // ................
    }


    public void method2() {
        // do something ...
        synchronized(lockB) {
            b ++;      
        }
        // ................
    }

}

У наведеному вище прикладі використовуються більш дрібнозернисті замки (2 замки замість одного ( lockA і lockB для змінних a і b відповідно), і в результаті дозволяє покращити одночасність, з іншого боку, він став складнішим, ніж перший приклад ...


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

7
Гранульованість, не надана "синхронізованою (це)", виходить за межі мого питання. І чи не повинні ваші поля блокування бути остаточними?
eljenso

10
щоб мати глухий кут, ми повинні здійснити дзвінок із блоку, синхронізованого A, до блоку, синхронізованого Б. Давебом, ви помиляєтесь ...
Андреас Бакуров

3
Наскільки я бачу, в цьому прикладі немає жодної тупикової точки. Я приймаю, що це просто псевдокод, але я би використовував одну з реалізацій java.util.concurrent.locks.Lock як java.util.concurrent.locks.ReentrantLock
Shawn Vader

15

Хоча я згоден з тим, що сліпо не дотримуватися догматичних правил, чи здається вам сценарій "викрадення замків" настільки ексцентричним? Потік справді може придбати замок на вашому об'єкті "зовні" ( synchronized(theObject) {...}), блокуючи інші потоки, що чекають синхронізованих методів екземпляра.

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

"Випадкова" версія здається менш імовірною, але, як кажуть, "зробіть щось ідіотське і хтось придумає кращого ідіота".

Тож я згоден із школою думки, що залежить від неї.


Редагуйте перші 3 коментаря eljenso:

Я ніколи не відчував проблеми з крадіжкою замка, але ось уявний сценарій:

Скажімо, ваша система є контейнером сервлетів, і об'єктом, який ми розглядаємо, є ServletContextреалізація. Його getAttributeметод повинен бути безпечним для потоків, оскільки атрибути контексту є спільними даними; тому ви заявляєте це якsynchronized . Давайте також уявимо, що ви надаєте службу публічного хостингу на основі своєї контейнерної реалізації.

Я ваш клієнт і розгорніть на вашому сайті "хороший" сервлет. Буває, що мій код містить дзвінок наgetAttribute .

Хакер, замаскований на іншого клієнта, розгортає на своєму сайті зловмисний сервлет. Він містить наступний код уinit методі:

синхронізовано (this.getServletConfig (). getServletContext ()) {
   в той час як (правда) {}
}

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

Ця атака неможлива, якщо getAttributeсинхронізовано на приватному блокуванні, оскільки сторонній код не може придбати цей замок.

Я визнаю, що приклад надуманий і надзвичайно спрощений погляд на те, як працює контейнер сервлетів, але IMHO це доводить суть.

Тож я зробив би свій вибір дизайну на основі розгляду безпеки: чи буду я мати повний контроль над кодом, який має доступ до екземплярів? Що може бути наслідком того, що нитка тримає замок на екземплярі безстроково?


це-залежить-від-що-клас-робить: якщо це "важливий" об'єкт, тоді заблокувати приватне посилання? Ще вистачить блокування екземпляра?
eljenso

6
Так, сценарій викрадення блокування мені здається далеко надуманим. Всі це згадують, але хто це насправді зробив чи пережив? Якщо ви "випадково" заблокували об'єкт, якого ви не повинні, то є така назва ситуації: це помилка. Полагодьте це.
eljenso

4
Крім того, блокування внутрішніх посилань не вільне від "зовнішньої синхронізованої атаки": якщо ви знаєте, що певна синхронізована частина коду очікує, що відбудеться зовнішня подія (наприклад, запис файлу, значення в БД, подія таймера), ви, ймовірно, можете домовитися про те, щоб блокувати також.
eljenso

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

12

Це залежить від ситуації.
Якщо існує лише одне об'єднання спільного використання або більше одного.

Дивіться повний робочий приклад тут

Невеликий вступ.

Нитки та об'єкти, що
поділяються, Можливість доступу до декількох потоків до однієї сутності, наприклад, декілька з'єднань. Оскільки потоки працюють одночасно, може виникнути шанс переосмислити свої дані іншим, що може бути заплутаною ситуацією.
Отже, нам потрібен певний спосіб забезпечити доступ до доступу до спільного доступу лише однією ниткою. (КОНКУРЕНТ).

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

синтаксис.

synchronized(this)
{
  SHARED_ENTITY.....
}

"це" забезпечило внутрішнє блокування, пов'язане з класом (розробник Java розробив клас Object таким чином, що кожен об'єкт може працювати як монітор). Наведений вище підхід працює добре, коли є лише одна спільна сутність та кілька потоків (1: N). N об'ємних утворень-M ниток Тепер подумайте про ситуацію, коли всередині умивальника є два умивальника і лише одна двері. Якщо ми використовуємо попередній підхід, тільки p1 може використовувати один умивальник одночасно, тоді як p2 буде чекати зовні. Це витрата ресурсів, оскільки ніхто не використовує B2 (умивальник). Мудрішим підходом було б створити меншу кімнату всередині умивальної кімнати та забезпечити їм по одній двері на умивальник. Таким чином, P1 може отримати доступ до B1, а P2 може отримати доступ до B2 і навпаки.
введіть тут опис зображення

washbasin1;  
washbasin2;

Object lock1=new Object();
Object lock2=new Object();

  synchronized(lock1)
  {
    washbasin1;
  }

  synchronized(lock2)
  {
    washbasin2;
  }

введіть тут опис зображення
введіть тут опис зображення

Дивіться більше на теми ----> тут


11

Здається, що в таборах C # та Java щодо цього є різна консенсус. Більшість кодів Java, які я бачив, використовує:

// apply mutex to this instance
synchronized(this) {
    // do work here
}

тоді як більшість кодів C # вибирає, напевно, безпечніше

// instance level lock object
private readonly object _syncObj = new object();

...

// apply mutex to private instance level field (a System.Object usually)
lock(_syncObj)
{
    // do work here
}

Ідіома C #, безумовно, безпечніша. Як було сказано раніше, зловмисний / випадковий доступ до блокування не може бути зроблений ззовні екземпляра. Код Java також є таким ризиком, але, схоже, спільнота Java з часом тяжіла до трохи менш безпечної, але трохи більш короткої версії.

Це не означає як копати проти Java, а лише відображення мого досвіду роботи на обох мовах.


3
Можливо, оскільки C # є молодшою ​​мовою, вони навчилися з поганих моделей, які з'ясовувались у таборі Java та кодували такі речі краще? Чи є також менше одинаків? :)
Білл К

3
Він він. Цілком можливо, правда, але я не збираюся підніматися до приманки! Я думаю, що я можу сказати напевно, це те, що в C # коді є більше великих літер;)
serg10,

1
Просто неправда (якщо сказати красиво)
tcurdt

7

java.util.concurrentПакет значно зменшити складність моєї потокобезпечна коди. У мене є лише анекдотичні докази, але більшість робіт, з якими я бачив, synchronized(x)повторно реалізують блокування, семафор або засувку, але використовують монітори нижнього рівня.

Зважаючи на це, синхронізація за допомогою будь-якого з цих механізмів є аналогом синхронізації на внутрішньому об'єкті, а не протікання замка. Це вигідно тим, що ви маєте абсолютну впевненість у тому, що ви контролюєте вхід на монітор двома або більше потоками.


6
  1. Зробіть ваші дані незмінними, якщо це можливо ( finalзмінні)
  2. Якщо ви не можете уникнути мутації спільних даних у кількох потоках, використовуйте конструкції програмування високого рівня [наприклад, зернистий LockAPI]

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

Приклад коду для використання інтерфейсу, ReentrantLockякий реалізуєLock

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

Переваги блокування над синхронізованим (це)

  1. Використання синхронізованих методів чи висловлювань змушує все придбання та вивільнення блокування відбуватися структурованим блоком.

  2. Реалізації блокування забезпечують додаткову функціональність щодо використання синхронізованих методів та висловлювань шляхом надання

    1. Спроба блокування придбання блокування ( tryLock())
    2. Спроба придбати замок, який можна перервати ( lockInterruptibly())
    3. Спроба придбати замок, який може закінчити час ( tryLock(long, TimeUnit)).
  3. Клас Lock також може надати поведінку та семантику, що сильно відрізняється від клавіші неявного блокування монітора, наприклад

    1. гарантоване замовлення
    2. використання не вступника
    3. Виявлення тупикової ситуації

Погляньте на це питання SE щодо різних типів Locks :

Синхронізація проти блокування

Ви можете досягти безпеки потоку, використовуючи розширений API паралельності замість синхронізованих блоків. Ця сторінка документації містить хороші конструктивні програми для досягнення безпеки потоку.

Блокування об'єктів блокування підтримують ідіоми блокування, які спрощують багато паралельних програм.

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

Одночасні колекції полегшують управління великими колекціями даних і можуть значно зменшити потребу в синхронізації.

Атомні змінні мають функції, що мінімізують синхронізацію та допомагають уникнути помилок узгодженості пам'яті.

ThreadLocalRandom (в JDK 7) забезпечує ефективне генерування псевдовипадкових чисел з декількох потоків.

Зверніться до пакетів java.util.concurrent і java.util.concurrent.atomic також для інших програм програмування.


5

Якщо ви вирішили це:

  • що вам потрібно зробити, це заблокувати поточний об’єкт; і
  • ви хочете заблокувати його деталізацією, меншою, ніж цілий метод;

то я не бачу табу над синхронізацією (це).

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

Що стосується питання, який замок об'єкта ви повинні використовувати, я думаю, що немає нічого поганого в синхронізації на поточному об'єкті, якщо цього можна було б очікувати за логікою того, що ви робите, і як зазвичай використовується ваш клас . Наприклад, із колекцією об'єкт, який ви логічно очікували б заблокувати, - це, як правило, сама колекція.


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

4

Я думаю, що є хороше пояснення того, чому кожна з цих життєво важливих прийомів під вашим поясом у книзі під назвою Брайан Гетц під назвою Java Concurrency In Practice. Він робить один момент дуже зрозумілим - ви повинні використовувати той самий замок "ВСЕГО", щоб захистити стан свого об'єкта. Синхронізований метод та синхронізація на об'єкті часто йдуть рука об руку. Наприклад, вектор синхронізує всі його методи. Якщо ви маєте ручку з векторним об’єктом і збираєтесь робити "ставити, якщо немає", то просто векторна синхронізація власних індивідуальних методів не захистить вас від корупції держави. Вам потрібно синхронізуватися за допомогою синхронізованих (vectorHandle). Це призведе до того, що замок SAME отримає кожен потік, який має ручку до вектора і захистить загальний стан вектора. Це називається блокування на стороні клієнта. Ми знаємо, що фактично вектор синхронізується (це) / синхронізує всі його методи, а отже, синхронізація на об'єкті векторного ручка призведе до належної синхронізації стану векторних об’єктів. Нерозумно вважати, що ти безпечний для ниток лише тому, що ти використовуєш безпечну колекцію. Саме тому ConcurrentHashMap явно ввів метод putIfAbsent - робити такі операції атомарними.

Підводячи підсумок

  1. Синхронізація на рівні методу дозволяє блокувати сторону клієнта.
  2. Якщо у вас є приватний об'єкт блокування, це робить неможливим блокування на стороні клієнта. Це добре, якщо ви знаєте, що у вашому класі немає функціональних можливостей "ставити, якщо немає".
  3. Якщо ви проектуєте бібліотеку, то синхронізація цього методу або синхронізація методу часто мудріша. Тому що ви рідко в змозі вирішити, яким чином ваш клас буде використовуватися.
  4. Якби вектор використовував приватний об'єкт блокування - його було неможливо отримати правильно, якщо він відсутній. Клієнтський код ніколи не отримає ручку з приватним замком, порушивши тим самим основне правило використання ТОЖОГО САМОГО ЗАМОКУ для захисту свого стану.
  5. Синхронізація цього чи синхронізованих методів має проблеми, як вказували інші - хтось може отримати замок і ніколи не звільняти його. Усі інші потоки очікували б звільнення замка.
  6. Тож знайте, що ви робите, і прийміть правильне.
  7. Хтось стверджував, що наявність об'єкта приватного блокування забезпечує кращу деталізацію - наприклад, якщо дві операції не пов'язані - вони можуть бути захищені різними замками, що призводить до кращої пропускної здатності. Але це, на мій погляд, дизайнерський запах, а не кодовий запах - якщо дві операції абсолютно не пов'язані, чому вони входять до класу SAME? Чому класний клуб взагалі повинен взаємно функціонувати? Може бути клас корисності? Хмммм - якась утиліта, що забезпечує обробку рядків та форматування дати календаря через той самий екземпляр ?? ... не має для мене сенсу принаймні !!

3

Ні, ви не повинні завжди . Однак я схильний уникати цього, коли на певному об'єкті виникають численні занепокоєння, які потребують лише безпечних потоків стосовно самих себе. Наприклад, у вас може бути об'єкт даних, що змінюється, з полями "label" та "parent"; вони повинні бути безпечними для потоків, але зміна одного не повинна блокувати інше від написання / читання. (На практиці я б уникну цього, оголосивши поля летючими та / або використовуючи обгортки AtomicFoo java.util.concurrent).

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

Я набагато скоріше мав би більш зернисті замки; навіть якщо ви хочете зупинити все від зміни (можливо, ви серіалізуєте об'єкт), ви можете просто придбати всі замки, щоб досягти того самого, плюс це явніше. Коли ви користуєтесь synchronized(this), не ясно, чому саме ви синхронізуєте, чи які побічні ефекти можуть бути. Якщо ви користуєтесь synchronized(labelMonitor), а ще краще labelLock.getWriteLock().lock(), зрозуміло, що ви робите, і чим обмежуються ефекти вашого критичного розділу.


3

Коротка відповідь : Ви повинні зрозуміти різницю та зробити вибір залежно від коду.

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


Чи правильно я вас розумію, коли ви говорите, це залежить від вашого досвіду?
eljenso

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

2

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

Коли вам потрібно просто зробити операції примітивного типу, щоб бути атомними, є доступні варіанти, як AtomicIntegerі подібні.

Але припустимо, у вас є два цілих числа, які пов'язані між собою, як xі yкоординати, які пов'язані один з одним і повинні бути змінені атомним чином. Тоді ви б захистили їх за допомогою одного і того ж замка.

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

class Point{
   private int x;
   private int y;

   public Point(int x, int y){
       this.x = x;
       this.y = y;
   }

   //mutating methods should be guarded by same lock
   public synchronized void changeCoordinates(int x, int y){
       this.x = x;
       this.y = y;
   }
}

У наведеному вище прикладі я маю лише один метод, який мутує як xі, так yі не два різні способи як xі yпов'язані між собою, і якби я дав два різні способи мутації xта yокремо, то це не було б безпечним для потоків.

Цей приклад є лише для того, щоб продемонструвати, а не обов’язково, як він повинен бути реалізований. Найкращий спосіб зробити це було б зробити це НЕПРАВНИМ .

Зараз в опозиції до Point прикладу, є приклад TwoCountersуже наданого @Andreas, коли стан, який захищається двома різними замками, оскільки стан не пов'язаний один з одним.

Процес використання різних замків для захисту споріднених станів називається Lock Striping або Lock Splitting


1

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

З точки зору читача, якщо ви бачите запитання про це , вам завжди потрібно відповісти на два запитання:

  1. який доступ захищений цим ?
  2. дійсно один замок, чи не хтось ввів помилку?

Приклад:

class BadObject {
    private Something mStuff;
    synchronized setStuff(Something stuff) {
        mStuff = stuff;
    }
    synchronized getStuff(Something stuff) {
        return mStuff;
    }
    private MyListener myListener = new MyListener() {
        public void onMyEvent(...) {
            setStuff(...);
        }
    }
    synchronized void longOperation(MyListener l) {
        ...
        l.onMyEvent(...);
        ...
    }
}

Якщо дві нитки починаються longOperation()на двох різних екземплярах BadObject, вони набувають своїх замків; коли прийшов час викликатиl.onMyEvent(...) , ми маємо тупик, оскільки жодна з ниток не може придбати замок іншого об'єкта.

У цьому прикладі ми можемо усунути тупик, використовуючи два блокування, один для коротких операцій та один для довгих.


2
Єдиний спосіб отримати тупик у цьому прикладі, коли BadObjectA посилається longOperationна B, передаючи точки A myListener, і навпаки. Не неможливо, але доволі суперечливо, підтримуючи мої попередні моменти.
eljenso

1

Як вже було сказано тут, синхронізований блок може використовувати визначену користувачем змінну як об'єкт блокування, коли синхронізована функція використовує лише "це". І звичайно, ви можете маніпулювати з областями своєї функції, які повинні бути синхронізовані тощо.

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

Більш детальне пояснення різниці ви можете знайти тут: http://www.artima.com/insidejvm/ed2/threadsynchP.html

Також використання синхронізованого блоку не добре через наступну точку зору:

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

Для детальнішої інформації в цій галузі я рекомендую вам прочитати цю статтю: http://java.dzone.com/articles/synchronized-considered


1

Це дійсно просто доповнює інші відповіді, але якщо ваше головне заперечення щодо використання приватних об'єктів для блокування полягає в тому, що він захаращує ваш клас полями, не пов'язаними з бізнес-логікою, тоді Project Lombok @Synchronizedповинен генерувати котельну табличку під час компіляції:

@Synchronized
public int foo() {
    return 0;
}

компілює до

private final Object $lock = new Object[0];

public int foo() {
    synchronized($lock) {
        return 0;
    }
}

0

Хороший приклад для використання синхронізованого (це).

// add listener
public final synchronized void addListener(IListener l) {listeners.add(l);}
// remove listener
public final synchronized void removeListener(IListener l) {listeners.remove(l);}
// routine that raise events
public void run() {
   // some code here...
   Set ls;
   synchronized(this) {
      ls = listeners.clone();
   }
   for (IListener l : ls) { l.processEvent(event); }
   // some code here...
}

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

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


Тут може бути використаний будь-який об'єкт як замок. Це не потрібно this. Це може бути приватне поле.
finnw

Правильно, але метою цього прикладу було показати, як робити належну синхронізацію, якщо ми вирішили використовувати метод синхронізації.
Барт Прокоп

0

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


0

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

// Synchronization objects (locks)
private static HashMap<String, Object> locks = new HashMap<String, Object>();
// Simple method
private static Object atomic() {
    StackTraceElement [] stack = Thread.currentThread().getStackTrace(); // get execution point 
    StackTraceElement exepoint = stack[2];
    // creates unique key from class name and line number using execution point
    String key = String.format("%s#%d", exepoint.getClassName(), exepoint.getLineNumber()); 
    Object lock = locks.get(key); // use old or create new lock
    if (lock == null) {
        lock = new Object();
        locks.put(key, lock);
    }
    return lock; // return reference to lock
}
// Synchronized code
void dosomething1() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 1
        ...
    }
    // other command
}
// Synchronized code
void dosomething2() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 2
        ...
    }
    // other command
}

0

Уникайте використання synchronized(this)механізму блокування: Це блокує весь екземпляр класу і може спричинити тупикові місця. У таких випадках рефактор коду блокується лише певним методом або змінною, таким чином весь клас не блокується. Synchronisedможе використовуватися всередині методу рівня.
Замість використання synchronized(this), нижче код показує, як ви могли просто заблокувати метод.

   public void foo() {
if(operation = null) {
    synchronized(foo) { 
if (operation == null) {
 // enter your code that this method has to handle...
          }
        }
      }
    }

0

Мої два центи в 2019 році, хоча це питання можна було вже вирішити.

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

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

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

Проблема для мене полягає в тому, що синхронізоване ключове слово в підписі визначення методу робить програмістам занадто просто, щоб не замислюватися над тим, що заблокувати, на що може бути важлива річ, про яку слід думати, якщо ви не хочете стикатися з проблемами в мульти -потокова програма.

Не можна стверджувати, що "типово" ви не хочете, щоб користувачі вашого класу мали змогу робити ці речі або "типово", які ви хочете ... Це залежить від того, яку функціональність ви кодуєте. Ви не можете скласти правило великого пальця, оскільки не можете передбачити всі випадки використання.

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

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

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

Примітка 2: Я не погоджуюся з аргументом, що якщо якийсь код випадково викраде ваш замок, його помилка, і вам доведеться її вирішити. Це певним чином аргумент, що я можу оприлюднити всі свої методи, навіть якщо вони не мають бути публічними. Якщо хтось «випадково» називає мій призначений приватним методом, його помилка. Навіщо вмикати цю аварію в першу чергу !!! Якщо можливість красти ваш замок є проблемою для вашого класу, не дозволяйте цього. Так просто.


-3

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

Це не лито з каменю, це здебільшого питання належної практики та запобігання помилкам.

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