17、volatile
volatile是不錯的機制,但是也不能保證原子性。
17.1. volatile 可見性
代碼驗證可見性
package com.interview.concurrent.volatiles;
import java.util.concurrent.TimeUnit;
/**
* @description:測試volatile的可見性
* @author yangxj
* @date 2020/2/26 17:54
*/
public class VolatileVisibility {
// volatile 讀取的時候去主內存中讀取在最新值!
private volatile static int num = 0;
public static void main(String[] args) throws InterruptedException { // Main線程
new Thread(()->{
/**
* @description:
* 由於num添加了volatile,所以線程每次讀取都會去主內存中讀取
* @author yangxj
* @date 2020/2/26 17:54
*/
while (num==0){
}
}).start();
TimeUnit.SECONDS.sleep(1);
num = 1;
System.out.println(num);
}
}
17.2. volatile 不保證原子性
驗證 volatile 不保證原子性
17.2.1. volatile 不保證原子性的原因分析
原子性:ACID 不可分割!完整,要麼同時失敗,要麼同時成功!
package com.interview.concurrent.volatiles;
/**
* @author yangxj
* @description 描述:驗證volatile的原子性
* @date 2020/2/26 17:55
*/
public class VolatileAcid {
private volatile static int num = 0;
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
add(); // 20 * 1000 = 20000
}
},String.valueOf(i)).start();
}
// main線程等待上面執行完成,判斷線程存活數 2
while (Thread.activeCount()>2){ // main gc
//線程放棄當前分得的 CPU 時間,但是不使線程阻塞
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
public static void add(){
num++;
}
}
運行效果如下:
運行效果並不是我們預期中的20000,這是什麼原因呢?
因爲numm++ 不是原子操作,而volatile也不保證原子性操作。
分析VolatileAcid類在堆中運行的字節碼
使用命令查看類運行的字節碼
javap -c xxx.class
num++被拆分成三部分,在這三者之間又會有其他進程進來讀取原始值,做加1操作。怎麼解決呢?看下文分解
17.2.2. volatile 不保證原子性解決方案
解決方案:
1、使用synchronized:前面已經說過;
2、使用原子性類工具java.util.concurrent.atomic
使用atomic解決原子性問題,示例代碼如下:
package com.interview.concurrent.volatiles;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author yangxj
* @description 描述:使用atomic解決原子性
* @date 2020/2/26 18:29
*/
public class AtomicIntegerAcid {
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
num.getAndIncrement(); // 等價於 num++
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
add(); // 20 * 1000 = 20000
}
},String.valueOf(i)).start();
}
// main線程等待上面執行完成,判斷線程存活數 2
while (Thread.activeCount()>2){ // main gc
//線程放棄當前分得的 CPU 時間,但是不使線程阻塞
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
}
17.3. volatile 禁止 指令重排講解
17.3.1. 什麼是指令重排
計算機在執行程序之後,爲了提高性能,編譯器和處理器會進行指令重排!
處理在指令重排的時候必須要考慮數據之間的依賴性!
指令重排:程序最終執行的代碼,不一定是按照你寫的順序來的!
int x = 11; // 語句1
int y = 12; // 語句2
x = y + 5; // 語句3
y = x*x ; // 語句4
//怎麼執行
1234
2134
1324
// 請問語句4 能在語句3前面執行嗎? 能
加深 : int x,y,a,b = 0;
線程1 | 線程2 |
---|---|
x = a; | y = b; |
b = 1; | a = 2; |
x = 0, y = 0 |
假設編譯器進行了指令重排,就會出現如下效果!
線程1 | 線程2 |
---|---|
b = 1; | a = 2; |
x = a; | y =b; |
x =2,y=1 |
package com.interview.concurrent.volatiles;
/**
* @author yangxj
* @description 描述:指令重排
* 兩個線程交替執行的!
* @date 2020/2/26 18:38
*/
public class InstructionReset {
public static void main(String[] args) {
InstructionWare instructionWare = new InstructionWare();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
instructionWare.m1();
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
instructionWare.m2();
}
},"B").start();
}
}
class InstructionWare{
int a = 0;
boolean flag = false;
public void m1(){ // A
flag = true; // 語句2
a = 1; // 語句1
}
public void m2(){ // B
if (flag){
a = a + 5; // 語句3
System.out.println("m2=>"+a);
}
}
}
由於有指令重排的問題,語句3可能在語句1先執行,這樣就導致最終結果a = 5而不是6!
volatite: 能實現禁止指令重排!
17.3.2. volatile實現禁止指令重排原理:內存屏障。
volatile實現禁止指令重排原理:內存屏障。
內存屏障: 作用於CPU的指令,主要作用兩個:
1、 保證特定的操作執行順序;
2、保證某些變量的內存可見性。
禁止指令重排,能保證線程的安全性
語句1先執行,這樣就導致最終結果a = 5而不是6!
volatite: 能實現禁止指令重排!