柚子快報邀請碼778899分享:jvm 【JavaEE】
柚子快報邀請碼778899分享:jvm 【JavaEE】
目錄
1.?線程不安全問題
2. 線程不安全的原因
3. 解決線程不安全問題
1.?線程不安全問題
線程安全問題是多線程編程必須考慮的重要問題,也因為其難以理解與處理,故而程序員也嘗試發(fā)明更多的編程模型來處理并發(fā)編程,如多進程、多線程、actor、csp等等;
我們知道,操作系統(tǒng)調(diào)度線程是搶占式執(zhí)行,這樣的隨機性可能會導致程序執(zhí)行出現(xiàn)一些bug,如果由于這樣的調(diào)度的隨機性使得代碼出現(xiàn)了bug,則認為代碼是不安全的,如果沒有引入bug,則認為代碼是安全的;
線程不安全的典型案例:使用兩個線程對同一個整型變量進行自增操作,每個線程自增五萬次:
class Counter{
//保存兩個線程要自增的變量
public int count = 0;
public void increase(){
count++;
}
}
public class Demo1 {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for(int i=0;i<50000;i++){
counter.increase();
}
});
Thread t2 = new Thread(()->{
for(int i=0;i<50000;i++){
counter.increase();
}
});
t1.start();
t2.start();
//在main線程中打印兩個線程自增結(jié)束后得到的count結(jié)果
//t1、t2執(zhí)行結(jié)束后再打印count結(jié)果
t1.join();
t2.join();
System.out.println(counter.count);
}
}
運行三次,輸出結(jié)果與預期并不相符且多次運行多次不同:
? ??? ????
注:1.t1.join()與t2.join()誰先誰后均可:
線程是隨機調(diào)度的,t1、t2線程的結(jié)束前后是未知的,
如果t1先結(jié)束,則先令main線程等待t1結(jié)束,待t1結(jié)束后再令main線程等待t2結(jié)束;
如果t2先結(jié)束,仍先令main等待t1結(jié)束,t2結(jié)束了t1還未結(jié)束,main線程仍然在等待t1結(jié)束,等t1結(jié)束后,t2已經(jīng)結(jié)束了,則此時t2.jion()立即返回;
2.站在CPU角度來看,count++實際上是3個CPU指令:
第一步:將內(nèi)存中count值加載到CPU寄存器中;(load)
第二步:寄存器中的值將其+1;(add)
第三步:把寄存器中的值寫回到內(nèi)存的count中;(save)
由于搶占式執(zhí)行,兩個線程同時執(zhí)行這三個指令的時候順序上充滿了隨機性,只有當兩個線程的三條指令串型執(zhí)行的時候才會符合預期,只要三條指令出現(xiàn)交錯,就會出現(xiàn)錯誤,如:
2. 線程不安全的原因
(1)根本原因:線程是搶占式執(zhí)行,線程間的調(diào)度充滿隨機性;
(2)修改共享數(shù)據(jù):多個線程對同一個變量進行修改操作,才會導致線程不安全問題;
當多個線程分別對不同的多個變量進行操作,或是多個線程對同一個變量進行讀操作,都不會導致線程不安全問題;
(3)操作的原子性問題:針對變量的操作不是原子性的,就會導致線程不安全問題,如上文示例中,自增操作其實是3條指令;
當操作是原子性的,如讀取變量的值就只對應一條機器指令,就不會導致線程不安全問題;
(4)內(nèi)存可見性問題:java編譯器的優(yōu)化操作使得在某些情況下線程之間出現(xiàn)信息不同步問題:
如線程t1一直在高速循環(huán)進行讀操作,線程t2不定時進行修改操作,此時由于t1的高速訪問可能無果,就會停止將數(shù)據(jù)從內(nèi)存中讀至寄存器中再進行讀取,而直接從寄存器中讀取,此時若t2線程進行修改操作,就會由于內(nèi)存可見性問題而使兩個線程信息不同步,出現(xiàn)安全問題,示例代碼如下:
import java.util.Scanner;
public class Demo2 {
private static int isQuit = 0;
public static void main(String[] args) {
Thread t = new Thread(()->{
while(0 == isQuit){
}
System.out.println("Thread t has finished.");
});
t.start();
Scanner scanner = new Scanner(System.in);
System.out.println("Please input the value of isQuit: ");
isQuit = scanner.nextInt();
System.out.println("Thread main has finished.");
}
}
輸出結(jié)果為:
并未輸出"Thread t has finished."說明t線程并未結(jié)束;?
(5)指令重排序問題:指令重排序也是編譯器優(yōu)化的一種操作,編譯器在某些情況下可能調(diào)整代碼的先后順序來提高程序的效率,單線程通常不會出現(xiàn)問題,但在多線程代碼中,可能就會誤判導致線程安全問題;
3. 解決線程不安全問題
對應上文的線程不安全問題原因,思考解決線程不安全問題的方法:
(1)線程調(diào)度的隨機性問題:無法從代碼層面進行改進的;
(2)多線程修改同一變量問題:部分情況下可調(diào)整代碼結(jié)構(gòu),使不同線程操作不同變量;
(3)變量操作的原子性問題:加鎖操作將多個操作打包為一個原子性操作;
(4)內(nèi)存可見性問題:
① 使用synchronized關(guān)鍵字可以保證內(nèi)存可見性,被synchronied修飾的代碼塊,相當于手動禁止了編譯器的優(yōu)化;
② 使用volatile關(guān)鍵字可以保證內(nèi)存可見性,禁止編譯器做出上述優(yōu)化:
import java.util.Scanner;
public class Demo2 {
private static volatile int isQuit = 0;
public static void main(String[] args) {
Thread t = new Thread(()->{
while(0 == isQuit){
}
System.out.println("Thread t has finished.");
});
t.start();
Scanner scanner = new Scanner(System.in);
System.out.println("Please input the value of isQuit: ");
isQuit = scanner.nextInt();
System.out.println("Thread main has finished.");
}
}
此時輸出結(jié)果為:
??
(5)指令重排序問題:synchronized關(guān)鍵字可以禁止指令重排序;
注:synchronized解決多線程修改同一變量問題代碼示例:
使用鎖后,就將線程間亂序的并發(fā)變成了一個串型操作,并發(fā)性降低但會更安全;
雖然效率有所降低但相較于單線程程序,還是能分擔步驟壓力,效率還是較高的;
java中加鎖的方式有很多種,最常使用的是synchronized關(guān)鍵字:
class Counter{
public int count=0;
synchronized public void increase(){
count++;
}
}
public class Demo1 {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for(int i=0;i<50000;i++){
counter.increase();
}
});
Thread t2 = new Thread(()->{
for(int i=0;i<50000;i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
輸出結(jié)果為:
??
注:(1)在increase()方法前加上synchronized修飾,此時進入方法就會自動加鎖,離開方法就會自動解鎖;
(2)當給一個線程加鎖成功時,其他線程嘗試加鎖就會觸發(fā)阻塞等待,此時對應的線程就處于clocked狀態(tài);
(3)阻塞狀態(tài)會一直持續(xù)到占用鎖的線程解鎖為止,時間軸縮略圖如下:
?(3)synchronized可以保證操作的原子性,保證內(nèi)存可見性,還可以禁止指令重排序;
柚子快報邀請碼778899分享:jvm 【JavaEE】
精彩文章
本文內(nèi)容根據(jù)網(wǎng)絡資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。