深入理解并發編程中的三個問題
1.可見性
可見性(Visibility):是指一個線程對共享變量進行修改,另一個先立即得到修改后的最新值。
1.1 可見性案例演示
一個線程根據boolean類型的標記flag, while循環,另一個線程改變這個flag變量的值,另 一個線程并不會停止循環。
public class VisibilityTest {
// 多個線程都會訪問的數據,我們稱為線程的共享數據
// 定義一個靜態的 boolean 變量 run,初始值為 true
private static boolean run = true;
public static void main(String[] args) throws InterruptedException {
// 創建并啟動線程 t1
Thread t1 = new Thread(() -> {
// 在 run 變量為 true 時循環輸出消息
while (run) {
System.out.println(Thread.currentThread().getName() + "線程1 run = " + run);
}
// 循環結束后輸出最終消息
System.out.println(Thread.currentThread().getName() + "線程1 run = " + run);
});
t1.start(); // 啟動線程 t1
Thread.sleep(1000); // 主線程睡眠1秒鐘
// 創建并啟動線程 t2
Thread t2 = new Thread(() -> {
run = false; // 將 run 變量設置為 false
System.out.println(Thread.currentThread().getName() + "時間到,線程2設置為false");
});
t2.start(); // 啟動線程 t2
}
}
輸出結果:
Thread-0線程1 run = true
Thread-0線程1 run = true
// .....結果省略
Thread-1時間到,線程2設置為false
Thread-0線程1 run = false
線程1開始運行時run=true,所以會不斷循環輸出Thread-0線程1 run = true,線程2運行后設置run=false,線程1發現run=true后就停止輸出。
總結:并發編程時,會出現可見性問題,當一個線程對共享變量進行了修改,另外的線程并沒有立即看到修改后的最新值。
2.原子性
原子性(Atomicity):在一次或多次操作中,要么所有的操作都執行并且不會受其他因素干擾而中斷,要么所有的操作都不執行。
2.1 可見性案例演示
5個線程各執行1000次 i++
public class AtomicityTest {
private static int number = 0; // 定義一個靜態的整數變量 number,初始值為 0
public static void main(String[] args) throws InterruptedException {
Runnable increment = () -> {
// 定義一個 Runnable 匿名類 increment,用于對 number 進行累加操作
for (int i = 0; i < 1000; i++) {
number++; // 對 number 進行累加操作
}
};
ArrayList<Thread> ts = new ArrayList<>(); // 創建一個 ArrayList 用于存儲線程對象
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment); // 創建一個新線程,傳入 increment Runnable 實例
t.start(); // 啟動線程
ts.add(t); // 將線程對象添加到 ArrayList 中
}
for (Thread t : ts) {
t.join(); // 等待所有子線程執行完畢
}
System.out.println("number = " + number); // 輸出最終的 number 值
}
}
思考:最終number結果可能是多少?
結果可能是5000,也可能是小于5000的結果都有可能。
對于 number++ 而言(number 為靜態變量),實際會產生如下的 JVM 字節碼指令:
9: getstatic #12 // Field number:I
12: iconst_1
13: iadd
14: putstatic #12 // Field number:I
可見number++是由多條語句組成,以上多條指令在一個線程的情況下是不會出問題的,但是在多線程情況下就可能會出現問題。比如一個線程在執行13: iadd時,另一個線程又執行9: getstatic。會導致兩次number++,實際上只加了1。
總結:并發編程時,會出現原子性問題,當一個線程對共享變量操作到一半時,另外的線程也有可能來操作共享變量,干擾了前一個線程的操作。
3.有序性
有序性(Ordering):是指程序中代碼的執行順序,Java在編譯時和運行時會對代碼進行優化,會導致程序最終的執行順序不一定就是我們編寫代碼時的順序。
3.1. 有序性可見性案例演示
有序性
jcstress是Java并發壓測工具。
pom文件,添加依賴。
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>0.5</version>
</dependency>
代碼:
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class OrderingTest {
int num = 0;
boolean ready = false;
// 線程1執行的代碼
@Actor
public void actor1(I_Result r) {
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 線程2執行的代碼
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}
I_Result 是一個對象,有一個屬性 r1 用來保存結果。
思考:在多線程情況下可能出現幾種結果?
- 情況1:線程1先執行actor1,這時ready = false,所以進入else分支結果為1。
- 情況2:線程2執行到actor2,執行了num = 2;和ready = true,線程1執行,這回進入 if 分支,結果為 4。
- 情況3:線程2先執行actor2,只執行num = 2;但沒來得及執行 ready = true,線程1執行,還是進入else分支,結果為1。
注意:還有第四種情況,結果為0。
- 情況4:線程2java編譯后結果
ready = true;
num = 2;
線程2先執行actor2,執行了ready = true,但沒來得及執行執行num = 2,線程1執行,還是進入if分支,結果為0。
運行測試:
mvn clean install
java -jar target/jcstress.jar
部分jcstress測試結果,0結果出現1384次。
完整pox.xml:
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javac.target>1.8</javac.target>
<uberjar.name>jcstress</uberjar.name>
</properties>
<dependencies>
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>0.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<compilerVersion>${javac.target}</compilerVersion>
<source>${javac.target}</source>
<target>${javac.target}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.2</version>
<executions>
<execution>
<id>main</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>${uberjar.name}</finalName>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jcstress.Main</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/TestList</resource>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>