Чи id = 1 - id атомний?


74

Зі сторінки 291 іспитів із практики програмістів OCP Java SE 6, питання 25:

public class Stone implements Runnable {
    static int id = 1;

    public void run() {
        id = 1 - id;
        if (id == 0) 
            pick(); 
        else 
            release();
    }

    private static synchronized void pick() {
        System.out.print("P ");
        System.out.print("Q ");
    }

    private synchronized void release() {
        System.out.print("R ");
        System.out.print("S ");
    }

    public static void main(String[] args) {
        Stone st = new Stone();
        new Thread(st).start();
        new Thread(st).start();
    }
}

Одна з відповідей:

Вихід може бути P Q P Q

Я позначив цю відповідь як правильну. Мої міркування:

  1. Ми починаємо дві нитки.
  2. Перший входить run().
  3. Відповідно до JLS 15.26.1 , це спочатку оцінює 1 - id. Результат є 0. Він зберігається в стеку нитки. Ми ось-ось збережемо це 0в статичному id, але ...
  4. Бум, планувальник вибирає другий потік для запуску.
  5. Отже, входить друга нитка run(). Статичний idнерухомий 1, тому він виконує метод pick(). P Qдрукується.
  6. Планувальник вибирає перший потік для запуску. Він бере 0з його стека і зберігає до статичного id. Отже, перший потік також виконується pick()і друкується P Q.

Однак у книзі написано, що ця відповідь неправильна:

Це неправильно, оскільки рядок id = 1 - idміняє значення idміж 0і 1. Немає шансів, щоб один і той же метод був виконаний двічі.

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


Вони, до речі, дозволили RSRS?
Джон Скіт,

1
@JonSkeet Не було такої відповіді. Вони дозволили P Q R S, P R S Qі P R Q S, до якої я згоден.
Адам Стельмащик

Думаю, ви взяли розділ JLS, на який ви посилаєтесь, поза контекстом. Цей розділ розглядає прості завдання (як в одному потоці). Я думаю, вам потрібно переглянути JLS 17.4. Модель пам'яті .
hfontanez

1
Звичайно P R S Qі P R Q Sнеможливі також, оскільки pickі releaseсинхронізовані. Мені чогось не вистачає (моя Java, мабуть, трохи іржава)?
matt

2
У оригінальному прикладі коду (із згаданої книги) releaseметод не є статичним. Тож P R S Qі P R Q S є можливими рішеннями насправді. Тим не менше, це не виправляє умови перегонів у runметоді, отже, книга все ще помиляється щодо цього питання.
isnot2bad

Відповіді:


78

Я помиляюся?

Ні, ви абсолютно праві - як і ваш приклад часової шкали.

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

Дещо бентежить, якщо довідковий матеріал, подібний до цього, є неправильним :(


1
Дякую, але що ви маєте на увазі під тим, що не гарантовано, що запис у idбудь-якому випадку буде сприйнятий іншим потоком ? Що це можна якось оптимізувати, і не може бути запису idв другий потік? Я не розумію цієї частини.
Адам Стельмащик

9
@AdamStelmaszczyk: Ні, я маю на увазі, що потік 1 міг записати нове значення id, але потік 2 може не побачити його відразу - він міг побачити старе значення.
Джон Скіт,

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

6
Існує ціла колекція інструментів для роботи з когерентністю кеш-пам’яті, починаючи від явних команд змиву кеш-пам’яті, атомів і закінчуючи синхронізацією. На щастя для тих, хто ніколи і ніколи не захоче мати справу з цими драконами, синхронізація з synchronizedабо mutexes зазвичай робить "те, що ти вважаєш за необхідне робити", якщо ти використовуєш їх для захисту своїх даних за допомогою стандартних шаблонів (як synchronizedу прикладі вище). Це просто ганьба, що вони помилилися id.
Корт Аммон

9
Варто зазначити, що автор навіть визнав, що в запитанні може бути щось не так (оскільки воно є), хоча, мабуть, вони ніколи не потрудились скласти список помилок.
Тім Стоун

-3

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

Однак для того, щоб змінити значення id кожного потоку:

  1. робить локальну копію значення, що зберігається в адресі пам'яті ідентифікатора, до реєстру ЦП;
  2. виконує операцію 1 - id. Строго кажучи, тут виконують дві операції (-id and +1);
  3. переміщує результат назад до місця пам'яті idна купі.

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

Крім того, аналіз коду може показати, що в будь-який момент часу ідентифікатор може бути лише 0 або 1.

Доказ:

  • Початкове значення id = 1; Один потік змінить його на 0 ( id = 1 - id). А інший потік поверне його до 1.

  • Початкове значення id = 0; Один потік змінить його на 1 ( id = 1 - id). А інший потік поверне його до 0.

Отже, стан значення id є дискретним або 0, або 1.

Кінець доказу.

Цей код може мати дві можливості:

  • Можливість 1. Перший потік отримує перший доступ до змінної id. Тоді значення id ( id = 1 - idзмінюється на 0. Після цього pick ()буде виконуватися лише метод , друк P Q. Потік два, буде оцінювати id на той момент id = 0; метод release()буде виконаний друком R S. В результаті P Q R Sбуде надруковано.

  • Можливість 2. Спочатку потоком два доступу до змінної id. Тоді значення id ( id = 1 - idзмінюється на 0. Після цього pick ()буде виконуватися лише метод , друк P Q. Потік один, буде оцінювати id в той час id = 0; метод release()буде виконаний друком R S. В результаті P Q R Sбуде надруковано.

Інших можливостей немає. Однак слід зазначити, що варіанти, P Q R Sтакі як P R Q Sабо R P Q Sтощо, можуть бути надруковані через pick()статичний метод і, отже, спільно використовуються між двома потоками. Це призводить до одночасного виконання цього методу, що може призвести до друку літер в іншому порядку залежно від вашої платформи.

Однак у будь-якому випадку ніколи метод pick()ні release ()виконується двічі, оскільки вони взаємовиключні . Тому P Q P Qне буде результатом.


2
На вашому кроці 2 результат 1 - d(нуль) буде збережений у стеку потоку. Тепер перед кроком 3 планувальник може змінити запущений потік. Отже, другий потік також матиме нуль від 1 - d. Будь ласка, уважно вивчіть сценарій, який я представив у питанні. Ось чому P Q P Qце можливо. Якщо вас все ще не переконує теоретичний аналіз, вже представлений на цій сторінці, також загляньте до останнього допису тут , де емпірично показано, що це можливо.
Адам Стельмащик

Ваш "доказ" опускає два випадки - при початковому значенні 1 обидва потоки можуть спробувати змінити його на 0; і з початковим значенням 0 обидва потоки можуть спробувати змінити його на 1. Ця відповідь неправильна.
Dawood ibn Kareem
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.