Translate

неділя, 28 жовтня 2012 р.

Java Concurrency and Volatile


 

Про службове слово 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