多線程(四)—— 內存可見性


一、可見性

   多個線程對同一個變量(稱為:共享變量)進行操作,但是這多個線程有可能被分配到多個處理器中運行,那么編譯器會對代碼進行優化,當線程要處理該變量時,多個處理器會將變量從主存復制一份分別存儲在自己的存儲器中,等到進行完操作后,再賦值回主存。

  這樣做的好處是提高了運行的速度,同樣優化帶來的問題之一是變量可見性——如果線程t1與線程t2分別被安排在了不同的處理器上面,那么t1與t2對於變量A的修改時相互不可見,如果t1給A賦值,然后t2又賦新值,那么t2的操作就將t1的操作覆蓋掉了,這樣會產生不可預料的結果。因此,需要保證變量的可見性(一個線程對共享變量值的修改,能夠及時地被其它線程看到)。

  注意:共享數據的訪問權限必須定義為private

  多線程操作共享變量實現可見性過程JVM的內存模型如下:

 

  所有的變量都存儲在主內存中每個線程都有自己獨立的工作內存,里面保存該線程使用到的變量的副本(主內存中該變量的一份拷貝)。

 JVM模型兩條規定

   1、線程對共享變量的所有操作必須在自己的內存中進行,不能直接從主內存中讀寫

   2、不同線程之間無法直接訪問其它線程工作內存中的變量,線程間變量值的傳遞需要通過主內存來完成

  那么共享變量應該怎樣實現可見性呢?  

  比如:線程1對共享變量的修改想要被線程2及時看到,必須經過如下2步操作:
  把工作內存1中的更新過的共享變量刷新到主內存中,再將主內存中最新的共享變量的值更新到2的工作內存中。

  初始值X=0,線程1將X=1,到其他線程X值的更改,如下圖的更改過程。

                  

   因此,要實現共享變量的可見性,必須保證兩點:
   線程修改后的共享變量值能夠及時從工作內存刷新到主內存中;
   其他線程能夠及時的把共享變量的最新值從主內存更新到自己的工作內存中。

   在Java語言層面支持的可見性實現原理方式有synchronize和volatile。

備注:

導致共享變量在線程間不可見的原因:
  線程的交叉執行;
  重排序結合線程的交叉執行;
  共享變量更新后的值沒有在工作內存與主內存及時更新。

 二、synchronize 

  能夠實現代碼的原子性(同步)和 內存的可見性。

原子性:即一個操作或者多個操作 要么全部執行並且執行的過程不會被任何因素打斷,要么就都不執行。

原子性就像數據庫里面的事務一樣,他們是一個團隊,同生共死。其實理解原子性非常簡單,我們看下面一個簡單的例子即可:

i = 0;---1 
j = i ; ---2 
i++; ---3 
i = j + 1; ---4

上面四個操作,有哪個幾個是原子操作,那幾個不是?如果不是很理解,可能會認為都是原子性操作,其實只有1才是原子操作,其余均不是。

1—在Java中,對基本數據類型的變量和賦值操作都是原子性操作; 
2—包含了兩個操作:讀取i,將i值賦值給j 
3—包含了三個操作:讀取i值、i + 1 、將+1結果賦值給i; 
4—同三一樣
  在單線程環境下我們可以認為整個步驟都是原子性操作,但是在多線程環境下則不同,Java只保證了基本數據類型的變量和賦值操作才是原子性的(注:在32位的JDK環境下,對64位數據的讀取不是原子性操作*,如long、double)。要想在多線程環境下保證原子性,則可以通過鎖、synchronized來確保。

   JVM對其的兩條規定:
  線程解鎖前,必須把共享變量的最新值刷新到主內存中;
  線程枷鎖前,將清空工作內存中的共享變量的值,從而使用共享變量時需要從主內存中重新讀取新的值。(枷鎖與解鎖需要是同一把鎖)。
  線程解鎖前對共享線程變量的修改在下次枷鎖時對其他線程可見。

  線程 執行互斥代碼的過程:    

1、獲得互斥鎖;
2、清空工作內存;
3、從主內存拷貝變量的最新的值到工作內存;
4、執行代碼;
5、將更改后的共享變量的值刷新到主內存;
6、釋放互斥鎖。

 三、volatile

  保證變量的可見性,不能保證變量的符合操作原子性。

實現內存可見
       深入的說:通過加入內存屏障和禁止重排序優化實現。對其變量執行寫操作時,會在寫操作后加入一條store屏障指令;對其進行讀操作時,會在讀操作前加入一條load屏障指令。
       通俗的說:volatile變量在每次被線程訪問時,都強迫從主線程中重讀該變量的值,而當該變量發生變化時,又會強迫線程將最新的值刷新到主內存中,這樣任何時刻,不同的線程總能看到該變量最新的值

 

  線程寫volatile變量的過程:
         1、改變線程工作內存中volatile變量副本的值;
    2、將改變后的副本的值從工作內存刷新到主內存。
    3、線程讀volatile變量的過程:
    4、從主內存中讀取volatile變量的最新值到線程的工作內存中;
    5、從工作內存中讀取volatile變量的副本。

  驗證 volatile 可以保證原子性。代碼如下:

 

/** * 驗證 volatile 是否保證原子性 * @author Administrator * */
public class VolatileDemo { private  volatile int number = 0; public int getNumber(){ return this.number; } public void increase(){ try { //更好的輸出效果
            Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block
 e.printStackTrace(); } this.number++; } /** * @param args */
    public static void main(String[] args) { // TODO Auto-generated method stub
        final VolatileDemo volDemo = new VolatileDemo(); //實現500次自增
        for(int i = 0 ; i < 500 ; i++){ new Thread(new Runnable() { @Override public void run() { volDemo.increase(); } }).start(); } //如果還有子線程在運行,主線程就讓出CPU資源, //直到所有的子線程都運行完了,主線程再繼續往下執行
        while(Thread.activeCount() > 1){ Thread.yield(); } System.out.println("number : " + volDemo.getNumber()); } }

 

  上述代碼,理想情況或單線程時輸出結果應該為:500,然而實際輸出的結果是小於500的數字(多執行幾遍)。這是因為 number++ 不是原子操作,會造成多個線程交叉執行。

 

方法一: synchronized

public void increase(){ try { //更好的輸出效果
            Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block
 e.printStackTrace(); }  synchronized(this){ this.number++; } }

  當然,也可以 public synchronized int increase(){...} 但是這樣造成程序性能更加低效。

方法二: java.util.concurrent.locks.ReentrantLock

import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class VolatileDemo { private Lock lock = new ReentrantLock(); private int number = 0; public int getNumber(){ return this.number; } public void increase(){ try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block
 e.printStackTrace(); } lock.lock(); try { this.number++; } finally { lock.unlock(); } } /** * @param args */
    public static void main(String[] args) { // TODO Auto-generated method stub
        final VolatileDemo volDemo = new VolatileDemo(); for(int i = 0 ; i < 500 ; i++){ new Thread(new Runnable() { @Override public void run() { volDemo.increase(); } }).start(); } //如果還有子線程在運行,主線程就讓出CPU資源, //直到所有的子線程都運行完了,主線程再繼續往下執行
        while(Thread.activeCount() > 1){ Thread.yield(); } System.out.println("number : " + volDemo.getNumber()); } }

方法三:java.util.concurrent.atomic.AtomicInteger;

import java.util.concurrent.atomic.AtomicInteger; public class VolatileDemo { private static AtomicInteger a = new AtomicInteger(); public int getNumber(){ return a.get(); } public void increase(){ try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block
 e.printStackTrace(); } a.getAndIncrement(); } /** * @param args */
    public static void main(String[] args) { // TODO Auto-generated method stub
        final VolatileDemo volDemo = new VolatileDemo(); for(int i = 0 ; i < 500 ; i++){ new Thread(new Runnable() { @Override public void run() { volDemo.increase(); } }).start(); } //如果還有子線程在運行,主線程就讓出CPU資源, //直到所有的子線程都運行完了,主線程再繼續往下執行
        while(Thread.activeCount() > 1){ Thread.yield(); } System.out.println("number : " + volDemo.getNumber()); } }

  AtomicInteger是一個提供原子操作的Integer類,通過線程安全的方式操作加減,因此十分適合高並發情況下的使用。

java.util.concurrent中實現的原子操作類包括:AtomicBoolean、AtomicInteger、AtomicIntegerArray、AtomicLong、AtomicReference、 AtomicReferenceArray。

 

 

volatile適用場合

在多線程中安全的使用volatile變量必須同時滿足兩個條件:
  ①對變量的寫入操作不依賴其當前值,如number++不可以,boolean變量可以
  ②該變量沒有包含在具有其他變量的不變式中,如果有多個volatile變量,則每個volatile變量必須獨立於其他的volatile變量

 

四、synchronize 與 volatile 比較

  1、volatile不需要枷鎖,比synchronize更輕量級,不會堵塞程序;
  2、從內存可見角度講,volatile讀相當於枷鎖,volatile寫相當於解鎖。
  3、synchronize既能保證可見性,又能保障原子性,而volatile只能保障可見性,不能保證原子性。

       4、synchronize 使用更加廣泛。

  

來自慕課網課程:細說Java多線程之內存可見性

 


免責聲明!

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



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