Про службове слово volatile джава програмісти згадують в більшості перед співбесідами або на них. І мало хто до кінця розбирається що воно таке і з чим його їдять. Причина для цього досить проста, щоб знати коли його використовувати необхідно мати досить непогані знання в галузях cpu регістру, кешування. Інша причина полягає в тому, що досить важко продемонструвати наслідки не використання цоьго слова. Саме з цим ми постараємось розібратись з-за допомогою маленької задачки.
Отже до задачки, спочатку не запускаючи програму подумайте що станеться з таким кодом
1. public class ConcurrencyFun implements Runnable
2. {
3. private String str;
4. void setStr(String str)
5. {
6. this.str = str;
7. }
8. public void run()
9. {
10. while (str == null);
11. System.out.println(str);
12. }
13. public static void main(String[] args) throws Exception
14. {
15. ConcurrencyFun fun = new ConcurrencyFun();
16. new Thread(fun).start();
17. Thread.sleep(1000);
18. fun.setStr(“Hello world!!”);
19. }
20. }
Більшість мабуть скажуть що код буде чекати 1 секунду і виводити повідомлення “Hello World!”. Новостворений (поороджений) потік чекає поки str не null і виводить його. Основний потік запускає породжений(?) потік, чекає 1 сек і вставляє в str значення “Hello World!”. Виглядає все просто? Чи не так?
Тепер спробуйте запустити цей код. На моїй машині програма виконується приблизно вічно :). Чому так?
Причина в тому, що JVM можестворювати свою власну копію на референс до str для кожного новоствореного потоку який її використовує. Це може бути в багатьох формах: може бути так що референс загружається в регістр і звідти постійно зчитується. Це мабуть те що і відбувається в нашому випадку. Може бути що референс загрузився в CPU кеш і ніколи не експайриться, навіть після обновлення (апдейту). Або також можливо що JVM створить копію на референс в потоках для більш ефективного використання доступу до памяті. Не залежно від того чи ви зрозуміли те що було згори, суть полягає в тому що зміни в полі str не обовязково будуть видимі всіма потоками які до цього поля доступаються, в нашому випадку вони ніколи не будуть видимі.
Саме тут і потрібне volatile. Ключове слово volatile повідомляє JVM, що будь-які записи у це поле повинні бути видимими для всіх потоків. Це означає що скомпільований код не може зчитувати значення змінної з регістру і використовувати його багато разів, а повинен кожного разу зчитувати його з памяті. Аналогічно з CPU кешом і з копіюванням JVM локальної копії в потоки stack frame.
Отже наш код буде мати вигляд:
1. public class ConcurrencyFun implements Runnable
2. {
3. private volatile String str;
4. void setStr(String str)
5. {
6. this.str = str;
7. }
8. public void run()
9. {
10. while (str == null);
11. System.out.println(str);
12. }
13. public static void main(String[] args) throws Exception
14. {
15. ConcurrencyFun fun = new ConcurrencyFun();
16. new Thread(fun).start();
17. Thread.sleep(1000);
18. fun.setStr(“Hello world!!”);
19. }
20. }
Як результат програма буде корректно працювати і друкувати привіт світу саме тоді коли і потрібно.
Посилання на оригінал статті:
http://jazzy.id.au/default/2009/04/24/java_concurrency_and_volatile.html
ПС: Якщо ви прочитали (передивились) оригіналі статті, то помітили, що там згадуються тільки Linux s Solaris OS, але я пробував приклади на своєму i5, Win7 і результат такий самий.
ПС2 Чекайте звіте з JavaDay 2012