[Java 併發編程實戰] 對 volatile 變量進行實例驗證(內含源碼)

「 天行健,君子以自強不息。地勢坤,君子以厚德載物。」———《易經》html

volatile 變量,在上一篇文章中已經有簡單說起相關概念和用法,這一篇主要對 Volatile 變量的特性進行源碼驗證。驗證它的涉及到的三個特性:java

  • 可見性
  • 指令重排序
  • 非原子性

#volatile 之可見性驗證 上一篇文章中,講到 volatile 變量一般被當作狀態標記使用。其中典型的應用是,檢查標記狀態,以肯定是否退出循環。下面咱們直接舉個反例,源碼以下:安全

public class Volatile {
		
	    boolean ready=true;  //volatile 狀態標誌變量
	    
	    private final static int SIZE = 10; //建立10個對象,可改變
	    
	    public static void main(String[] args) throws InterruptedException{
	        
	        Volatile vs[]=new Volatile[SIZE];
	
	        for(int n=0;n<SIZE;n++)
	            (vs[n]=new Volatile()).test(); 
	        
	        System.out.println("mainThread end");//調用結束打印,死循環時不打印
	    }
	
	    public void test() throws InterruptedException{
	        Thread t2=new Thread(){
	            public void run(){
	                while(ready);//變量爲true時,讓其死循環
	            }
	        };
	        Thread t1=new Thread(){
	            public void run(){
	            	ready=false;
	            }
	        };
	        t2.start();
	        Thread.yield();
	        t1.start();
	        t1.join();//保證一次只運行一個測試,以此減小其它線程的調度對 t2對boolValue的響應時間 的影響
	        t2.join();
	    }
	}

其中,ready 變量是咱們要驗證的 volatile 變量。一開始 ready 初始化爲 true,其次啓動 t2 線程讓其進入死循環;接着,t1 線程啓動,而且讓 t1 線程先執行,將 ready 改成 false。理論上來說,此時 t2 線程應該跳出死循環,可是實際上並無。此時 t2 線程讀到的 ready 的值仍然爲 true。因此這段程序一直沒有打印出結果。這即是多線程間的不可見性問題,官方話術爲: 線程 t1 修改後的值對線程 t2 來講並不可見。下圖能夠看到程序一直處於運行狀態:微信

這裏寫圖片描述

解決辦法是:對變量 ready 聲明爲 volatile,再次執行者段程序,可以順利打印出 「mainTread end」。volatile 保證了變量 ready 的可見性。多線程

這裏寫圖片描述

另外補充說明我這個例子用的 Java 版本:併發

這裏寫圖片描述 #volatile 之重排序問題說明 有序性:表示程序的執行順序按照代碼的前後順序執行。經過下面代碼,咱們將更加直觀的理解有序性。jvm

int a = 1;
int b = 2;
a = 3;     //語句A
b = 4;     //語句B

上面代碼,語句 A 必定在語句 B 以前執行嗎? 答案是否認的。由於這裏可能發生指令重排序。語句 B 可能先於語句 A 先自行。ide

什麼是指令重排序?處理器爲了提升運行效率,可能對輸入代碼進行優化,他不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是他會保證程序最終執行的結果和代碼順序執行的結果是一致的。測試

可是下面這種狀況,語句 B 必定在 語句 A 以後執行。優化

int a = 1;
int b = 2;
a = 3;     //語句A
b = a + 3;     //語句B

緣由是,變量 b 依賴 a 的值,重排序時處理器會考慮指令之間的依賴性。

固然,這個 volatile 有什麼關係呢? volatile 變量能夠必定程度上保證有序性,volatile 關鍵字禁止指令重排序。

//x、y爲非volatile變量
//flag爲volatile變量
	
x = 1;        //語句1
y = 2;        //語句2
flag = true;  //語句3
	
x = 3;         //語句4
y = 4;       //語句5

這裏要說明的是,flag 爲 volatile 變量;能保證

  1. 語句1,語句2 必定是在語句3的前面執行,但不保證語句1,語句2的執行順序。
  2. 語句4,語句5 必定是在語句3的後面執行,但不保證語句4,語句5的執行順序。
  3. 語句1,語句2 的執行結果,對語句3,語句4,語句5是可見的。

以上,就是關於 volatile 的禁止重排序的說明。、

#volatile 之非原子性問題驗證 volatile 關鍵字並不能保證原子性,如自增操做。下面看一個例子:

public class Volatile{
	
	private volatile int count = 0;
	
	public static void main(String[] args) {
		
		final Volatile v = new Volatile();
		
		for(int i = 0; i < 1000; i++) {
			new Thread(new Runnable() {
	
				@Override
				public void run() {
					// TODO Auto-generated method stub
					v.count++;
				}
				
			}).start();
		}
		
		while(Thread.activeCount() > 1)
			Thread.yield();
		
		System.out.println(v.count);
	}
	
}

這個程序執行的結果並無達到咱們的指望值,1000。而且每次的運行結果可能都不同,以下圖,有多是 997 等。

這裏寫圖片描述

來看下面一副圖,分解自增操做的步驟。

  1. read&load 從主內存複製變量到當前工做內存。
  2. use&assign 執行代碼,改變共享變量的值。
  3. store&write 用工做內存數據刷新主內存相關內容。

這裏寫圖片描述

可是,這一系列的操做並非原子的。也就是在 read&load 以後,若是主內存 count 發生變化,線程工做內存中的值因爲已經加載,不會產生對應的變化。因此計算出來的結果和咱們預期不同。

對於 volatile 修飾的變量,jvm 虛擬機只是保證從主內存加載到線程工做內存中的值是最新的。

因此,假如線程 A 和 B 在read&load 過程當中,發現主內存中的值都是5,那麼都會加載這個最新的值 5。線程 A 修改後寫到主內存,更新主內存的值爲6。線程 B 因爲已經 read & load,注意到此時線程 B 工做內存中的值仍是5, 因此修改後也會將6更新到主內存。

那麼兩個線程分別進行一次自增操做後,count 只增長了1,結果也就錯了。

固然,咱們能夠經過併發安全類AomicInteger, 內置鎖 sychronized,顯示鎖 ReentrantLock,來規避這個問題,讓程序運行結果達到咱們的指望值 1000.

1)採用併發安全類 AomicInteger 的方式:

import java.util.concurrent.atomic.AtomicInteger;
	
public class Volatile{
	
	private AtomicInteger  count = new AtomicInteger(0);
	
	public static void main(String[] args) {
		
		final Volatile v = new Volatile();
		
		for(int i = 0; i < 1000; i++) {
			new Thread(new Runnable() {
	
				@Override
				public void run() {
					// TODO Auto-generated method stub
					v.count.incrementAndGet();
				}
				
			}).start();
		}
		
		while(Thread.activeCount() > 1)
			Thread.yield();
		
		System.out.println(v.count);
	}
	
}

2) 採用內置鎖 synchronized 的方式:

public class Volatile{
	
	private int count = 0;
	
	public static void main(String[] args) {
		
		final Volatile v = new Volatile();
		
		for(int i = 0; i < 1000; i++) {
			new Thread(new Runnable() {

				@Override
				public void run() {
					// TODO Auto-generated method stub
					synchronized  (this) {
						v.count++;
					}
				}
				
			}).start();
		}
		
		while(Thread.activeCount() > 1)
			Thread.yield();
		
		System.out.println(v.count);
	}
	
}

3)採用顯示鎖的方式

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
	
public class Volatile{
	
	private int count = 0;
	
	Lock lock = new ReentrantLock();
	
	public static void main(String[] args) {
		
		final Volatile v = new Volatile();
		
		for(int i = 0; i < 1000; i++) {
			new Thread(new Runnable() {
	
				@Override
				public void run() {
					// TODO Auto-generated method stub
						v.lock.lock();
						try {
							v.count++;
						}finally {
							v.lock.unlock();
						}
				}
				
			}).start();
		}
		
		while(Thread.activeCount() > 1)
			Thread.yield();
		
		System.out.println(v.count);
	}
	
}

#參考

http://www.cnblogs.com/dolphin0520/p/3920373.html http://www.javashuo.com/article/p-plzfocdq-gv.html https://blog.csdn.net/xilove102/article/details/52437581

本文原創首發於微信公衆號 [ 林裏少年 ],歡迎關注第一時間獲取更新。 這裏寫圖片描述

相關文章
相關標籤/搜索