話不多說(shuō),先上一張圖
沒(méi)錯(cuò)侠姑,我們今天聊的東西创橄,跟他沒(méi)啥關(guān)系。
上面這是java的內(nèi)存結(jié)構(gòu)(我就是忽悠你們來(lái)的)莽红。
今兒主要先聊一聊java的內(nèi)存模型(嗯妥畏,也不是非想跟你們聊,主要是標(biāo)題得從這玩意兒來(lái)引出)安吁。
但是也不能干聊不是醉蚁?想起兄弟們?cè)?jīng)對(duì)我靈魂的拷問(wèn)(無(wú)圖言屌),所以我就又從網(wǎng)上盜了一張圖鬼店。
來(lái)來(lái)來(lái)网棍,看圖說(shuō)話:
1.java中所有的變量都是存在主內(nèi)存里的。
2.各自的線程在工作的時(shí)候會(huì)自己拿到一塊工作內(nèi)存妇智。里面保存了該線程用到的變量的副本滥玷。
3.線程對(duì)變量的操作氏身,都是對(duì)自己工作內(nèi)存中變量的操作,不能操作主內(nèi)存罗捎。
4.最終工作內(nèi)存中的數(shù)據(jù)是需要同步回主內(nèi)存观谦,來(lái)完成主內(nèi)存中變量的更新的。
舉個(gè)例子吧:
就好比主內(nèi)存就是大哥桨菜,線程就是小弟豁状,大哥每天督促小弟們干活,小弟們彼此不怎么交流倒得,每天就是干了活泻红,然后自己找個(gè)本記下來(lái),找時(shí)間告訴大哥霞掺。
那么咱們來(lái)看下面的這一個(gè)場(chǎng)景:
大哥說(shuō)讓仨小弟進(jìn)點(diǎn)兒貨谊路。讓一個(gè)小弟聯(lián)系貨源,一個(gè)小弟聯(lián)系運(yùn)輸公司菩彬,一個(gè)小弟聯(lián)系裝卸工缠劝。小弟1去聯(lián)系貨源了,聯(lián)系好了告訴大哥骗灶,“貨源聯(lián)系好了”惨恭。這時(shí)候小弟2來(lái)了,一聽(tīng)貨源好了耙旦,就去聯(lián)系運(yùn)輸公司了脱羡。運(yùn)輸公司聯(lián)系好了,小弟2還沒(méi)回去告訴大哥呢免都,小弟3跑過(guò)去問(wèn)大哥了锉罐,一聽(tīng)大哥說(shuō)貨源好了,也跑出去聯(lián)系運(yùn)輸公司了绕娘。結(jié)果來(lái)了倆運(yùn)輸公司脓规,對(duì)著成堆的貨物干瞪眼。
這里面有幾個(gè)問(wèn)題呢业舍?
1.小弟2忙活清了抖拦,應(yīng)該打個(gè)電話趕緊通知大哥,他找到運(yùn)輸公司了舷暮。
2.大哥知道小弟2出去忙活事情了态罪,小弟3這時(shí)候來(lái)了,大哥應(yīng)該別讓小弟3著急下面,等小弟2干完了复颈,小弟3再上。
3.小弟2要是告訴大哥,他出去找運(yùn)輸公司了耗啦,大哥怎么還會(huì)讓小弟3接著去找運(yùn)輸公司呢凿菩?
如果在這個(gè)場(chǎng)景里,我們會(huì)說(shuō)帜讲,這不一群智障么衅谷,這種錯(cuò)誤都能發(fā)生,辦事不過(guò)腦子的么似将?获黔!
是的,java就是這么的智障在验。
其實(shí)呢玷氏,上面闡述的三個(gè)問(wèn)題,也就是我們java內(nèi)存模型在設(shè)計(jì)的時(shí)候腋舌,所圍繞的三個(gè)問(wèn)題:
可見(jiàn)性盏触,有序性和原子性。(終于聊到重點(diǎn)了朋友們?榻取)
下面要上定義了赞辩!
可見(jiàn)性:一個(gè)線程對(duì)共享變量做了修改之后,其他的線程立即能夠看到(感知到)該變量這種修改(變化)授艰。
Java內(nèi)存模型是通過(guò)將在工作內(nèi)存中的變量修改后的值同步到主內(nèi)存诗宣,在讀取變量前從主內(nèi)存刷新最新值到工作內(nèi)存中,這種依賴(lài)主內(nèi)存的方式來(lái)實(shí)現(xiàn)可見(jiàn)性的想诅。
有序性:在本線程內(nèi)觀察,操作都是有序的岛心;如果在一個(gè)線程中觀察另外一個(gè)線程来破,所有的操作都是無(wú)序的。
java內(nèi)存模型所保證的是忘古,同線程內(nèi)徘禁,所有的操作都是由上到下的,但是多個(gè)線程并行的情況下髓堪,則不能保證其操作的有序性送朱。
原子性:一個(gè)操作不能被打斷,要么全部執(zhí)行完畢干旁,要么不執(zhí)行驶沼。
基本數(shù)據(jù)類(lèi)型的訪問(wèn)都是原子性的(默認(rèn)64位,32位的機(jī)器對(duì)long争群、double這種占8個(gè)字節(jié)的是非原子性的)回怜,而針對(duì)非原子性的數(shù)據(jù),多線程的訪問(wèn)則是不安全的换薄。
以上是java內(nèi)存模型中玉雾,單線程針對(duì)這三種問(wèn)題作出的最基本的控制翔试,但是并發(fā)編程的場(chǎng)景中, 多線程的出現(xiàn)會(huì)導(dǎo)致這三個(gè)問(wèn)題頻頻發(fā)生复旬。
那么聰明的你一定會(huì)問(wèn)了垦缅,我們應(yīng)該如何去控制呢?(不問(wèn)的都拉出去彈雞兒)
一個(gè)一個(gè)來(lái):
可見(jiàn)性:
volatile關(guān)鍵字:通過(guò)volatile關(guān)鍵字修飾內(nèi)存中的變量驹碍,該變量在線程之間共享
其實(shí)對(duì)于可見(jiàn)性而言壁涎,無(wú)論是普通變量還是volatile變量都是如此,區(qū)別在于:volatile的特殊規(guī)則保證了volatile變量值修改后的新值立刻同步到主內(nèi)存幸冻,每次使用volatile變量前立即從主內(nèi)存中刷新粹庞,因此volatile保證了多線程之間的操作變量的可見(jiàn)性,而普通變量則不能保證這一點(diǎn)洽损。
但是volatile只是對(duì)關(guān)鍵字進(jìn)行了修飾庞溜,保證了其可見(jiàn)性,而對(duì)于線程的有序性和變量的原子性碑定,volatile根本沒(méi)有什么卵用流码,也就是什么意思呢?
private static volatile int volatileCount = 0;
private static void volatileCount() {
for (int i = 0; i < 10; i++) {
Executors.newFixedThreadPool(3).execute(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("volatile count: " + ++volatileCount);
});
}
}
代碼中的volatile延刘,加與不加漫试,沒(méi)多少區(qū)別。
因?yàn)関olatile只能保證其變量的可見(jiàn)性碘赖。但是每個(gè)線程啥時(shí)候開(kāi)始跑驾荣,我們根本不知道。當(dāng)A線程訪問(wèn)到volatileCount = 1時(shí)普泡,并且開(kāi)始執(zhí)行時(shí)播掷,如果B線程此時(shí)也訪問(wèn)到了,他也一樣會(huì)執(zhí)行撼班,雖然讀取volatileCount時(shí)歧匈,我們都能保證他是最新的,但是我們不能保證砰嘁,當(dāng)A線程在操作的時(shí)候件炉,B線程不能進(jìn)去插一腳。
原子性:
java.util.concurrent.atomic矮湘,原子類(lèi)了解一下斟冕?
在java中,我們知道++操作實(shí)際上并不是線程安全的缅阳,為了保證線程間的變量原子性宫静,java引入了atomic類(lèi)。它的作用就是保證,使用的變量孤里,一定是原子性的伏伯。
代碼咱看一下:
private static AtomicInteger atomicCount = new AtomicInteger(0);
private static void atomicCount() {
for (int i = 0; i < 10; i++) {
Executors.newFixedThreadPool(3).execute(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//atomicCount.incrementAndGet()方法的意思是讓其自增 1,等同于++
System.out.println("atomic count: " + atomicCount.incrementAndGet());
});
}
}
// atomic count: 5
// atomic count: 1
// atomic count: 3
// atomic count: 2
// atomic count: 8
// atomic count: 10
// atomic count: 4
// atomic count: 6
// atomic count: 7
// atomic count: 9
結(jié)果也放在下面了捌袜,可見(jiàn)说搅,我們保證了它的唯一性,他是一定會(huì)自增到10的虏等。說(shuō)白了弄唧,就是緩存鎖機(jī)制。
關(guān)于緩存鎖的機(jī)制霍衫,下面有一個(gè)官方解釋?zhuān)梢詫W(xué)習(xí)一下:
緩存鎖是指通過(guò)鎖住CPU緩存候引,在CPU緩存區(qū)實(shí)現(xiàn)共享變量的原子性操作。
如果緩存在處理器的緩存行中敦跌,內(nèi)存區(qū)域在LOCK操作期間被鎖定澄干,當(dāng)它執(zhí)行鎖操作,回寫(xiě)主內(nèi)存時(shí)柠傍,處理器不在總線鎖上聲言LOCK#信號(hào)麸俘,而是修改內(nèi)部?jī)?nèi)存地址,并允許它的緩存一致性機(jī)制來(lái)保證操作的原子性惧笛。因?yàn)榫彺嬉恢滦詸C(jī)制會(huì)阻止同時(shí)修改被兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù)从媚,當(dāng)其他處理器回寫(xiě)已被鎖定的緩存行的數(shù)據(jù)時(shí)會(huì)起緩存行無(wú)效。
緩存鎖使用的是比較并交換策略(Compare And Swap簡(jiǎn)稱(chēng)CAS)患整,CAS操作需要輸入兩個(gè)數(shù)值拜效,一個(gè)舊值(期望操作前的值)和一個(gè)新值,在操作期間先比較下舊值有沒(méi)有發(fā)生變化各谚,如果沒(méi)有發(fā)生變化拂檩,才交換成新值,發(fā)生了變化則不交換嘲碧。
這段話也解釋了為什么雖然我們不能保證其有序性,但是父阻,我們依然能正常的自增到10愈涩。
有序性:
synchronized,日常用的最多的東西加矛,當(dāng)使用synchronized關(guān)鍵字時(shí)履婉,只能有一個(gè)線程執(zhí)行直到執(zhí)行完成后或異常,才會(huì)釋放鎖斟览。所以可以保證synchronized代碼塊或方法只會(huì)有一個(gè)線程執(zhí)行毁腿,保障了程序的有序性。
private static void synchronizedCount() {
for (int i = 0; i < 10; i++) {
Executors.newFixedThreadPool(3).execute(->() {
@Override
public void run() {
synchronized (MyClass.class) { // 通過(guò)synchronized關(guān)鍵字來(lái)保證線程之間的有序性
System.out.println("synchronized count: " + ++synchronizedCount);
}
}
});
}
}
結(jié)果看上去很美好,有序已烤,且從1到10鸠窗,一個(gè)不差。
但是胯究,synchronized能不能保證原子性和可見(jiàn)性呢稍计?還記得單例的DCL寫(xiě)法么?
public class Singleton{
private static volatile Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(instance==null){
synchronized(Singleton.class){
if(instance==null){
instance=new Singleton();
}
}
}
return instance;
}
}
如果不加volatile會(huì)有什么問(wèn)題呢裕循?
java內(nèi)存模型(jmm)并不限制處理器重排序臣嚣,在執(zhí)行instance=new Singleton();時(shí)剥哑,并不是原子語(yǔ)句硅则,實(shí)際是包括了下面三大步驟:
1.為對(duì)象分配內(nèi)存
2.初始化實(shí)例對(duì)象
3.把引用instance指向分配的內(nèi)存空間
這個(gè)三個(gè)步驟并不能保證按序執(zhí)行,處理器會(huì)進(jìn)行指令重排序優(yōu)化株婴,存在這樣的情況:
優(yōu)化重排后執(zhí)行順序?yàn)椋?,3,2, 這樣在線程1執(zhí)行到3時(shí)怎虫,instance已經(jīng)不為null了,線程2此時(shí)判斷instance!=null督暂,則直接返回instance引用揪垄,但現(xiàn)在實(shí)例對(duì)象還沒(méi)有初始化完畢,此時(shí)線程2使用instance可能會(huì)造成程序崩潰逻翁。
所以說(shuō)饥努,實(shí)際上synchronized只是保證了其有序性,并沒(méi)有辦法保證其原子性八回,而其可見(jiàn)性酷愧,是依靠java的內(nèi)存模型來(lái)保證的。
OK缠诅,基本說(shuō)清溶浴,就此打住,老少爺們們管引,咱下期再見(jiàn)士败。