Найпростіший і зрозумілий приклад мінливого ключового слова в Java


75

Я читаю про мінливе ключове слово на Java і повністю розумію його теоретичну частину.

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

Нижче фрагмент коду не працює належним чином (взято звідси ):

class Test extends Thread {

    boolean keepRunning = true;

    public void run() {
        while (keepRunning) {
        }

        System.out.println("Thread terminated.");
    }

    public static void main(String[] args) throws InterruptedException {
        Test t = new Test();
        t.start();
        Thread.sleep(1000);
        t.keepRunning = false;
        System.out.println("keepRunning set to false.");
    }
}

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

У мене є два основних запитання:

  • Хтось може пояснити нестабільність на прикладі? Не з теорією від JLS.
  • Чи нестійкий замінник синхронізації? Чи досягається це атомність?

Минулий пост про це широко розповідає stackoverflow.com/questions/7212155/java-threading-volatile
AurA

4
Ви думаєте назад. В ідеалі, якщо keepRunning не був мінливим, потік повинен продовжувати працювати необмежено довго . Насправді все навпаки: додавання volatile гарантує, що зміна поля буде видно. Без ключового слова просто немає ніяких гарантій, все може статися; ви не можете вказати, що потік повинен продовжувати працювати [...] .
Бруно Рейс

3
Ось у чому річ: помилки видимості пам’яті за своєю природою важко (неможливо?) Продемонструвати на простому прикладі, який буде невдалим кожного разу. Якщо припустити, що у вас багатоядерна машина, ваш приклад, ймовірно, зазнає невдачі принаймні пару разів, якщо ви багато запускаєте її (скажімо, 1000 запусків). Якщо у вас велика програма - така, що, наприклад, вся програма та її об'єкти не вміщуються в кеші процесора, - це збільшує ймовірність побачити помилку. В основному, помилки одночасності такі, що якщо теорія каже, що вона може зламатися, це, мабуть, буде, але лише раз на кілька місяців, і, можливо, у виробництві.
yshavit

Існує хороший приклад вже перерахований stackoverflow.com/questions/5816790 / ...
gurubelli

Ось приклад із записом vanillajava.blogspot.co.uk/2012/01/…
Пітер Лорі

Відповіді:


51

Нестійкий -> Гарантує видимість, а НЕ атомність

Синхронізація (блокування) -> Гарантує видимість та атомність (якщо це зроблено належним чином)

Нестійкість не замінює синхронізацію

Використовуйте volatile лише тоді, коли ви оновлюєте посилання і не виконуєте з ним інших операцій.

Приклад:

volatile int i = 0;

public void incrementI(){
   i++;
}

не буде безпечним для потоку без використання синхронізації або AtomicInteger, оскільки збільшення - це складна операція.

Чому програма не працює необмежено довго?

Ну, це залежить від різних обставин. У більшості випадків JVM досить розумна, щоб очистити вміст.

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

Також:

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


4
Це неправильно. летючі гарантує атомну природу. У документації Oracle це чітко зазначено. Див. Docs.oracle.com/javase/tutorial/essential/concurrency/… .
Калпа Гунаратна

4
У Java, коли ми маємо декілька потоків, кожен потік має власний стек (простір пам'яті), і кожен потік ініціює свою копію змінних, до яких він може отримати доступ. Якщо мінливого ключового слова немає, щоб прикрасити int i, кожен потік може використовувати його під час виконання. При оголошенні volatile кожен потік повинен зчитувати / записувати значення i з / в безпосередньо основну пам'ять, а не в / з локальних копій. Отже, у перспективі кожного потоку операції до / із змінною i є атомними.
Кальпа Гунаратна

atomicityчастина відповіді бентежить. Синхронізація дає вам взаємний ексклюзивний доступ та видимість . volatileдає лише видимість . Також volatileробить читання / запис longі doubleатомарним (синхронізація робить це теж за своєю взаємовиключною природою).
IlyaEremin

27

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

Загальне пояснення щодо летких змінних наведено нижче:

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

Ефекти видимості летких змінних виходять за межі значення самої летючої змінної. Коли потік A пише в летючу змінну, а згодом потік B зчитує ту саму змінну, значення всіх змінних, які були видимими для A до запису в летючу змінну, стають видимими для B після зчитування змінної змінної.

Найбільш поширеним використанням змінних змінних є позначка завершення, переривання або статусу:

  volatile boolean flag;
  while (!flag)  {
     // do something untill flag is true
  }

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

Блокування може гарантувати як видимість, так і атомність; мінливі змінні можуть гарантувати лише видимість.

Ви можете використовувати летючі змінні лише тоді, коли виконуються всі наступні критерії:

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

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

Це уривок із "Паралельності Java на практиці" , найкращої книги, яку ви можете знайти на цю тему.


15

Я трохи змінив ваш приклад. Тепер використовуйте приклад з keepRunning як мінливий і нелеткий член:

class TestVolatile extends Thread{
    //volatile
    boolean keepRunning = true;

    public void run() {
        long count=0;
        while (keepRunning) {
            count++;
        }

        System.out.println("Thread terminated." + count);
    }

    public static void main(String[] args) throws InterruptedException {
        TestVolatile t = new TestVolatile();
        t.start();
        Thread.sleep(1000);
        System.out.println("after sleeping in main");
        t.keepRunning = false;
        t.join();
        System.out.println("keepRunning set to " + t.keepRunning);
    }
}

Чудовий приклад. Це у мене прекрасно спрацювало. без мінливості на keepRunning нитка зависає назавжди. Як тільки ви позначите keepRunning як нестійкий - він зупиняється після t.keepRunning = false;
Борис

4
Приклад працював у мене, шукав робочий приклад. +1, бо мені це допомогло, а відсутність пояснень не зашкодило і не заслуговує на голосування проти.
John Doe

1
Привіт paritosht та @John Doe, не могли б ви допомогти пояснити, чому ваш код є робочим прикладом? Коли моя машина виконує код, наданий у питанні, з мінливим ключовим словом або без нього, він все одно зупиняється.
shanwu

Я отримую той самий результат з і з вами votaliteтут
WW

13

Що таке мінливе ключове слово?

мінливе ключове слово запобігає caching of variables.

Розглянемо код, спочатку без мінливого ключового слова

class MyThread extends Thread {
    private boolean running = true;   //non-volatile keyword

    public void run() {
        while (running) {
            System.out.println("hello");
        }
    }

    public void shutdown() {
        running = false;
    }
}

public class Main {

    public static void main(String[] args) {
        MyThread obj = new MyThread();
        obj.start();

        Scanner input = new Scanner(System.in);
        input.nextLine(); 
        obj.shutdown();   
    }    
}

В ідеалі ця програма повинна виконуватися print helloдо RETURN keyнатискання. Але на some machinesцьому може статися така змінна працює , cachedі ви не можете змінити її значення методом shutdown (), що призводить до infiniteдруку привітного тексту.

Таким чином, використовуючи нестабільне ключове слово, це означає, guaranteedщо ваша змінна не буде кешована, тобто будеrun fine включена all machines.

private volatile boolean running = true;  //volatile keyword

Таким чином, використання летючого ключового слова - це goodі safer programming practice.


7

Variable Volatile: Volatile Keyword застосовується до змінних. volatile ключове слово в Java гарантує, що значення змінної volatile завжди буде зчитуватися з основної пам'яті, а не з локального кешу Thread.

Access_Modifier volatile DataType Variable_Name;

Нестабільне поле: вказівка ​​на віртуальну машину про те, що кілька потоків можуть намагатися отримати доступ / оновити значення поля одночасно. До особливого виду змінних екземпляра, які мають спільно використовувати між усіма потоками зі зміненим значенням. Подібно до змінної Static (Class), лише одна копія леткого значення кешується в основній пам'яті, так що перед виконанням будь-яких операцій ALU кожен потік повинен прочитати оновлене значення з основної пам'яті після операції ALU, він повинен записати в основну пам'ять direclty. (Запис у летючу змінну v синхронізується - з усіма наступними зчитуваннями v будь-яким потоком) Це означає, що зміни до летючої змінної завжди видно іншим потокам.

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

Ось nonvoltaile variableякщо a Thread t1 змінює значення в кеші t1, Thread t2 не може отримати доступ до зміненого значення, доки t1 не запише, t2 прочитає з основної пам'яті останнє змінене значення, що може призвести до Data-Inconsistancy.

нестійкі не можуть бути кешовані - асемблер

    +--------------+--------+-------------------------------------+
    |  Flag Name   |  Value | Interpretation                      |
    +--------------+--------+-------------------------------------+
    | ACC_VOLATILE | 0x0040 | Declared volatile; cannot be cached.|
    +--------------+--------+-------------------------------------+
    |ACC_TRANSIENT | 0x0080 | Declared transient; not written or  |
    |              |        | read by a persistent object manager.|
    +--------------+--------+-------------------------------------+

Shared Variables: Пам'ять, яку можна спільно використовувати між потоками, називається спільною пам’яттю або кучевою пам’яттю. Усі поля екземпляра, статичні поля та елементи масиву зберігаються в купі пам'яті.

Синхронізація : синхронізована застосовується до методів, блоків. дозволяє виконувати лише 1 потік за раз на об’єкті. Якщо t1 бере контроль, тоді решта потоків повинна почекати, поки він не звільнить елемент керування.

Приклад:

public class VolatileTest implements Runnable {

    private static final int MegaBytes = 10241024;

    private static final Object counterLock = new Object();
    private static int counter = 0;
    private static volatile int counter1 = 0;

    private volatile int counter2 = 0;
    private int counter3 = 0;

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            concurrentMethodWrong();
        }

    }

    void addInstanceVolatile() {
        synchronized (counterLock) {
            counter2 = counter2 + 1;
            System.out.println( Thread.currentThread().getName() +"\t\t « InstanceVolatile :: "+ counter2);
        }
    }

    public void concurrentMethodWrong() {
        counter = counter + 1;
        System.out.println( Thread.currentThread().getName() +" « Static :: "+ counter);
        sleepThread( 1/4 );

        counter1 = counter1 + 1;
        System.out.println( Thread.currentThread().getName() +"\t « StaticVolatile :: "+ counter1);
        sleepThread( 1/4 );

        addInstanceVolatile();
        sleepThread( 1/4 );

        counter3 = counter3 + 1;
        sleepThread( 1/4 );
        System.out.println( Thread.currentThread().getName() +"\t\t\t\t\t « Instance :: "+ counter3);
    }
    public static void main(String[] args) throws InterruptedException {
        Runtime runtime = Runtime.getRuntime();

        int availableProcessors = runtime.availableProcessors();
        System.out.println("availableProcessors :: "+availableProcessors);
        System.out.println("MAX JVM will attempt to use : "+ runtime.maxMemory() / MegaBytes );
        System.out.println("JVM totalMemory also equals to initial heap size of JVM : "+ runtime.totalMemory() / MegaBytes );
        System.out.println("Returns the amount of free memory in the JVM : "+ untime.freeMemory() / MegaBytes );
        System.out.println(" ===== ----- ===== ");

        VolatileTest volatileTest = new VolatileTest();
        Thread t1 = new Thread( volatileTest );
        t1.start();

        Thread t2 = new Thread( volatileTest );
        t2.start();

        Thread t3 = new Thread( volatileTest );
        t3.start();

        Thread t4 = new Thread( volatileTest );
        t4.start();

        Thread.sleep( 10 );;

        Thread optimizeation = new Thread() {
            @Override public void run() {
                System.out.println("Thread Start.");

                Integer appendingVal = volatileTest.counter2 + volatileTest.counter2 + volatileTest.counter2;

                System.out.println("End of Thread." + appendingVal);
            }
        };
        optimizeation.start();
    }

    public void sleepThread( long sec ) {
        try {
            Thread.sleep( sec * 1000 );
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Статичне [ Class Field] та нестабільне [ Instance Field] - обидва не кешуються потоками

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

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

@подивитися


6

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

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

Інша річ, на яку слід звернути увагу, - System.out.println(...)це синхронізація, оскільки основнаPrintStream виконує синхронізацію, щоб зупинити перекриття виводу. Отже, ви отримуєте синхронізацію пам'яті "безкоштовно" в основному потоці. Однак це все ще не пояснює, чому цикл читання взагалі бачить оновлення.

Незалежно від того, чи є println(...)рядки поза межами, ваша програма обертається для мене під Java6 на MacBook Pro з Intel i7.

Хтось може пояснити нестабільність на прикладі? Не з теорією від JLS.

Я вважаю, що ваш приклад хороший. Не впевнений, чому він не працює з усіма System.out.println(...)видаленими операторами. Це працює для мене.

Чи нестійкий замінник синхронізації? Чи досягається це атомність?

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

Однак з точки зору atomicityвідповіді "це залежить". Якщо ви читаєте або пишете значення з поля, це volatileзабезпечує належну атомність. Однак збільшення volatileполя страждає від обмеження, яке ++насправді становить 3 операції: читання, збільшення, запис. У цьому випадку або в більш складних випадках мьютексу synchronizedможе знадобитися повний блок. AtomicIntegerвирішує ++проблему за допомогою складного випробувального циклу.


Я прокоментував обидва твердження SOPln, але він все одно зупиняється через кілька секунд .. Ви можете показати мені приклад, який би працював, як очікувалося?
tmgr

Ви працюєте на одній процесорній системі @ tm99? Тому що ваша програма для мене вічно крутиться на Macbook Pro Java6.
Grey


2
"Будь-який синхронізований блок (або будь-яке летке поле) призводить до синхронізації всієї пам'яті" - ви впевнені? Чи могли б ви надати посилання на JLS? Наскільки я пам’ятаю, єдина гарантія полягає в тому, що модифікації пам’яті, виконані до звільнення блокування L1, будуть видимі потокам після того, як вони отримають той самий замок L1; з мінливими компонентами, всі модифікації пам'яті перед нестійким записом у F1 видно потоку після леткого читання того самого поля F1, що дуже відрізняється від твердження, що вся * пам'ять синхронізована. Це не так просто, як будь-який потік, на якому запущений синхронізований блок.
Бруно Рейс

1
Коли будь-який бар'єр пам'яті перетинається (з synchronizedабо volatile), для всієї пам'яті існує взаємозв'язок "трапляється раніше" . Немає гарантій щодо порядку блокування та синхронізації, якщо ви не заблокуєте той самий монітор, до якого вас посилають @BrunoReis. Але якщо println(...)заповнить, ви гарантовано оновите keepRunningполе.
Грей

3

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


У сучасному багатопроцесорному @Jeff ваш останній коментар є дещо неправильним / оманливим. JVM насправді розумний щодо того, щоб не змивати значення, оскільки робити це - хіт продуктивності.
Сірий

Коли для KeepRunning встановлено значення false за допомогою main, потік все ще бачить оновлення, оскільки JVM розумно змиває значення. Однак це не гарантується (див. Коментар від @Gray вище).
Джефф Сторі

2

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

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

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


2

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

  1. Ви можете використовувати, volatileщоб примусити всі потоки отримувати останнє значення змінної з основної пам'яті.
  2. Ви можете використовувати synchronizationдля захисту критичних даних
  3. Ви можете використовувати LockAPI
  4. Ви можете використовувати Atomicзмінні

Ознайомтеся з іншими прикладами летючої Java .


1

Знайдіть рішення нижче,

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

public class VolatileDemo {

    private static volatile int MY_INT = 0;

    public static void main(String[] args) {

        ChangeMaker changeMaker = new ChangeMaker();
        changeMaker.start();

        ChangeListener changeListener = new ChangeListener();
        changeListener.start();

    }

    static class ChangeMaker extends Thread {

        @Override
        public void run() {
            while (MY_INT < 5){
                System.out.println("Incrementing MY_INT "+ ++MY_INT);
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException exception) {
                    exception.printStackTrace();
                }
            }
        }
    }

    static class ChangeListener extends Thread {

        int local_value = MY_INT;

        @Override
        public void run() {
            while ( MY_INT < 5){
                if( local_value!= MY_INT){
                    System.out.println("Got Change for MY_INT "+ MY_INT);
                    local_value = MY_INT;
                }
            }
        }
    }

}

Будь ласка, зверніться за цим посиланням http://java.dzone.com/articles/java-volatile-keyword-0, щоб отримати більше ясності в ньому.


Хоча це посилання може відповісти на питання, краще включити сюди основні частини відповіді та надати посилання для довідки. Відповіді лише на посилання можуть стати недійсними, якщо пов’язана сторінка зміниться.
admdrew

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

1

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

public class VolatileTest {
    private static final Logger LOGGER = MyLoggerFactory.getSimplestLogger();

    private static volatile int MY_INT = 0;

    public static void main(String[] args) {
        new ChangeListener().start();
        new ChangeMaker().start();
    }

    static class ChangeListener extends Thread {
        @Override
        public void run() {
            int local_value = MY_INT;
            while ( local_value < 5){
                if( local_value!= MY_INT){
                    LOGGER.log(Level.INFO,"Got Change for MY_INT : {0}", MY_INT);
                     local_value= MY_INT;
                }
            }
        }
    }

    static class ChangeMaker extends Thread{
        @Override
        public void run() {

            int local_value = MY_INT;
            while (MY_INT <5){
                LOGGER.log(Level.INFO, "Incrementing MY_INT to {0}", local_value+1);
                MY_INT = ++local_value;
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) { e.printStackTrace(); }
            }
        }
    }
}

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


0

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

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

Нестабільна змінна повинна використовуватися лише в контексті потоку. дивіться приклад тут

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