java多線程以內存可見性

前言:

談到多線程,不可避免的就會談到數據爭用的問題,而要解決數據爭用,就須要學習內存可見性相關的知識java

1、共享變量在線程間的可見性:

首先對相關的幾個名詞進行解釋:緩存

1. 什麼是「可見性」:一個線程對共享變量值的修改,可以及時被其它線程看到安全

2. 什麼是「共享變量」:若是一個變量在多個線程的工做內存中都存在副本,那麼這個變量就是這幾個線程的共享變量多線程

3. 什麼是「線程的工做內存」:要了解這個,咱們須要先了解一下java內存模型(JMM)併發

JMM描述了java程序中各類變量(線程共享變量)的訪問規則,以及在JVM 中將變量存儲到內存和從內存中取出變量這樣的底層細節。ide

  • 全部變量都存儲在主內存中
  • 每一個線程都有本身獨立的工做內存,裏面保存該線程使用到的變量的副本(主內存中該變量的一份拷貝)

這裏有兩條規定性能

  • 線程對共享變量的全部操做都必須在本身的工做內存中進行,不能直接從主內存中讀寫
  •  不一樣線程之間沒法直接訪問其餘線程工做內存中的變量,線程間變量值的傳遞須要經過主內存來完成(主內存做爲橋樑)

致使共享變量在線程間不可見的緣由:學習

  1. 線程交叉執行
  2. 重排序結合線程交叉執行
  3. 共享變量更新後的值沒有在工做內存和主內存間及時更新

所以,要實現共享變量的可見性,必須保證兩點:優化

  1. 線程修改後的共享變量值可以及時從工做內存中刷新到主內存中
  2. 其餘線程可以及時把共享變量的最新值從主內存更新到本身的工做內存

2、synchronized:

synchronized是java語言層面支持的可見性實現方式之一(不包括jdk1.5以後,concurrent併發包下的一些高級特性),它能夠實現互斥鎖(即實現同步),從而保證在任意一個時刻都只有一個線程在執行鎖裏的代碼。線程執行互斥代碼的過程以下:this

  1. 在synchronized入口處得到互斥鎖 
  2. 清空工做內存
  3. 從主內存拷貝變量的最新副本到工做內存
  4. 執行鎖內部代碼
  5. 將更改後的共享變量的值刷新到主內存
  6. 程序退出synchronized語句塊時釋放互斥鎖
package mkw.demo.syn;

public class SynchronizedDemo {
	//共享變量
    private boolean ready = false;
    private int result = 0;
    private int number = 1;   
    //寫操做
    public synchronized void write(){
    	ready = true;	      				//1.1				
    	number = 2;		                    //1.2			    
    }
    //讀操做
    public synchronized void read(){			   	 
    	if(ready){					//2.1
    		result = number*3;	 	//2.2
    	}   	
    	System.out.println("result的值爲:" + result);
    }

    //內部線程類
    private class ReadWriteThread extends Thread {
    	//根據構造方法中傳入的flag參數,肯定線程執行讀操做仍是寫操做
    	private boolean flag;
    	public ReadWriteThread(boolean flag){
    		this.flag = flag;
    	}
        @Override                                                                    
        public void run() {
        	if(flag){
        		//構造方法中傳入true,執行寫操做
        		write();
        	}else{
        		//構造方法中傳入false,執行讀操做
        		read();
        	}
        }
    }

    public static void main(String[] args)  {
    	SynchronizedDemo synDemo = new SynchronizedDemo();
    	//啓動線程執行寫操做
    	synDemo .new ReadWriteThread(true).start();
    	try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    	//啓動線程執行讀操做
    	synDemo.new ReadWriteThread(false).start();
    }
}

  

3、volatile:

特色:

  • 可以保證volatile變量的可見性
  • 不能保證volatile變量複合操做的原子性

volatile如何實現內存可見性:

深刻來講:經過加入內存屏障和禁止重排序優化來實現的。

  • 對volatile變量執行寫操做時,會在寫操做後加入一條store屏障指令,將緩存區中的值強制刷新到主內存
  • 對volatile變量執行讀操做時,會在讀操做前加入一條load屏障指令,強制使緩存區中的緩存失效,每次都從主內存中讀最新的值

通俗地講:volatile變量在每次被線程訪問時,都強制從主內存中讀取該變量的值,而當該變量發生變化時,又會強制線程將最新的值刷新到主內存。這樣任什麼時候刻,不一樣線程總能看到該變量的最新值。

要保證操做的原子性,有如下解決方案:

  1. 使用synchronized關鍵字

 

    2. 使用ReentrantLock(java.util.concurrent.locks包下)

    3. 使用AutomicInteger(java.util.concurrent.automic包下)

volatile使用注意事項:

因爲volatile不能保證原子性,線程可能發生交叉執行,因此可能會產生一些問題:

要在多線程中安全的使用volatile變量,必須同時知足如下條件:

1. 對變量的寫入操做不依賴當前值

  • 不知足:number++、count=count*5等
  • 知足:boolean變量、記錄溫度變化的變量等

2. 該變量沒有包含在具備其餘變量的不變式中(若程序中有多個volatile變量,每一個volatile變量的狀態要獨立於其餘volatile變量)

  • 不知足:不變式low<up

寫代碼時會發現,不少時候至少都會和上述兩個條件之一衝突,所以volatile並無synchronized使用普遍

4、synchronized和volatile的比較 

1. volatile不須要加鎖,比synchronized更輕量級,不會阻塞線程,;

2. 從內存可見性角度,volatile讀至關於加鎖,volatile寫至關於解鎖;

3. synchronized既能保證可見性,又能保證原子性,而volatile只能保證可見性,沒法保證原子性。

volatile使用有諸多限制,所以沒有synchronized使用普遍,但因爲volatile比synchronized更輕量,執行效率更高,所以若是能保證線程安全的狀況下,儘量選擇volatile

附錄:

  1. 指令重排序:代碼書寫順序與實際執行的順序不一樣,指令重排序是編譯器或處理器爲了提升程序性能而作的優化(只有數據依賴關係纔會禁止指令重排序,參見下圖的sum值,依賴num1+num2,故該行不能重排序
  2. as-if-serial語義:不管如何重排序,程序執行的結果應該與代碼順序執行結果一致(java編譯器、運行時和處理器都會敖政java在單線程下遵循as-if-serial語義)

          

相關文章
相關標籤/搜索