Яка різниця між атомною / летючою / синхронізованою?


297

Як атомні / летючі / синхронізовані працюють всередині?

Яка різниця між наступними кодовими блоками?

Код 1

private int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

Код 2

private AtomicInteger counter;

public int getNextUniqueIndex() {
    return counter.getAndIncrement();
}

Код 3

private volatile int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

Чи volatileпрацює таким чином? Є

volatile int i = 0;
void incIBy5() {
    i += 5;
}

дорівнює

Integer i = 5;
void incIBy5() {
    int temp;
    synchronized(i) { temp = i }
    synchronized(i) { i = temp + 5 }
}

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

І яка різниця між внутрішнім читанням і записом до мінливих змінних / атомних змінних? Я читав в якійсь статті, що в потоці є локальна копія змінних - що це?


5
Це викликає безліч питань, з кодом, який навіть не складається. Можливо, вам варто прочитати хорошу книгу, як-от Java Concurrency in Practice.
JB Nizet

4
@JBNizet ви праві !!! У мене є ця книга, вона не має атомної концепції коротко, і я не отримую деяких концепцій цього. прокляття, це моя помилка не автора.
hardik

4
Вам не потрібно байдуже, як це реалізовано (і це залежить від ОС). Що ви повинні розуміти - це договір: значення збільшується атомно, а всі інші потоки гарантовано бачать нове значення.
JB Nizet

Відповіді:


392

Ви конкретно запитуєте, як вони працюють всередині , тож ось ви:

Немає синхронізації

private int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

В основному воно зчитує значення з пам'яті, збільшує його і повертає в пам'ять. Це працює в одному потоці, але сьогодні, в епоху багатоядерних, багатоядерних процесорів, багаторівневих кешів, він не працюватиме правильно. Перш за все, це вводить стан перегонів (декілька потоків можуть читати значення одночасно), але також проблеми із видимістю. Значення може зберігатися лише в " локальній " пам'яті процесора (деякий кеш) і не бути видимим для інших процесорів / ядер (і, таким чином, - потоків). Ось чому багато хто посилається на локальну копію змінної в потоці. Це дуже небезпечно. Розглянемо цей популярний, але зламаний код для зупинки потоку:

private boolean stopped;

public void run() {
    while(!stopped) {
        //do some work
    }
}

public void pleaseStop() {
    stopped = true;
}

Додати volatileдо stoppedзмінної, і це працює добре - якщо будь-який інший потік змінює stoppedзмінну за допомогою pleaseStop()методу, ви гарантовано побачите цю зміну негайно в while(!stopped)циклі робочої нитки . BTW - це не гарний спосіб переривання потоку, див. Як зупинити потік, який працює вічно, без будь-якого використання та Зупинення конкретного потоку Java .

AtomicInteger

private AtomicInteger counter = new AtomicInteger();

public int getNextUniqueIndex() {
  return counter.getAndIncrement();
}

AtomicIntegerВикористовує клас CAS ( порівняння і заміни ) низькорівневих операцій ЦП (не потрібно ніякої синхронізації!) Вони дозволяють змінювати певну змінну , тільки якщо поточне значення одно що - то інше (і повертається успішно). Отже, коли ви виконуєте, getAndIncrement()він фактично працює в циклі (спрощена реальна реалізація):

int current;
do {
  current = get();
} while(!compareAndSet(current, current + 1));

Так в основному: читати; намагайтеся зберігати збільшене значення; якщо не вдалося (значення більше не дорівнює current), прочитайте та повторіть спробу. compareAndSet()Реалізуються в машинному коді (збірка).

volatile без синхронізації

private volatile int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

Цей код невірний. Він вирішує проблему з видимістю ( volatileгарантує, що інші потоки можуть бачити зміни, внесені до counter), але все ж має умову гонки. Це було пояснено неодноразово: до / після наростання не є атомним.

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

volatile без синхронізації (2)

volatile int i = 0;
void incIBy5() {
  i += 5;
}

Та сама проблема, що і вище, але ще гірша, тому що iні private. Стан гонки все ще присутній. Чому це проблема? Якщо, скажімо, два потоки запускають цей код одночасно, вихід може бути + 5або + 10. Однак ви гарантовано побачите зміни.

Кілька незалежних synchronized

void incIBy5() {
  int temp;
  synchronized(i) { temp = i }
  synchronized(i) { i = temp + 5 }
}

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

synchronized(new Object()) {
  //thread-safe, SRSLy?
}

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

Навіть якщо ви використовували остаточну змінну (або this) для синхронізації, код все одно є неправильним. Дві нитки можуть спочатку прочитати , iщоб tempсинхронно (мають однакове значення локально в temp), то перше привласнює нового значення i(скажімо, від 1 до 6) , а інший робить те ж саме (від 1 до 6).

Синхронізація повинна тривати від зчитування до присвоєння значення. Ваша перша синхронізація не впливає (читання intатома), а друга. На мою думку, це правильні форми:

void synchronized incIBy5() {
  i += 5 
}

void incIBy5() {
  synchronized(this) {
    i += 5 
  }
}

void incIBy5() {
  synchronized(this) {
    int temp = i;
    i = temp + 5;
  }
}

10
Єдине, що я хотів би додати, це те, що JVM копіює змінні значення в регістри для роботи над ними. Це означає, що потоки, що працюють на одному процесорі / ядрі, все ще можуть бачити різні значення енергонезалежної змінної.
Девід Харкнесс

@thomasz: це порівнянняAndSet (поточне, струм + 1) синхронізоване ?? якщо ні, ніж те, що відбувається, коли два потоки виконують цей метод одночасно ??
hardik

@Hardik: compareAndSetце лише тонка обгортка навколо операції CAS. Я вникаю у деякі деталі у своїй відповіді.
Томаш Нуркевич

1
@thomsasz: гаразд, я переглядаю це запитання про посилання і на це відповів jon skeet, він каже, що "нитка не може прочитати мінливу змінну, не перевіряючи, чи не виконав запис будь-який інший потік". але що станеться, якщо один потік знаходиться між операцією написання, а друга нитка читає його !! я помиляюся ?? чи не це гонка на атомній роботі ??
hardik

3
@Hardik: створіть інше запитання, щоб отримати більше відповідей на те, що ви запитуєте, ось тільки ви і я, і коментарі не підходять для запитання. Не забудьте опублікувати посилання на нове запитання тут, щоб я міг продовжити.
Томаш Нуркевич

61

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

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

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

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

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

Додаток (квітень 2016 р.)

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

Додаток (липень 2016 р.)

Синхронізація відбувається на об’єкті . Це означає, що виклик синхронізованого методу класу заблокує thisоб'єкт виклику. Статичні синхронізовані методи заблокують сам Classоб’єкт.

Аналогічно, введення синхронізованого блоку вимагає блокування thisоб'єкта методу.

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


25

непостійний:

volatile- ключове слово. volatileзмушує всі потоки отримувати останнє значення змінної з основної пам'яті замість кешу. Не потрібне блокування для доступу до змінних змінних. Усі потоки можуть одночасно отримувати значення змінних змінних.

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

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

Коли використовувати: Один потік змінює дані, а інші потоки повинні читати останнє значення даних. Інші потоки вживатимуть певних дій, але вони не оновлюватимуть дані .

AtomicXXX:

AtomicXXXкласи підтримують безпечне програмування без заблокування потоку на окремих змінних. Ці AtomicXXXкласи (як AtomicInteger) вирішує помилки невідповідності пам’яті / побічні ефекти модифікації мінливих змінних, до яких можна отримати доступ у декількох потоках.

Коли користуватися: Кілька потоків можуть читати та змінювати дані.

синхронізовано:

synchronizedце ключове слово, яке використовується для захисту методу або блоку коду. Здійснення методу як синхронізованого має два ефекти:

  1. По-перше, неможливо переплутати два виклики synchronizedметодів на одному об’єкті. Коли одна нитка виконує synchronizedметод для об'єкта, всі інші потоки, які викликають synchronizedметоди для одного блоку об'єктів (призупинення виконання), поки перший потік не буде виконано з об'єктом.

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

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

AtomicXXXеквівалентно, volatile + synchronizedнавіть якщо реалізація інша. AmtomicXXXрозширює volatileзмінні + compareAndSetметоди, але не використовує синхронізацію.

Питання, пов'язані з ДП:

Різниця між летючими та синхронізованими в Java

Летючий бул проти AtomicBoolean

Гарні статті для читання: (Вище вмісту взято на цих сторінках документації)

https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html

https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html


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

5

Я знаю, що два потоки не можуть входити в блок синхронізації одночасно

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

private Integer i = 0;

synchronized(i) {
   i++;
}

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

якщо це правда, ніж як цей atomic.incrementAndGet () працює без синхронізації ?? і чи безпечна нитка ??

так. Він не використовує блокування для досягнення безпеки нитки.

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

І яка різниця між внутрішнім читанням і записом до змінної змінної / атомної змінної ??

Атомний клас використовує летючі поля. Різниці в полі немає. Різниця - виконані операції. Класи Atomic використовують операції CompareAndSwap або CAS.

Я читав в якійсь статті, що в потоці є локальна копія змінних, що це?

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

Це лише проблема, коли пам'ять спільно використовується принаймні одним потоком, який оновлює її.


@Aniket Thakur Ви впевнені в цьому? Ціле число незмінне. Тож i ++, ймовірно, автоматично розблокує значення int, збільшить його, а потім створить новий Integer, який не є тим самим екземпляром, як раніше. Спробуйте зробити я остаточним, і ви отримаєте помилки компілятора під час виклику i ++.
fuemf5

2

Синхронізований Vs Atomic Vs летючий:

  • Летючі та атомні застосовуються лише для змінної, тоді як синхронізована застосовується за методом.
  • Летючі забезпечують видимість, а не атомність / консистенцію об'єкта, тоді як інші забезпечують видимість та атомність.
  • Зберігають змінну змінну в оперативній пам’яті, і вона швидше отримує доступ, але ми не можемо досягти безпеки теми або синхронізації без синхронізованого ключового слова.
  • Синхронізований реалізований як синхронізований блок або синхронізований метод, в той час як і те й інше. За допомогою синхронізованого ключового слова ми можемо набрати безпечний кілька рядків коду, тоді як з обома ми не можемо досягти того ж.
  • Синхронізований може блокувати той самий об’єкт класу чи інший об’єкт класу, тоді як обидва не можуть.

Будь ласка, виправте мене, якщо щось я пропустив.


1

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

Скажіть, наприклад: летючий int i = 2; i ++, що є не що інше, як i = i + 1; що робить i як значення 3 в пам'яті після виконання цього оператора. Сюди входить зчитування наявного значення з пам'яті для i (що становить 2), завантаження в регістр акумулятора процесора і виконання обчислення шляхом збільшення наявного значення на одне (2 + 1 = 3 в акумуляторі), а потім записування назад цього збільшення значення повернутися до пам'яті. Ці операції недостатньо атомні, хоча значення i є мінливим. Я мінливий гарантує лише те, що ЄДИННЕ читання / запис з пам'яті є атомним, а не МНОГО. Отже, нам потрібно синхронізувати також навколо i ++, щоб це не було дурманним атомним твердженням. Запам’ятайте той факт, що заява включає в себе кілька тверджень.

Сподіваюсь, пояснення досить зрозуміле.


1

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

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

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