4. 多線程帶來的的風(fēng)險(xiǎn)-線程安全 (重點(diǎn))
4.1 觀察線程不安全
static class Counter {
public int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
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);
}
大家觀察下是否適用多線程的現(xiàn)象是否一致?同時(shí)嘗試思考下為什么會(huì)有這樣的現(xiàn)象發(fā)生呢?
原因是 1.load 2. add 3. save
注意:可能會(huì)導(dǎo)致 小于5w
4.2 線程安全的概念
想給出一個(gè)線程安全的確切定義是復(fù)雜的,但我們可以這樣認(rèn)為:
如果多線程環(huán)境下代碼運(yùn)行的結(jié)果是符合我們預(yù)期的,即在單線程環(huán)境應(yīng)該的結(jié)果,則說這個(gè)程序是線程安全的
4.3 線程不安全的原因
★1. 修改共享數(shù)據(jù)(多個(gè)線程修改同一個(gè)變量)
上面的線程不安全的代碼中, 涉及到多個(gè)線程針對(duì) counter.count 變量進(jìn)行修改.
此時(shí)這個(gè) counter.count 是一個(gè)多個(gè)線程都能訪問到的 “共享數(shù)據(jù)”
counter.count 這個(gè)變量就是在堆上. 因此可以被多個(gè)線程共享訪問
★2. 操作不是原子性
什么是原子性
我們把一段代碼想象成一個(gè)房間,每個(gè)線程就是要進(jìn)入這個(gè)房間的人。如果沒有任何機(jī)制保證,A進(jìn)入房間之后,還沒有出來;B 是不是也可以進(jìn)入房間,打斷 A 在房間里的隱私。這個(gè)就是不具備原子性的。
那我們應(yīng)該如何解決這個(gè)問題呢?是不是只要給房間加一把鎖,A 進(jìn)去就把門鎖上,其他人是不是就進(jìn)不來了。這樣就保證了這段代碼的原子性了。
有時(shí)也把這個(gè)現(xiàn)象叫做同步互斥,表示操作是互相排斥的
一條 java 語(yǔ)句不一定是原子的,也不一定只是一條指令
比如剛才我們看到的 n++,其實(shí)是由三步操作組成的:
- 從內(nèi)存把數(shù)據(jù)讀到 CPU
- 進(jìn)行數(shù)據(jù)更新
- 把數(shù)據(jù)寫回到 CPU
不保證原子性會(huì)給多線程帶來什么問題
如果一個(gè)線程正在對(duì)一個(gè)變量操作,中途其他線程插入進(jìn)來了,如果這個(gè)操作被打斷了,結(jié)果就可能是錯(cuò)誤的。
這點(diǎn)也和線程的搶占式調(diào)度密切相關(guān). 如果線程不是 “搶占” 的, 就算沒有原子性, 也問題不大.
★3. 內(nèi)存可見性
可見性指, 一個(gè)線程對(duì)共享變量值的修改,能夠及時(shí)地被其他線程看到
Java 內(nèi)存模型 (JMM): Java虛擬機(jī)規(guī)范中定義了Java內(nèi)存模型.
目的是屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的并發(fā)效果.
- 線程之間的共享變量存在 主內(nèi)存 (Main Memory).
- 每一個(gè)線程都有自己的 “工作內(nèi)存” (Working Memory) .
- 當(dāng)線程要讀取一個(gè)共享變量的時(shí)候, 會(huì)先把變量從主內(nèi)存拷貝到工作內(nèi)存, 再?gòu)墓ぷ鲀?nèi)存讀取數(shù)據(jù).
- 當(dāng)線程要修改一個(gè)共享變量的時(shí)候, 也會(huì)先修改工作內(nèi)存中的副本, 再同步回主內(nèi)存
由于每個(gè)線程有自己的工作內(nèi)存, 這些工作內(nèi)存中的內(nèi)容相當(dāng)于同一個(gè)共享變量的 “副本”. 此時(shí)修改線程1 的工作內(nèi)存中的值, 線程2 的工作內(nèi)存不一定會(huì)及時(shí)變化
- 初始情況下, 兩個(gè)線程的工作內(nèi)存內(nèi)容一致
![]()
- 一旦線程1 修改了 a 的值, 此時(shí)主內(nèi)存不一定能及時(shí)同步. 對(duì)應(yīng)的線程2 的工作內(nèi)存的 a 的值也不一定能及時(shí)同步.
這個(gè)時(shí)候代碼中就容易出現(xiàn)問題
此時(shí)引入了兩個(gè)問題:
- 為啥要整這么多內(nèi)存?
- 為啥要這么麻煩的拷來拷去?
- 為啥整這么多內(nèi)存?
實(shí)際并沒有這么多 “內(nèi)存”. 這只是 Java 規(guī)范中的一個(gè)術(shù)語(yǔ), 是屬于 “抽象” 的叫法.
所謂的 “主內(nèi)存” 才是真正硬件角度的 “內(nèi)存”. 而所謂的 “工作內(nèi)存”, 則是指 CPU 的寄存器和高速緩存.- 為啥要這么麻煩的拷來拷去?
因?yàn)?CPU 訪問自身寄存器的速度以及高速緩存的速度, 遠(yuǎn)遠(yuǎn)超過訪問內(nèi)存的速度(快了 3 - 4 個(gè)數(shù)量級(jí), 也就是幾千倍, 上萬(wàn)倍)比如某個(gè)代碼中要連續(xù) 10 次讀取某個(gè)變量的值, 如果 10 次都從內(nèi)存讀, 速度是很慢的. 但是如果
只是第一次從內(nèi)存讀, 讀到的結(jié)果緩存到 CPU 的某個(gè)寄存器中, 那么后 9 次讀數(shù)據(jù)就不必直接訪問
內(nèi)存了. 效率就大大提高了那么接下來問題又來了, 既然訪問寄存器速度這么快, 還要內(nèi)存干啥??
答案就是一個(gè)字: 貴值的一提的是, 快和慢都是相對(duì)的. CPU 訪問寄存器速度遠(yuǎn)遠(yuǎn)快于內(nèi)存, 但是內(nèi)存的訪問速度又遠(yuǎn)遠(yuǎn)快于硬盤.
對(duì)應(yīng)的, CPU 的價(jià)格最貴, 內(nèi)存次之, 硬盤最便宜
★4. 代碼順序性
什么是代碼重排序
一段代碼是這樣的:
- 去前臺(tái)取下 U 盤
- 去教室寫 10 分鐘作業(yè)
- 去前臺(tái)取下快遞
如果是在單線程情況下,JVM、CPU指令集會(huì)對(duì)其進(jìn)行優(yōu)化,比如,按 1->3->2的方式執(zhí)行,也是沒問題,可以少跑一次前臺(tái)。這種叫做指令重排序
編譯器對(duì)于指令重排序的前提是 “保持邏輯不發(fā)生變化”. 這一點(diǎn)在單線程環(huán)境下比較容易判斷, 但是在多線程環(huán)境下就沒那么容易了, 多線程的代碼執(zhí)行復(fù)雜程度更高, 編譯器很難在編譯階段對(duì)代碼的執(zhí)行效果進(jìn)行預(yù)測(cè), 因此激進(jìn)的重排序很容易導(dǎo)致優(yōu)化后的邏輯和之前不等價(jià)
重排序是一個(gè)比較復(fù)雜的話題, 涉及到 CPU 以及編譯器的一些底層工作原理, 此處不做過多討論文章來源:http://www.zghlxwxcb.cn/news/detail-539834.html
4.4 解決之前的線程不安全問題
這里用到的機(jī)制,我們馬上會(huì)給大家解釋文章來源地址http://www.zghlxwxcb.cn/news/detail-539834.html
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
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);
}
到了這里,關(guān)于【javaEE面試題(四)線程不安全的原因】【1. 修改共享數(shù)據(jù) 2. 操作不是原子性 3. 內(nèi)存可見性 4. 代碼順序性】的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!