Чи використовуються статичні змінні між потоками?


95

Мій викладач у класі Java на верхньому рівні з потокових технологій сказав щось, у чому я не був впевнений.

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

По суті, він сказав, що можливо, що readyоновлено в основному потоці, але НЕ в ReaderThread, тому це ReaderThreadбуде нескінченно циклічно.

Він також стверджував, що програму можна надрукувати 0або 42. Я розумію, як 42можна друкувати, але ні 0. Він зазначив, що це буде у випадку, коли для numberзмінної встановлено значення за замовчуванням.

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

Він показав цей код:

public class NoVisibility {  
    private static boolean ready;  
    private static int number;  
    private static class ReaderThread extends Thread {   
        public void run() {  
            while (!ready)   Thread.yield();  
            System.out.println(number);  
        }  
    }  
    public static void main(String[] args) {  
        new ReaderThread().start();  
        number = 42;  
        ready = true;  
    }  
}

Видимість нелокальних змінних не залежить від того, чи є вони статичними змінними, полями об'єктів або елементами масиву, всі вони мають однакові міркування. (З проблемою, через яку елементи масиву не можна зробити мінливими.)
Paŭlo Ebermann

1
запитайте свого вчителя, яку архітектуру він вважає можливим побачити «0». Проте, теоретично він правий.
bestsss

4
@bestsss Задаючи таке запитання, вчитель виявить, що він пропустив всю суть того, що говорив. Справа в тому, що компетентні програмісти розуміють, що гарантоване, а що ні, і не покладаються на те, що не гарантується, принаймні не без розуміння, що саме не гарантується і чому.
Девід Шварц

Вони розподіляються між усім, що завантажується одним завантажувачем класу. Включаючи нитки.
Маркіз Лорнський

Ваш учитель (і прийнята відповідь) на 100% праві, але я згадаю, що це трапляється рідко - це така проблема, яка буде ховатися роками і проявляти себе лише тоді, коли це буде найшкідливішим. Навіть короткі тести, які намагаються викрити проблему, схильні діяти так, ніби все добре (можливо, тому, що у них немає часу, щоб JVM зробив багато оптимізації), тому це дійсно хороша проблема, про яку слід знати.
Білл К

Відповіді:


75

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

Існує проблема видимості, нав'язана моделлю пам'яті JVM. Ось стаття, яка розповідає про модель пам’яті та про те, як записи стають видимими для потоків . Ви не можете розраховувати на зміни, які один потік робить видимими для інших потоків вчасно (насправді JVM не зобов’язаний робити ці зміни видимими для вас у будь-який часовий проміжок), якщо ви не встановите взаємозв’язок „до-до”. .

Ось цитата з цього посилання (надана в коментарі Джеда Веслі-Сміта):

Розділ 17 Специфікації мови Java визначає відношення "відбувається до" на операціях пам'яті, таких як читання та запис спільних змінних. Результати запису одним потоком гарантовано будуть видимими для читання іншим потоком, лише якщо операція запису відбувається - до операції читання. Синхронізовані та мінливі конструкції, а також методи Thread.start () та Thread.join () можуть формувати взаємозв'язки, що відбуваються раніше. Зокрема:

  • Кожна дія в потоці відбувається - до кожної дії в цьому потоці, яка надходить пізніше в порядку програми.

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

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

  • Виклик для запуску потоку відбувається - до будь-якої дії у запущеному потоці.

  • Усі дії в потоці відбуваються до того, як будь-який інший потік успішно повернеться із об’єднання в цьому потоці.


3
На практиці "вчасно" та "коли-небудь" є синонімами. Дуже можливо, щоб вищезгаданий код ніколи не припинявся.
ДЕРЕВО

4
Крім того, це демонструє ще один анти-шаблон. Не використовуйте летючі речовини, щоб захистити більше, ніж одну частину спільного стану. Тут число і готові - це два стани, і щоб послідовно їх оновлювати / читати, вам потрібна фактична синхронізація.
ДЕРЕВО

5
Частина про те, що врешті-решт стане помітною, є неправильною. Без будь-яких явних відносин, що трапляються раніше, не існує гарантії, що будь-який запис коли-небудь побачить інший потік, оскільки JIT цілком в межах своїх прав вводити читання в реєстр, і тоді ви ніколи не побачите жодного оновлення. Будь-яке навантаження - це удача, і на нього не слід покладатися.
Джед Веслі-Сміт

2
"якщо ви не використовуєте летюче ключове слово або не синхронізуєте." слід прочитати "якщо не буде відповідних стосунків між автором та читачем" і це посилання: download.oracle.com/javase/6/docs/api/java/util/concurrent/…
Джед Веслі-Сміт

2
@bestsss добре помічений. На жаль, ThreadGroup порушена у багатьох відношеннях.
Джед Веслі-Сміт

37

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

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

У цій статті представлено погляд, який відповідає тому, як він подав інформацію:

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

  • Кожен потік у Java відбувається в окремому просторі пам'яті (це явно не відповідає дійсності, тому будьте готові до цього).

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

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

...

модель нитки

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


12

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

Тому Java визначає модель пам'яті , яка визначає, за яких обставин потоки можуть бачити постійний стан спільних даних.

У вашому конкретному випадку додавання volatileгарантує видимість.


8

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

І теоретично, записи, зроблені іншим потоком, можуть виглядати в іншому порядку, якщо тільки змінні не оголошені volatileабо записи явно не синхронізовані.


4

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


2

При ініціалізації змінної статичного примітивного типу java за замовчуванням призначає значення статичним змінним

public static int i ;

коли ви визначаєте змінну таким чином, значення за замовчуванням i = 0; ось чому існує можливість отримати вам 0. тоді основний потік оновлює значення boolean готового до true. оскільки ready - це статична змінна, основний потік та інший потік посилаються на ту саму адресу пам'яті, тому змінна готової змінюється. так вторинний потік виходить із циклу while і значення друку. під час друку значення ініціалізованого значення числа дорівнює 0. якщо процес потоку пройшов цикл while перед змінною номера основного потоку оновлення. тоді є можливість надрукувати 0


-2

@dontocsata ти можеш повернутися до свого вчителя і навчити його трохи :)

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

Наступні 2 змінні будуть розміщені в одному рядку кешу практично під будь-якою відомою архітектурою.

private static boolean ready;  
private static int number;  

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

Запущений потік ReaderThreadбуде підтримувати процес живим, оскільки він не є демоном! Таким чином, readyі numberбуде змито (або номер раніше, якщо відбудеться перемикання контексту), і справжньої причини для переупорядкування в цьому випадку немає, принаймні я навіть не можу придумати жодного. Вам знадобиться щось справді дивне, щоб побачити щось, крім 42. Знову ж таки, я припускаю, що обидві статичні змінні будуть в одному рядку кешу. Я просто не можу уявити лінію кешу довжиною 4 байти АБО JVM, яка не буде їх призначати у безперервній області (лінія кешу).


3
@bestsss, хоча сьогодні це все правда, він покладається на поточну реалізацію JVM та апаратну архітектуру за правдивість, а не на семантику програми. Це означає, що програма все ще не працює, хоча вона може працювати. Легко знайти тривіальний варіант цього прикладу, який насправді зазнає невдачі зазначеними способами.
Джед Уеслі-Сміт

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

6
Можливо, найгірша порада щодо написання безпечного для потоку коду, який я бачив.
Лоуренс Дол

4
@Bestsss: Проста відповідь на ваше запитання полягає в наступному: "Код до специфікації та документа, а не до побічних ефектів вашої конкретної системи чи реалізації". Це особливо важливо для платформи віртуальних машин, розробленої для агностики базового обладнання.
Lawrence Dol

1
@Bestsss: Викладачі говорять про те, що (а) код цілком може працювати, коли ви його тестуєте, і (б) код зламаний, оскільки це залежить від роботи апаратного забезпечення, а не від гарантій специфікації. Справа в тому, що, схоже, це нормально, але це не нормально.
Lawrence Dol
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.