新聞中心
內(nèi)存可見(jiàn)性
說(shuō)到內(nèi)存可見(jiàn)性問(wèn)題就不得不提 Java 內(nèi)存模型,Java 內(nèi)存模型(Java Memory Model)簡(jiǎn)稱(chēng)為 JMM,主要是用來(lái)屏蔽不同硬件和操作系統(tǒng)的內(nèi)存訪(fǎng)問(wèn)差異的,因?yàn)樵诓煌挠布筒煌牟僮飨到y(tǒng)下,內(nèi)存的訪(fǎng)問(wèn)是有一定的差異得,這種差異會(huì)導(dǎo)致相同的代碼在不同的硬件和不同的操作系統(tǒng)下有著不一樣的行為,而 Java 內(nèi)存模型就是解決這個(gè)差異,統(tǒng)一相同代碼在不同硬件和不同操作系統(tǒng)下的差異的。

邊壩網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)建站!從網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開(kāi)發(fā)、APP開(kāi)發(fā)、自適應(yīng)網(wǎng)站建設(shè)等網(wǎng)站項(xiàng)目制作,到程序開(kāi)發(fā),運(yùn)營(yíng)維護(hù)。創(chuàng)新互聯(lián)建站從2013年開(kāi)始到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來(lái)保證我們的工作的順利進(jìn)行。專(zhuān)注于網(wǎng)站建設(shè)就選創(chuàng)新互聯(lián)建站。
Java 內(nèi)存模型規(guī)定:所有的變量(實(shí)例變量和靜態(tài)變量)都必須存儲(chǔ)在主內(nèi)存中,每個(gè)線(xiàn)程也會(huì)有自己的工作內(nèi)存,線(xiàn)程的工作內(nèi)存保存了該線(xiàn)程用到的變量和主內(nèi)存的副本拷貝,線(xiàn)程對(duì)變量的操作都在工作內(nèi)存中進(jìn)行。線(xiàn)程不能直接讀寫(xiě)主內(nèi)存中的變量,如下圖所示:
然而,Java 內(nèi)存模型會(huì)帶來(lái)一個(gè)新的問(wèn)題,那就是內(nèi)存可見(jiàn)性問(wèn)題,也就是當(dāng)某個(gè)線(xiàn)程修改了主內(nèi)存中共享變量的值之后,其他線(xiàn)程不能感知到此值被修改了,它會(huì)一直使用自己工作內(nèi)存中的“舊值”,這樣程序的執(zhí)行結(jié)果就不符合我們的預(yù)期了,這就是內(nèi)存可見(jiàn)性問(wèn)題,我們用以下代碼來(lái)演示一下這個(gè)問(wèn)題:
private static boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!flag) {
}
System.out.println("終止執(zhí)行");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("設(shè)置 flag=true");
flag = true;
}
});
t2.start();
}
以上代碼我們預(yù)期的結(jié)果是,在線(xiàn)程 1 執(zhí)行了 1s 之后,線(xiàn)程 2 將 flag 變量修改為 true,之后線(xiàn)程 1 終止執(zhí)行,然而,因?yàn)榫€(xiàn)程 1 感知不到 flag 變量發(fā)生了修改,也就是內(nèi)存可見(jiàn)性問(wèn)題,所以會(huì)導(dǎo)致線(xiàn)程 1 會(huì)永遠(yuǎn)的執(zhí)行下去,最終我們看到的結(jié)果是這樣的:
如何解決以上問(wèn)題呢?只需要給變量 flag 加上 volatile 修飾即可,具體的實(shí)現(xiàn)代碼如下:
private volatile static boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!flag) {
}
System.out.println("終止執(zhí)行");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("設(shè)置 flag=true");
flag = true;
}
});
t2.start();
}
以上程序的執(zhí)行結(jié)果如下圖所示:
禁止指令重排序
指令重排序是指編譯器或 CPU 為了優(yōu)化程序的執(zhí)行性能,而對(duì)指令進(jìn)行重新排序的一種手段。
指令重排序的實(shí)現(xiàn)初衷是好的,但是在多線(xiàn)程執(zhí)行中,如果執(zhí)行了指令重排序可能會(huì)導(dǎo)致程序執(zhí)行出錯(cuò)。指令重排序最典型的一個(gè)問(wèn)題就發(fā)生在單例模式中,比如以下問(wèn)題代碼:
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) { // ①
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // ②
}
}
}
return instance;
}
}以上問(wèn)題發(fā)生在代碼 ② 這一行“instance = new Singleton();”,這行代碼看似只是一個(gè)創(chuàng)建對(duì)象的過(guò)程,然而它的實(shí)際執(zhí)行卻分為以下 3 步:
- 創(chuàng)建內(nèi)存空間。
- 在內(nèi)存空間中初始化對(duì)象 Singleton。
- 將內(nèi)存地址賦值給 instance 對(duì)象(執(zhí)行了此步驟,instance 就不等于 null 了)。
如果此變量不加 volatile,那么線(xiàn)程 1 在執(zhí)行到上述代碼的第 ② 處時(shí)就可能會(huì)執(zhí)行指令重排序,將原本是 1、2、3 的執(zhí)行順序,重排為 1、3、2。但是特殊情況下,線(xiàn)程 1 在執(zhí)行完第 3 步之后,如果來(lái)了線(xiàn)程 2 執(zhí)行到上述代碼的第 ① 處,判斷 instance 對(duì)象已經(jīng)不為 null,但此時(shí)線(xiàn)程 1 還未將對(duì)象實(shí)例化完,那么線(xiàn)程 2 將會(huì)得到一個(gè)被實(shí)例化“一半”的對(duì)象,從而導(dǎo)致程序執(zhí)行出錯(cuò),這就是為什么要給私有變量添加 volatile 的原因了。
要使以上單例模式變?yōu)榫€(xiàn)程安全的程序,需要給 instance 變量添加 volatile 修飾,它的最終實(shí)現(xiàn)代碼如下:
public class Singleton {
private Singleton() {}
// 使用 volatile 禁止指令重排序
private static volatile Singleton instance = null; // 【主要是此行代碼發(fā)生了變化】
public static Singleton getInstance() {
if (instance == null) { // ①
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // ②
}
}
}
return instance;
}
}
總結(jié)
volatile 是 Java 并發(fā)編程的重要組成部分,它的主要作用有兩個(gè):保證內(nèi)存的可見(jiàn)性和禁止指令重排序。volatile 常使用在一寫(xiě)多讀的場(chǎng)景中,比如 CopyOnWriteArrayList 集合,它在操作的時(shí)候會(huì)把全部數(shù)據(jù)復(fù)制出來(lái)對(duì)寫(xiě)操作加鎖,修改完之后再使用 setArray 方法把此數(shù)組賦值為更新后的值,使用 volatile 可以使讀線(xiàn)程很快的告知到數(shù)組被修改,不會(huì)進(jìn)行指令重排,操作完成后就可以對(duì)其他線(xiàn)程可見(jiàn)了。
分享題目:面試突擊:Volatile有什么用?
URL標(biāo)題:http://fisionsoft.com.cn/article/coeddog.html


咨詢(xún)
建站咨詢(xún)
