【Java并發】記憶體可見性
從以下四個方面講解記憶體可見性問題
- 共享變量線上程之間的可見性
- synchronized實作可見性
- volatile實作可見性
指令重排序
as-if-serial語義
volatile使用注意事項
- synchronized和volatile比較
共享變量線上程之間的可見性
可見性:一個線程對共享變量值的修改,能夠及時地被其他線程看到
共享變量:如果一個變量在多個線程的工作記憶體中都存在副本,那麼這個變量就是這幾個線程的共享變量
Java記憶體模型(Java Memory Model, JMM)
Java記憶體模型,描述了Java程式中各種變量(線程共享變量)的通路規則,以及在JMM中将變量存儲到記憶體和從記憶體中
讀取出變量這樣的底層細節。
所有的變量都存儲在主記憶體中
每個線程都有自己獨立的工作記憶體,裡面儲存該線程使用到的變量的副本(主記憶體中該變量的一份拷貝)
線程1 線程2 線程3
------- ------- -------
工作記憶體1(X的副本1) 工作記憶體2(X的副本2) 工作記憶體3(X的副本3)
------------- ------------------------------------- -----
主記憶體(共享變量X)
兩條規定:
(1)線程對共享變量的所有操作都必須在自己的工作記憶體中進行,不能直接從主記憶體中讀寫(線程不能與主記憶體直接進行互動)
(2)不同線程之間無法直接通路其他線程工作記憶體中的變量,線程之間變量值的傳遞需要通過主記憶體來完成。(記憶體總線)
共享變量可見性實作的原理
線程1對共享變量的修改要想被線程2及時看到,必須要經過如下兩個步驟:
(1) 把工作記憶體1中更新過的共享變量重新整理到主記憶體中
(2) 将主記憶體中最新的共享變量的值更新到工作記憶體2中
synchronized實作可見性
要實作共享變量的可見性,必須保證兩點:
(1) 線程修改後的共享變量值能夠及時從工作記憶體重新整理到主記憶體中
(2) 其他線程能夠及時把共享變量的最新值從主記憶體更新到自己的工作記憶體中
可見性的實作方法
Java語言層面支援的可見性實作方式:
synchronized
volatile
synchronized實作可見性
synchronized能夠實作:
原子性(同步)
可見性
JMM關于synchronized的兩條規定:
(1)線程解鎖前,必須把共享變量的最新值重新整理到主記憶體中
(2)線程加鎖時,将清空工作記憶體中共享變量的值,進而使用共享變量時需要從主記憶體中重新讀取最新的值(注意:加鎖與解鎖需要是同一把鎖)
線程解鎖前對共享變量的修改在下次加鎖時對其他線程可見
線程執行互斥代碼的過程:
(1) 獲得互斥鎖
(2) 清空工作記憶體
(3) 從主記憶體拷貝變量的最新副本到工作記憶體
(4) 執行代碼
(5) 将更改後的共享變量的值重新整理到主記憶體
(6) 釋放互斥鎖
指令重排序:
重排序:代碼書寫的順序與實際執行的順序不同,指令重排序是編譯器或處理器為了提供程式性能而做的優化
(1) 編譯器優化的重排序(編譯器優化)
(2) 指令級并行重排序(處理器優化)
(3) 記憶體系統的重排序(處理器優化)
as-if-serial語義:無論如何重排序,程式執行的結果應該與代碼順序執行的結果一緻
(Java編譯器、運作時和處理器都會保證Java在單線程下遵循as-if-serial語義)
as-if-serial例子
int num1 = ; // 1
int num2 = ; // 2
int sum = num1 + num2; // 3
單線程:第1、2行的額順序可以重排序,但第3行不能
重排序不會給單線程帶來記憶體可見性問題
多線程中程式交錯執行時,重排序可能會造成記憶體可見性問題
隻有資料依賴關系,才會禁止重排序。
public class SynchronizedDemo {
//共享變量
private boolean ready = false;
private int result = ;
private int number = ;
//寫操作
public void write(){
ready = true; //1.1
number = ; //1.2
}
//讀操作
public void read(){
if(ready){ //2.1
result = number*; //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();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//啟動線程執行讀操作
synDemo.new ReadWriteThread(false).start();
}
}
if(ready)
result = number * ;
可以重排序為
int mid = number * ;
if (read) result = mid;
導緻共享變量線上程間不可見的原因:
(1) 線程的交叉執行
(2) 重排序結合線程交叉執行
(3) 共享變量更新後的值沒有在工作記憶體與主記憶體間及時更新
不可見的原因:
(1) 線程的交叉執行
(2) 重排序結合線程交叉執行
(3) 共享變量更新後的值沒有在工作記憶體與主記憶體間及時更新
synchronized針對(1)(2)(3)解決方案是原子性、原子性、可見性
volatile實作可見性
指令重排序
as-if-serial語義
volatile使用注意事項
volatile關鍵字
能夠保證volatile變量的可見性
不能保證volatile變量符合操作的原子性
volatile如何實作記憶體可見性:
深入來說:通過加入記憶體屏障和禁止重排序優化來實作的。
對volatile變量執行寫操作時,處理器會在寫操作後加入一條store屏障指令
(它會将cpu寫緩存器的緩存強制重新整理到主記憶體中,主記憶體中存放的就是最新的值,同時它還能防止處理器把volatile前面的變量重排序到volatile寫操作之後)
對volatile變量執行讀操作時,會在讀操作前加入一條load屏障指令
(它會使緩存器中的緩存失效,是以讀volatile變量時,需要從主記憶體中讀取它的最新值,同時它還能起到禁止指令重排序的效果。)
Java記憶體模型中一共定義了8種指令,來完成主記憶體和工作記憶體之間的互動操作。
通俗地講: volatile變量在每次被線程通路時,都強迫從主記憶體中重讀該變量的值,而當該變量發生變化時,
又會強迫線程将最新的值重新整理到主記憶體。這樣任何時刻,不同的線程總能看到該變量的最新值。
線程寫volatile變量的過程:
(1) 改變線程工作記憶體中 volatile 變量副本的值
(2) 将改變後的副本的值從工作記憶體重新整理到主記憶體
線程讀volatile變量的過程:
(1) 從主記憶體中讀取volatile變量的最新值到線程的工作記憶體中
(2) 從工作記憶體中讀取volatile變量的副本
volatile不能保證volatile變量複合操作的原子性:
private int number = ;
number ++; // 不是原子操作,它包括三個操作:讀取number的值;将number的值加1;寫入最新的number的值
synchronized(this){
number ++; // 加入synchronized,變為原子操作
}
private volatile int number = ;
變為volatile變量,無法保證原子性。它隻能保證可見性,就是及時更新主記憶體中變量和工作記憶體變量副本的值。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class VolatileDemo {
private Lock lock = new ReentrantLock();
private int number = ;
public int getNumber(){
return this.number;
}
public void increase(){
try {
Thread.sleep();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
lock.lock();
try {
this.number++; // 被加鎖的代碼必須放入到try塊裡面
} finally {
lock.unlock(); // 在finally 中解鎖,保證鎖一定能被釋放
}
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
final VolatileDemo volDemo = new VolatileDemo();
for(int i = ; i < ; i++){
new Thread(new Runnable() {
@Override
public void run() {
volDemo.increase();
}
}).start();
}
//如果還有子線程在運作,主線程就讓出CPU資源,
//直到所有的子線程都運作完了,主線程再繼續往下執行
while(Thread.activeCount() > ){
Thread.yield();
}
System.out.println("number : " + volDemo.getNumber());
}
}
保證number自增操作的原子性:
使用synchronized關鍵字
使用ReentrantLock(java.util.concurrent.locks, j.u.c)
使用AtomicInteger(java.util.concurrent.atomic)
volatile使用場合
要在多線程中安全的使用volatile變量,必須同時滿足:
(1) 對變量的寫入操作不依賴其目前值
不滿足:number ++、count = count * 5等
滿足:boolean變量、記錄溫度變化的變量等
(2) 該變量沒有包含在具有其他變量的不變式中
也就是,如果程式中包含多個volatile變量,那麼每一個volatile變量都要獨立于其他的volatile變量。
不滿足:不變式 low < up
而大多數程式都跟這兩點中的一點或者兩點有沖突,是以它沒有synchronized使用廣泛。
synchronized和volatile比較
(1) volatile不需要加鎖,比synchronized更輕量級,不會阻塞線程;(效率更高)
(2) 從記憶體可見性角度講,volatile讀相當于加鎖,volatile寫相當于解鎖;
(3) synchronized既能保證可見性,又能保證原子性,而volatile隻能保證可見性,無法保證原子性。
總結
什麼是記憶體可見性
Java記憶體模型(JMM)
實作可見性的方式:synchronized和volatile
final也可以保證記憶體可見性
synchronized和volatile實作記憶體可見性的原理
synchronized實作可見性
指令重排序
as-if-serial語義
volatile實作可見性
volatile能夠保證可見性
volatile不能保證原子性
volatile使用注意事項
問:即使沒有保證可見性的措施,很多時候共享變量依然能夠在主記憶體和工作記憶體見得到及時的更新?
答:一般隻有在短時間内高并發的情況下才會出現變量得不到及時更新的情況,
因為CPU在執行時會很快地重新整理緩存,是以一般情況下很難看到這種問題。
對64位(long/double)變量的讀寫可能不是原子操作:
Java記憶體模型允許JVM将沒有被volatile修飾的64位資料類型的讀寫操作劃分為兩次32位的讀寫操作來進行
導緻問題:有可能會出現讀取到”半個變量”的情況
解決方法:加volatile關鍵字
synchronized和volatile比較
volatile比synchronized更輕量級
volatile沒有synchronized使用的廣泛
聲明:該部落格是根據慕課《細說Java多線程之記憶體可見性》的筆記整理而成,在此感謝本講的講師MartonZhang。