咱們都知道,隨着祖國愈來愈繁榮昌盛,隨着科技的進步,設備的更新換代,計算機體系結構、操做系統、編譯程序都在不斷地改革創新,但始終有一點是不變的(我對鴨血粉絲的熱愛忠貞不渝):那就是下面三者的性能耗時:CPU < 內存 < I/Ohtml
但也正由於這些改變,也就在併發程序中出現了一些詭異的問題,而其中最昭著的三大問題就是:可見性、有序性、原子性。java
今天咱們就主要來學習一下三者中的可見性。編程
可見性 的定義是:一個線程對共享變量的修改,另一個線程可以馬上看到。緩存
在單核時代,全部線程都在一個CPU上執行,因此一個線程的寫,必定是對其它線程可見的。就比如,一個總經理下面就一個項目負責人。安全
此時,項目經理查看到任務G後,分配給員工A和員工B,那麼這個任務的進度就能隨時掌握在項目經理手中了;每一個員工都能從項目經理處得知最新的項目進度。bash
而在多核時代後,每一個CPU都有本身的緩存,這就出現了可見性問題。併發
此時,兩個項目經理同時查看到任務G後,各自分配給本身下屬員工,那麼這個任務的進度就只能掌握在各自項目經理手中了,由於全部員工的工做進度並非彙報給同一個項目經理;那麼,每一個員工只能得知本身項目組員工的工做進度,並不能得知其餘項目組的工做進度。因此,當多個項目經理在作同一個任務時,就可能出現任務配比不均、任務進度拖延、任務重複進行等多種問題。app
總結上面的例子來說,就是由於進度的不及時更新,致使數據不是最新,致使決策失誤。因此,咱們隱約能夠看出,內存並不直接與Cpu打交道,而是經過高速緩存與Cpu打交道。ide
cpu <——> 高速緩存 <———> 內存
複製代碼
經過一張圖片來表示就是(多核):函數
下文咱們的闡述,若無特殊說明,都是基於多核的。
可見性問題都是由Cpu緩存不一致爲併發編程帶來,而其中的主要有下面三種狀況:
線程交叉執行多數狀況是因爲線程切換致使的,例以下圖中的線程A在執行過程當中切換到線程B執行完成後,再切換回線程A執行剩下的操做;此時線程B對變量的修改不能對線程A當即可見,這就致使了計算結果和理想結果不一致的狀況。
例以下面這段代碼
int a = 0; //行1
int b = 0; //行2
a = b + 10; //行3
b = a + 9; //行4
複製代碼
若是行1和行2在編譯的時候改變順序,執行結果不會受到影響;
若是將行3和行4在變異的時候交換順序,執行結果就會受到影響,由於b的值得不到預期的19;
由圖知:因爲編譯時改變了執行順序,致使結果不一致;而兩個線程的交叉執行又致使線程改變後的結果也不是預期值,簡直雪上加霜!
由於主線程對共享變量的修改沒有及時更新,子線程中不能當即獲得最新值,致使程序不能按照預期結果執行。
例以下面這段代碼:
package com.itquan.service.share.resources.controller;
import java.time.LocalDateTime;
/**
* @author :mmzsblog
* @description:共享變量在線程間的可見性測試
*/
public class VisibilityDemo {
// 狀態標識flag
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
System.out.println(LocalDateTime.now() + "主線程啓動計數子線程");
new CountThread().start();
Thread.sleep(1000);
// 設置flag爲false,使上面啓動的子線程跳出while循環,結束運行
VisibilityDemo.flag = false;
System.out.println(LocalDateTime.now() + "主線程將狀態標識flag被置爲false了");
}
static class CountThread extends Thread {
@Override
public void run() {
System.out.println(LocalDateTime.now() + "計數子線程start計數");
int i = 0;
while (VisibilityDemo.flag) {
i++;
}
System.out.println(LocalDateTime.now() + "計數子線程end計數,運行結束:i的值是" + i);
}
}
}
複製代碼
運行結果是:
從控制檯的打印結果能夠看出,由於主線程對flag的修改,對計數子線程沒有當即可見,因此致使了計數子線程久久不能跳出while循環,結束子線程。
對於這種狀況,固然不能忍,因此就引出了下一個問題:如何解決線程間不可見性
爲了保證線程間可見性咱們通常有3種選擇:
volatile
關鍵字能保證可見性,但也只能保證可見性,在此處就能保證flag的修改能當即被計數子線程獲取到。
此時糾正上面例子出現的問題,只需在定義全局變量的時候加上volatile
關鍵字
// 狀態標識flag
private static volatile boolean flag = true;
複製代碼
將標識狀態flag在定義的時候使用Atomic相關類來進行定義的話,就能很好的保證flag屬性的可見性以及原子性。
此時糾正上面例子出現的問題,只需在定義全局變量的時候將變量定義成Atomic相關類
// 狀態標識flag
private static AtomicBoolean flag = new AtomicBoolean(true);
複製代碼
不過值得注意的一點是,此時原子類相關的方法設置新值和獲得值的放的是有點變化,以下:
// 設置flag的值
VisibilityDemo.flag.set(false);
// 獲取flag的值
VisibilityDemo.flag.get()
複製代碼
此處咱們使用的是Java常見的synchronized關鍵字。
此時糾正上面例子出現的問題,只需在爲計數操做i++
添加synchronized
關鍵字修飾
synchronized (this) {
i++;
}
複製代碼
經過上面三種方式,咱們都能獲得相似以下的指望結果:
然而,接下來咱們要對其中的volatile
和synchronized
關鍵字作一番較爲詳細的解釋。歡迎關注公衆號"Java學習之道",查看更多幹貨!
Java內存模型對volatile關鍵字定義了一些特殊的訪問規則,當一個變量被volatile修飾後,它將具有兩種特性,或者說volatile具備下列兩層語義:
針對第一點,volatile保證了不一樣線程對這個變量進行讀取時的可見性,具體表現爲:
附一張CPU緩存模型圖:
綜上所述:就是用volatile修飾的變量,對這個變量的讀寫,不能使用 CPU 緩存,必須從內存中讀取或者寫入。
使用volatile沒法保障線程安全,那麼volatile的做用是什麼呢?
其中之一:(對狀態量進行標記,保證其它線程看到的狀態量是最新值)
volatile關鍵字是Java虛擬機提供的最輕量級的同步機制,不少人因爲對它理解不夠(其實這裏你想理解透的話能夠看看happens-before原則),而每每更願意使用synchronized來作同步。
synchronized關鍵字的做用域有二種:
1)是某個對象實例內,synchronized aMethod(){}
能夠防止多個線程同時訪問這個對象的synchronized方法。
若是一個對象有多個synchronized方法,只要一個線程訪問了其中的一個synchronized方法,其它線程不能同時訪問這個對象中任何一個synchronized方法。
這時,不一樣的對象實例的synchronized方法是不相干擾的。也就是說,其它線程照樣能夠同時訪問相同類的另外一個對象實例中的synchronized方法。
由於當修飾非靜態方法的時候,鎖定的是當前實例對象。
2)是某個類的範圍,synchronized static aStaticMethod{}
防止多個線程同時訪問這個類中的synchronized static 方法。它能夠對類的全部對象實例起做用。
由於當修飾靜態方法的時候,鎖定的是當前類的 Class 對象。
除了方法前用synchronized關鍵字,synchronized關鍵字還能夠用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。
用法是:
synchronized(this){
/*區塊*/
}
複製代碼
它的做用域是當前對象;
synchronized關鍵字是不能繼承的,也就是說,基類的方法
synchronized f(){
// 具體操做
}
複製代碼
在繼承類中並不自動是
synchronized f(){
// 具體操做
}
複製代碼
而是變成了
f(){
// 具體操做
}
複製代碼
繼承類須要你顯式的指定它的某個方法爲synchronized方法;
綜上3點所述:synchronized關鍵字主要有如下這3種用法:
這三種用法就基本保證了共享變量在讀取的時候,讀取到的是最新的值。
線程解鎖前,必須把共享變量的最新值刷新到主內存
線程加鎖時,將清空工做內存中共享變量的值,從而是使用共享變量時,須要從主內存中從新讀取最新的值(注意:加鎖與解鎖是同一把鎖)
從上面的這兩條規則也能夠看出,這種方式保證了內存中的共享變量必定是最新值。
但咱們在使用synchronized保證可見性的時候也要注意如下幾點:
以上內容就是我對並法中的可見性的一點理解與總結了,下期咱們接着敘述併發中的有序性。
歡迎關注公衆號:Java學習之道
我的博客網站:www.mmzsblog.cn