【慕課網學習筆記】Java共享變量的可見性和原子性


1. Java內存模型(Java Memory Model, JMM)

Java的內存模型如下,所有變量都存儲在主內存中,每個線程都有自己的工作內存。

共享變量:如果一個變量在多個線程中都使用到了,那么這個變量就是這幾個線程的共享變量。

可見性:一個線程對共享變量的修改,能夠及時地到主內存並且讓其他的線程看到。

怎么理解上面的可見性的意思呢?

線程對共享變量的修改,只能在自己的工作內存里操作,不能直接對主內存中的共享變量進行修改。而且一個線程不能直接訪問另一個線程中的變量的值,只能通過主內存進行共享傳遞。

那么就要求線程A對共享變量修改后,及時地更新到主內存中,線程B才可以及時地從主內存獲取最新的值到工作內存。

比如一個共享變量int i = 0; 線程A將其改為i =1; 其他線程此時獲取i的值,應該能及時地得到1,而不是0。

 

2. synchronized實現可見性

synchronized除了常見的原子性,還實現了可見性。這是因為:

1) 線程解鎖前,必須把共享變量的最新值刷新到主內存中去;

2) 線程加鎖時,將清空工作內存中的共享變量的值,使用到共享變量時,從主內存中獲取最新的共享變量值(加鎖和解鎖需要同一把鎖)

 

3. volatile實現可見性

通過內存屏障和禁止重排序優化來實現可見性。

1) 對共享變量進行寫操作后,加入一條store屏障指令,強制將共享變量的值刷新到主內存;

2) 對共享變量進行讀操作前,加入一條load屏障指令,強制從主內存中將最新值刷新到工作內存;

 

4.volatile不能保證原子性

一個比較典型的例子是++運算符。

在下面的代碼中,一共創建了1000個線程,預期應該是加了1000次,那么number的值應該是1000,實際上有可能並不是。

這是因為,++運算符並不是一次操作。以number++為例,可以看作是,先從主內存中取出number的值,然后將其加1,刷新工作內存,刷新主內存,這么幾個步驟。

而volatile並不能保證原子性,這就意味着,有可能出現這種情況:

1)線程A獲取到主內存的number的值(假設為10)到工作內存

2)此時CPU調度,A暫停,線程B開始執行,同樣從主內存中獲取到number為10,number++后,number為11,刷新到主內存

3)線程A繼續執行number++,它的工作內存中number為10,執行完畢刷新到主內存,此時,number的值為11. 也就是說,AB兩個線程同時進行了+1操作,但最終的結果,只加了1

 1 public class VolatileDemo {
 2 
 3     private int number = 0;
 4 
 5     public void increase() {
 6         number++;
 7     }
 8 
 9     public int getNumber() {
10         return number;
11     }
12 
13     public static void main(String[] args) {
14         final VolatileDemo demo = new VolatileDemo();
15 
16         for (int i = 0; i <= 999; i++) {
17             new Thread(new Runnable() {
18                 @Override
19                 public void run() {
20                     demo.increase();
21                 }
22             }).start();
23         }
24         
25         //線程未執行完,主線程讓出CPU資源
26         while(Thread.activeCount() > 1){
27             Thread.yield();
28         }
29 
30         //待上面的線程都執行完了,再打印,避免打印的不是最后的數據
31         System.out.println(demo.getNumber());
32     }
33 }

 

5.volatile適用場景

1)對共享變量的寫操作,不依賴於其之前的值

不合適:number++, number = number * 2, number += 1等

合適:boolean值

2)該變量沒有包含在具有其他變量的不變式中,也就是說,不同的volatile變量之間,不能互相依賴

 

6.AtomicInteger實現遞增

上面我們已經知道一個整型的共享變量要實現遞增,如果使用++運算符,即使加上volatile關鍵字,也是無法保證其原子性的。而如果在訪問變量時加上synchronized塊,或者可重入鎖,開銷又太大。

JDK1.5之后,可以使用AtomicInteger進行遞增。該類是線程安全的。

將上面的代碼修改如下,就可以保證原子性和可見性。

 1 import java.util.concurrent.atomic.AtomicInteger;
 2 
 3 public class VolatileDemo {
 4 
 5     private AtomicInteger number = new AtomicInteger(0);
 6 
 7     public void increase() {
 8         number.incrementAndGet();
 9     }
10 
11     public int getNumber() {
12         return number.intValue();
13     }
14 
15     public static void main(String[] args) {
16         final VolatileDemo demo = new VolatileDemo();
17 
18         for (int i = 0; i <= 999; i++) {
19             new Thread(new Runnable() {
20                 @Override
21                 public void run() {
22                     demo.increase();
23                 }
24             }).start();
25         }
26         
27         //線程未執行完,主線程讓出CPU資源
28         while(Thread.activeCount() > 1){
29             Thread.yield();
30         }
31 
32         //待上面的線程都執行完了,再打印,避免打印的不是最后的數據
33         System.out.println(demo.getNumber());
34     }
35 }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM