多執行緒的原子性,可見性,有序性
java記憶體模型定義了主存,工作記憶體等這些抽象概念,底層對應著cpu暫存器,快取,cpu指令優化等。
由此引出了 原子性,可見性,有序性
一、原子性
保證指令不會受到上下文切換的影響而產生指令交錯,鎖就是用來解決這個問題的
二、可見性
為了保證指令不會受cpu快取的影響
2.1 現象描述和解釋
先看一個例子
private final static Logger LOGGER = LoggerFactory.getLogger(Test8.class); static boolean flag=true; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() { @Override public void run() { while (flag){ } } }); t1.start(); Thread.sleep(1000); LOGGER.info("改變標記"); flag=false; }
上邊的程式碼t1執行緒當flag=true時會一直迴圈,主執行緒1s後改變flag,按預想的t1應該會結束,實際上t1執行緒不會結束,
這就是可見性問題。
java記憶體模型中有主記憶體,每個執行緒都有自己的工作記憶體,當一個變數被頻繁讀取時,jit編譯器會將flag的值快取到工作記憶體中的快取記憶體中,後邊從快取中讀取。這樣當主執行緒修改了flag並更新到主記憶體後t1執行緒還是從快取記憶體獲取flag,所以一直不能停止
總結下來就是一個執行緒對主記憶體的資料進行了修改,但對另外一個執行緒不可見
2.2 解決辦法
(1) 給共享的變數加一個關鍵字volatile
,表示容易變化的,這樣對這個執行緒的讀取就不會走快取,一直從工作記憶體獲取。
static volatile boolean flag=true;
(2) synchronized也可以解決可見性問題
獲取共享變數值的時候先加鎖,這樣也能保證可見性
public class Test8 { private final static Logger LOGGER = LoggerFactory.getLogger(Test8.class); final static Object lock = new Object(); static boolean flag=true; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() { @Override public void run() { while (true){ synchronized (lock){ if(!flag){ break; } } } } }); t1.start(); Thread.sleep(1000); LOGGER.info("改變標記"); synchronized (lock){ flag=false; } } }
這種解決方式下需要注意對 flag
變數的所有操作都要放在synchronized塊中
2.3 簡單應用
兩階段終止模式可以使用執行緒的interrupt方法和打斷標記來實現,這種方式需要特殊處理InterruptedException
異常,在異常處理中重新設定打斷標記,否則就不能正常停止。
也可以使用volatile關鍵字來實現,這種方式就不需要特殊處理InterruptedException
異常了
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Monitor monitor = new Monitor();
monitor.start();
// 2秒後主執行緒中執行停止
Thread.sleep(3000);
monitor.stop();
}
}
//建設器類,有一個執行緒一直在監控
class Monitor{
static Logger logger = LoggerFactory.getLogger(Monitor.class);
Thread t;
//控制是否停止的標記
private volatile boolean isStop=false;
//開始監控的方法
public void start(){
t = new Thread(new Runnable() {
@Override
public void run() {
while (true){
Thread current =Thread.currentThread();
// 因為isStop被volatile修飾了,所以其他執行緒的修改可以感知到,
// 這樣就可以用來控制執行緒是否結束
if(isStop){
//被打斷了
logger.debug("要結束了,執行結束前的操作");
break;
}
try {
//每隔一秒執行一次監控邏輯
Thread.sleep(1000);
logger.debug("執行監控邏輯");
} catch (InterruptedException e) {
e.printStackTrace();
//這種方式不需要在異常中進行特殊處理
}
}
}
},"t1");
t.start();
}
//停止監視器的方法
public void stop(){
isStop=true;
}
}
2.4 volatile解決可見性問題的原理
volatile的原理是基於記憶體屏障,
在讀取被volatile修飾的變數時會在讀取指令之前加入讀屏障,
在寫入被volatile修飾的變數時會在寫指令之後加入寫屏障,
讀屏障會保證屏障之後對volatile變數的讀取都從主記憶體中讀,寫屏障會保證屏障之前對volatile變數的修改都會重新整理到主記憶體中。
三、有序性
保證指令不會受cpu指令並行優化(指令重排)的影響
2.1 問題描述
jit編譯器會在不影響最終結果的前提下調整指令的執行順序,在多執行緒環境下可能就會出現一些問題。
例如建立物件時,在java層面看到的是一句程式碼
User user = new User()
在位元組碼指令層面會對應著幾個步驟
(1)建立例項,(2)執行構造方法(3)暴露引用
而 2和3的順序是有可能被調整的,這樣在多執行緒環境下如果這個user是個共享變數,當前執行緒有可能先執行了3那麼其他執行緒就有可能拿到一個不完整的物件。
2.2 解決辦法
變數用volatile關鍵字修飾
2.3 volatile解析有序性問題原理
在讀取被volatile修飾的變數時會在讀取指令之前加入讀屏障,
在寫入被volatile修飾的變數時會在寫指令之後加入寫屏障,寫屏障會保證之前的指令不進行指令重排。
2.4 有序性應用 多執行緒單例模式
public class App {
private App(){}
//volatile關鍵字禁止指令重排
private volatile static App app;
public static App getInstance(){
if(app == null) {
//只有第一次建立物件時才會進入同步塊並加鎖
synchronized (App.class){
//防止多個執行緒同時進入了第一個if
if(app == null){
app = new App();
}
}
}
return app;
}
}