「 天行健,君子以自強不息。地勢坤,君子以厚德載物。」———《易經》
volatile 變量,在上一篇文章中已經有簡單提及相關概念和用法,這一篇主要對 Volatile 變量的特性進行源碼驗證。驗證它的涉及到的三個特性:
- 可見性
- 指令重排序
- 非原子性
volatile 之可見性驗證
上一篇文章中,講到 volatile 變量通常被當做狀态标記使用。其中典型的應用是,檢查标記狀态,以确定是否退出循環。下面我們直接舉個反例,源碼如下:
public class Volatile {
boolean ready=true; //volatile 狀态标志變量
private final static int SIZE = ; //建立10個對象,可改變
public static void main(String[] args) throws InterruptedException{
Volatile vs[]=new Volatile[SIZE];
for(int n=;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 之重排序問題說明
有序性:表示程式的執行順序按照代碼的先後順序執行。通過下面代碼,我們将更加直覺的了解有序性。
int a = ;
int b = ;
a = ; //語句A
b = ; //語句B
上面代碼,語句 A 一定在語句 B 之前執行嗎? 答案是否定的。因為這裡可能發生指令重排序。語句 B 可能先于語句 A 先自行。
什麼是指令重排序?處理器為了提高運作效率,可能對輸入代碼進行優化,他不保證程式中各個語句的執行先後順序同代碼中的順序一緻,但是他會保證程式最終執行的結果和代碼順序執行的結果是一緻的。
但是下面這種情況,語句 B 一定在 語句 A 之後執行。
int a = ;
int b = ;
a = ; //語句A
b = a + ; //語句B
原因是,變量 b 依賴 a 的值,重排序時處理器會考慮指令之間的依賴性。
當然,這個 volatile 有什麼關系呢?
volatile 變量可以一定程度上保證有序性,volatile 關鍵字禁止指令重排序。
//x、y為非volatile變量
//flag為volatile變量
x = ; //語句1
y = ; //語句2
flag = true; //語句3
x = ; //語句4
y = ; //語句5
這裡要說明的是,flag 為 volatile 變量;能保證
- 語句1,語句2 一定是在語句3的前面執行,但不保證語句1,語句2的執行順序。
- 語句4,語句5 一定是在語句3的後面執行,但不保證語句4,語句5的執行順序。
- 語句1,語句2 的執行結果,對語句3,語句4,語句5是可見的。
以上,就是關于 volatile 的禁止重排序的說明。、
volatile 之非原子性問題驗證
volatile 關鍵字并不能保證原子性,如自增操作。下面看一個例子:
public class Volatile{
private volatile int count = ;
public static void main(String[] args) {
final Volatile v = new Volatile();
for(int i = ; i < ; i++) {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
v.count++;
}
}).start();
}
while(Thread.activeCount() > )
Thread.yield();
System.out.println(v.count);
}
}
這個程式執行的結果并沒有達到我們的期望值,1000。并且每次的運作結果可能都不一樣,如下圖,有可能是 997 等。
來看下面一副圖,分解自增操作的步驟。
- read&load 從主記憶體複制變量到目前工作記憶體。
- use&assign 執行代碼,改變共享變量的值。
- 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();
public static void main(String[] args) {
final Volatile v = new Volatile();
for(int i = ; i < ; i++) {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
v.count.incrementAndGet();
}
}).start();
}
while(Thread.activeCount() > )
Thread.yield();
System.out.println(v.count);
}
}
2) 采用内置鎖 synchronized 的方式:
public class Volatile{
private int count = ;
public static void main(String[] args) {
final Volatile v = new Volatile();
for(int i = ; i < ; i++) {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
synchronized (this) {
v.count++;
}
}
}).start();
}
while(Thread.activeCount() > )
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 = ;
Lock lock = new ReentrantLock();
public static void main(String[] args) {
final Volatile v = new Volatile();
for(int i = ; i < ; 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() > )
Thread.yield();
System.out.println(v.count);
}
}
參考
http://www.cnblogs.com/dolphin0520/p/3920373.html
https://blog.csdn.net/gao_chun/article/details/45095995
https://blog.csdn.net/xilove102/article/details/52437581
本文原創首發于微信公衆号 [ 林裡少年 ],歡迎關注第一時間擷取更新。