首頁 > Java > java教程 > 帶你了解volatile變數--Java並發程式設計與技術內幕

帶你了解volatile變數--Java並發程式設計與技術內幕

php是最好的语言
發布: 2018-07-26 15:29:52
原創
1505 人瀏覽過

 Java語言提供了一種稍微弱的同步機制,即volatile變量,用來確保將變數的更新操作通知到其他執行緒。 volatile 效能:volatile 的讀取效能消耗與普通變數幾乎相同,但是寫入操作稍慢,因為它需要在本機程式碼中插入許多記憶體屏障指令來保證處理器不會發生亂序執行。

 一、volatile變數

       Java語言提供了一個稍微較弱的同步機制,即volatile變數,用來確保將變數的更新操作通知其他執行緒。當把變數宣告為volatile類型後,編譯器與執行時間都會注意到這個變數是共享的,因此不會將該變數上的操作與其他記憶體操作一起重新排序。 volatile變數不會被緩存在暫存器或對其他處理器不可見的地方,因此在讀取volatile類型的變數時總是會傳回最新寫入的值。

       在存取volatile變數時不會執行加鎖操作,因此也不會使執行緒阻塞,因此volatile變數是一種比sychronized關鍵字更輕量級的同步機制。

帶你了解volatile變數--Java並發程式設計與技術內幕

當對非 volatile 變數進行讀寫的時候,每個執行緒先從記憶體拷貝變數到CPU快取中。如果電腦有多個CPU,每個執行緒可能在不同的CPU上處理,這表示每個執行緒可以拷貝到不同的 CPU cache 中。

       而宣告變數是 volatile 的,JVM 保證了每次讀取變數都從記憶體中讀,跳過 CPU cache 這一步驟。

當一個變數定義為volatile 之後,將具備兩種特性:

  1.保證此變數對所有的線程的可見性,這裡的“可見性”,如本文開頭所述,當一個線程修改了這個變數的值,volatile 保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。但普通變數做不到這一點,普通變數的值在線程間傳遞都需要透過主記憶體(詳見:Java記憶體模型)來完成。

  2.禁止指令重新排序最佳化。有volatile修飾的變量,賦值後多執行了一個「load addl $0x0, (%esp)」操作,這個操作相當於一個記憶體屏障(指令重排序時不能把後面的指令重排序到記憶體屏障之前的位置),只有一個CPU存取記憶體時,並不需要記憶體屏障;(什麼是指令重新排序:是指CPU採用了允許將多條指令不按程序規定的順序分開發送給各相應電路單元處理)。

volatile 效能:

  volatile 的讀取效能消耗與普通變數幾乎相同,但是寫入操作稍慢,因為它需要在本機程式碼中插入許多記憶體屏障指令來保證處理器不會發生亂序執行。

 二、記憶體可見度

       由於 Java 記憶體模型(JMM)規定,所有的變數都存放在主記憶體中,而每個執行緒都有自己的工作記憶體(高速緩存) 。

執行緒在工作時,需要將主記憶體中的資料拷貝到工作記憶體中。這樣對數據的任何操作都是基於工作內存(效率提高),並且不能直接操作主內存以及其他線程工作內存中的數據,之後再將更新之後的數據刷新到主內存中。

這裡所提到的主內存可以簡單地認為是堆內存,而工作內存則可以認為是棧內存。

所以在並發運行時可能會出現執行緒 B 所讀取到的資料是執行緒 A 更新之前的資料。

顯然這肯定是會出問題的,因此 volatile 的作用出現了:

當一個變數被 volatile 修飾時,任何執行緒對它的寫入操作都會立即刷新到主記憶體中,並且會強制讓快取了該變數的執行緒中的資料清空,必須從主記憶體重新讀取最新資料。

 volatile 修飾之後並不是讓執行緒直接從主記憶體取得數據,依然需要將變數拷貝到工作記憶體中。

記憶體可見性的應用

當我們需要在兩個執行緒間依據主記憶體通訊時,通訊的那個變數就必須的用 volatile 來修飾:

public class Test {

private static /*volatile*/ boolean stop = false;

public static void main(String[] args) throws Exception {
    Thread t = new Thread(
            () -> {
                int i = 0;
                while (!stop) {
                    i++;
                 System.out.println("hello");
                }
            });
    t.start();

    Thread.sleep(1000);
    TimeUnit.SECONDS.sleep(1);
    System.out.println("Stop Thread");
    stop = true;
}
}
登入後複製

上面的例子如果不設定為volatile,則該線程可能永遠不會退出

但這裡有個誤區,這樣的使用方式容易給人的感覺是:

對 volatile 修飾的變數進行並發操作是執行緒安全的。

這裡要重點強調,volatile 並不能保證執行緒安全性!

如下程式:

public class VolatileInc implements Runnable {

private static volatile int count = 0; //使用 volatile 修饰基本数据内存不能保证原子性

//private static AtomicInteger count = new AtomicInteger() ;

@Override

public void run() {
    for (int i = 0; i < 100; i++) {
        count++;
        //count.incrementAndGet() ;

    }
}

public static void main(String[] args) throws InterruptedException {
    VolatileInc volatileInc = new VolatileInc();
    IntStream.range(0,100).forEach(i->{
        Thread t= new Thread(volatileInc, "t" + i);
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    System.out.println(count);
}
}
登入後複製

當我們三個執行緒(t1,t2,main)同時對一個 int 進行累加時會發現最終的值都會小於 100000。

这是因为虽然 volatile 保证了内存可见性,每个线程拿到的值都是最新值,但 count ++ 这个操作并不是原子的,这里面涉及到获取值、自增、赋值的操作并不能同时完成。

所以想到达到线程安全可以使这三个线程串行执行(其实就是单线程,没有发挥多线程的优势)。也可以使用 synchronize 或者是锁的方式来保证原子性。还可以用 Atomic 包中 AtomicInteger 来替换 int,它利用了 CAS 算法来保证了原子性。

三、指令重排序

内存可见性只是 volatile 的其中一个语义,它还可以防止 JVM 进行指令重排优化。

举一个伪代码:

int a=10 ;//1
int b=20 ;//2
int c= a+b ;//3
登入後複製

一段特别简单的代码,理想情况下它的执行顺序是:1>2>3。但有可能经过 JVM 优化之后的执行顺序变为了 2>1>3。

可以发现不管 JVM 怎么优化,前提都是保证单线程中最终结果不变的情况下进行的。

可能这里还看不出有什么问题,那看下一段伪代码:

 private static Map<String,String> value ;
 private static volatile boolean flag = fasle ;
  //以下方法发生在线程 A 中 初始化 Map
 public void initMap(){
 //耗时操作
 value = getMapValue() ;//1
 flag = true ;//2
}

 //发生在线程 B中 等到 Map 初始化成功进行其他操作
public void doSomeThing(){
while(!flag){
    sleep() ;
}
 //dosomething
 doSomeThing(value);
}
登入後複製

这里就能看出问题了,当 flag 没有被 volatile 修饰时,JVM 对 1 和 2 进行重排,导致 value都还没有被初始化就有可能被线程 B 使用了。

所以加上 volatile 之后可以防止这样的重排优化,保证业务的正确性。

指令重排的的应用

一个经典的使用场景就是双重懒加载的单例模式了:

class Singleton{
private volatile static Singleton instance = null;

private Singleton() {

}

public static Singleton getInstance() {
    if(instance==null) {
        synchronized (Singleton.class) {
            if(instance==null)
                instance = new Singleton();
        }
    }
    return instance;
}
登入後複製

这里的 volatile 关键字主要是为了防止指令重排。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:

    1.给 instance 分配内存

    2.调用 Singleton 的构造函数来初始化成员变量

    3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。

       但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

相关文章:

具体介绍java高并发中volatile的实现原理

Java中如何正确使用Volatile变量?

相关视频:

Java多线程与并发库高级应用视频教程

以上是帶你了解volatile變數--Java並發程式設計與技術內幕的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板