??博客主頁(yè):?【小扳_-CSDN博客】
?感謝大家點(diǎn)贊??收藏?評(píng)論?
文章目錄
? ? ? ? 1.0 指令重排序概述
? ? ? ? 1.1 指令重排序主要分為兩種類型
? ? ? ? 1.2 指令重排序所引發(fā)的問題
? ? ? ? 2.0 內(nèi)存可見性概述
? ? ? ? 2.1 導(dǎo)致內(nèi)存可見性問題主要涉及兩個(gè)方面
? ? ? ? 2.2 解決內(nèi)存可見性問題
? ? ? ? 2.2.1 使用 volatile 關(guān)鍵字
? ? ? ? 2.2.2 使用 synchronized 關(guān)鍵字
? ? ? ? 3.0 線程的等待通知機(jī)制概述
? ? ? ? 3.1 等待 - wait()
? ? ? ? 3.2 通知 - notity()
? ? ? ? 3.3 通知所有 - notifyAll()
? ? ? ? 1.0 指令重排序概述
????????指令重排序是指編譯器或處理器為了提高性能,在不改變程序執(zhí)行結(jié)果的前提下,可以對(duì)指令序列進(jìn)行重新排序的優(yōu)化技術(shù)。這種優(yōu)化技術(shù)可以使得計(jì)算機(jī)在執(zhí)行指令時(shí)更高效地利用計(jì)算資源,提高程序的執(zhí)行效率。
? ? ? ? 1.1 指令重排序主要分為兩種類型
? ? ? ? 1)編譯器重排序:編譯器在生成目標(biāo)代碼時(shí)會(huì)對(duì)源代碼中的指令進(jìn)行優(yōu)化和重排,以提高程序的執(zhí)行效率。編譯器重排序時(shí)在編譯階段完成的,目的是生成更高效率的機(jī)器代碼。
? ? ? ? 2)處理器重排序:處理器在執(zhí)行指令也可以對(duì)指令進(jìn)行重排序,以最大程度地利用處理器的流水線和多核等特性。目的提高指令的執(zhí)行效率。
? ? ? ? 1.2 指令重排序所引發(fā)的問題
? ? ? ? 雖然指令重排序可以提高程序的執(zhí)行效率但是在多線程編程中可能會(huì)引發(fā)內(nèi)存可見性問題。由于指令重排序可能導(dǎo)致共享變量的讀寫順序與代碼中的順序不一致,當(dāng)多個(gè)線程同時(shí)訪問共享變量時(shí),可能會(huì)出現(xiàn)數(shù)據(jù)不一致的情況。
? ? ? ? 2.0 內(nèi)存可見性概述
????????在多線程編程中,由于線程之間的執(zhí)行是并發(fā)的,每個(gè)線程有自己的工作內(nèi)存,共享變量存儲(chǔ)在主內(nèi)存中,線程在執(zhí)行過(guò)程中會(huì)將共享變量從主內(nèi)存中拷貝到自己的工作內(nèi)存中進(jìn)行操作,操作完成后再將結(jié)果寫回主內(nèi)存。這里的工作內(nèi)存指的是:寄存器或者是緩存。
? ? ? ? 2.1 導(dǎo)致內(nèi)存可見性問題主要涉及兩個(gè)方面
? ? ? ? 1)多線程并發(fā)操作搶占式執(zhí)行導(dǎo)致內(nèi)存可見性:如果一個(gè)現(xiàn)車給修改了共享變量的值,但其他線程無(wú)法立即看到這個(gè)修改之后的共享變量,就會(huì)導(dǎo)致數(shù)據(jù)不一致的情況。
? ? ? ? 2)指令重排序?qū)е聝?nèi)存可見性:由于編譯器和處理器可以對(duì)指令進(jìn)行重排序優(yōu)化,可能會(huì)導(dǎo)致共享變量的讀寫順序與代碼中的順序不一致,從而影響了線程對(duì)共享變量的可見性。
代碼如下:
public class demo1 { public static int count = 0; public static void main(String[] args) { Thread t1 = new Thread(()->{ while (count == 0){ }; System.out.println("線程 t1 結(jié)束"); }); Thread t2 = new Thread(()->{ Scanner scanner = new Scanner(System.in); System.out.println("輸出:"); count = scanner.nextInt(); }); t1.start(); t2.start(); } }
? ? ? ? t1 在啟動(dòng)線程之后,只要 count == 0 這個(gè)條件滿足時(shí),就會(huì)進(jìn)入循環(huán);t2 啟動(dòng)線程要求輸出一個(gè)值并且將該值賦值給 count 。
????????預(yù)想過(guò)程:只要輸出一個(gè)非 0 的值時(shí),那么 count 不為 0 了,t1 線程中的循環(huán)就會(huì)退出,因此會(huì)輸出 ”線程 t1 結(jié)束“ 這句話。最后程序結(jié)束。
運(yùn)行結(jié)果:
? ? ? ? 輸出 1 之后,按理來(lái)說(shuō),count 此時(shí)應(yīng)該賦值為 1 了,那么 t1 中的循環(huán)應(yīng)該要結(jié)束了并且得輸出一段話。但是,看到結(jié)果,即使輸出了 1 之后,t1 還在循環(huán)中。
原因如下:
? ? ? ? 由于 t1 循環(huán)中的代碼塊里面是沒有任何代碼,無(wú)需任何操作,在 CPU 中主要執(zhí)行兩條指令:load 將內(nèi)存中的 count 加載到寄存器中;cmp 將 count 與 0 之間進(jìn)行比較。
????????因?yàn)?cpm 執(zhí)行這條指令直接在寄存器中操作,而 load 需要將內(nèi)存的數(shù)據(jù)加載到寄存器中,這個(gè)操作的速度就比 cmp 的速度慢很多很多了。所以編譯器重排序在生成目標(biāo)代碼時(shí)對(duì)源代碼中的指令進(jìn)行優(yōu)化重排,將 count 變量存儲(chǔ)到寄存器或者緩存中,目的為了提高執(zhí)行效率。然而,t2 線程對(duì) count 進(jìn)行重新賦值后,將重新賦值后的 count 寫回到主存中,但是 t1 線程是沒有看到重新賦值后的 count 變量。因?yàn)閷?duì)于 t1 線程來(lái)說(shuō),count 變量已經(jīng)”固定“在工作內(nèi)存中,沒有重新加載主存中的 count 變量,而是反復(fù)讀取自己工作內(nèi)存中的 count == 0 這個(gè)變量。
? ? ? ? 總而言之,指令重排序?qū)е铝藘?nèi)存可見性問題。
? ? ? ? 2.2 解決內(nèi)存可見性問題
? ? ? ? 主要有兩個(gè)方法:使用 volatile 關(guān)鍵字、使用 synchronized 關(guān)鍵字。
? ? ? ? 2.2.1 使用 volatile 關(guān)鍵字
? ? ? ? volatile 關(guān)鍵字可以確保被修飾的變量對(duì)所有線程可見,禁止指令重排序。
代碼如下:
當(dāng)給 count 加上 volatile 關(guān)鍵時(shí),編譯器或者處理器就不會(huì)對(duì)指令重排序了
import java.util.Scanner; public class demo1 { public static volatile int count = 0; public static void main(String[] args) { Thread t1 = new Thread(()->{ while (count == 0){ }; System.out.println("線程 t1 結(jié)束"); }); Thread t2 = new Thread(()->{ Scanner scanner = new Scanner(System.in); System.out.println("輸出:"); count = scanner.nextInt(); }); t1.start(); t2.start(); } }
運(yùn)行結(jié)果:
? ? ? ? 當(dāng)輸出 1 回車之后,count 就會(huì)重新賦值為 1 。從而 t1 中的循環(huán)退出,輸出打印之后,整個(gè)進(jìn)程就結(jié)束了。
? ? ? ? 2.2.2 使用 synchronized 關(guān)鍵字
? ? ? ? 可以確保同一時(shí)刻只有一個(gè)線程可以訪問共享變量,同時(shí)保證了線程間的數(shù)據(jù)一致性。
代碼如下:
import java.util.Scanner; public class demo1 { public static int count = 0; public static void main(String[] args) { Object o = new Object(); Thread t1 = new Thread(()->{ synchronized (o){ System.out.println("線程 t1 開始"); while (count == 0){ try { o.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } }; System.out.println("線程 t1 結(jié)束"); } }); Thread t2 = new Thread(()->{ System.out.println("輸出:"); Scanner scanner = new Scanner(System.in); synchronized (o){ count = scanner.nextInt(); o.notify(); } }); t1.start(); t2.start(); } }
運(yùn)行結(jié)果:
????????t1 線程在進(jìn)入循環(huán)前會(huì)先獲取對(duì)象 o 的鎖,并在循環(huán)體中通過(guò) o.wait() 釋放鎖并等待喚醒。當(dāng) t2 線程修改了 count 的值后,會(huì)再次獲取對(duì)象 o 的鎖并調(diào)用 o.notify() 喚醒 t1 線程,從而解除等待狀態(tài),保證了內(nèi)存可見性和線程間的通信。
????????
? ? ? ? 3.0 線程的等待通知機(jī)制概述
????????線程的等待通知機(jī)制是多線程編程中常用的一種同步機(jī)制,用于實(shí)現(xiàn)線程間的協(xié)作和通信。
? ? ? ? 3.1 等待 - wait()
? ? ? ? 線程調(diào)用對(duì)象的 wait() 方法時(shí),會(huì)釋放對(duì)象的鎖并且同時(shí)進(jìn)入等待狀態(tài),直到其他線程調(diào)用相同對(duì)象的 notify() 或者 notifyAll() 方法來(lái)喚醒它。在等待的過(guò)程中,線程會(huì)一直處于阻塞狀態(tài)。?
? ? ? ? 3.2 通知 - notity()
? ? ? ? 線程調(diào)用對(duì)象的 notify() 方法時(shí),會(huì)喚醒等待在該對(duì)象上的一個(gè)線程,若有多個(gè)等待喚醒的線程時(shí),具體喚醒的線程是不確定的,使其從等待狀態(tài)轉(zhuǎn)為就緒狀態(tài),被喚醒的線程會(huì)嘗試重新獲取對(duì)象的鎖,并繼續(xù)執(zhí)行。
? ? ? ? 3.3 通知所有 - notifyAll()
? ? ? ? 線程調(diào)用對(duì)象的 notifyAll() 方法時(shí),會(huì)喚醒所有等待在該對(duì)象上的線程,使它們從等待狀態(tài)轉(zhuǎn)為就緒狀態(tài)。被喚醒的線程會(huì)競(jìng)爭(zhēng)對(duì)象的鎖,只有一個(gè)線程能夠獲取鎖并繼續(xù)執(zhí)行,其他線程會(huì)再次進(jìn)入等待狀態(tài)。
舉個(gè)例子:
public class demo2 { public static void main(String[] args) throws InterruptedException { Object lock = new Object(); Thread t1 = new Thread(()->{ synchronized (lock){ try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("正在執(zhí)行 t1 線程"); } }); Thread t2 = new Thread(()->{ synchronized (lock){ try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("正在執(zhí)行 t2 線程"); } }); Thread t3 = new Thread(()->{ synchronized (lock){ lock.notify(); lock.notify(); System.out.println("正在執(zhí)行 t3 線程"); } }); t1.start(); t2.start(); Thread.sleep(1000); t3.start(); } }
? ? ? ? t1 ,t2 線程都在阻塞狀態(tài),等待 t3 線程通知,但是 t3 線程還沒釋放鎖,所以 t1 ,t2 線程繼續(xù)阻塞狀態(tài)。直到 t3 線程釋放鎖之后,t1,t2 線程就可以競(jìng)爭(zhēng)獲取鎖,假設(shè) t1 獲取鎖之后,執(zhí)行完代碼,釋放鎖,t1 線程結(jié)束。再到 t2 線程獲取鎖,執(zhí)行完代碼釋放鎖,t2 線程也結(jié)束。因此線程的先后順序:t3 線程一定是最早結(jié)束的,接著到 t1 或者 t2 線程隨機(jī)其中的一個(gè)線程。
運(yùn)行結(jié)果:
補(bǔ)充:
????????等待通知機(jī)制通常需要搭配 synchronized 關(guān)鍵字來(lái)確保線程安全。在Java中, wait()、notiyf() 和 notiyfAll() 方法必須在同步代碼塊或同步方法中調(diào)用,即在獲取對(duì)象鎖的情況下使用,以避免出現(xiàn)并發(fā)訪問的問題。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-851794.html
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-851794.html
到了這里,關(guān)于JavaEE 初階篇-深入了解多線程安全問題(指令重排序、解決內(nèi)存可見性與等待通知機(jī)制)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!