java也是支持多線程的語(yǔ)言。
什么是線程呢?
在說(shuō)線程之前先說(shuō)一下什么是進(jìn)程
進(jìn)程:指當(dāng)前正在執(zhí)行的程序,代表一個(gè)應(yīng)用程序在內(nèi)存中的執(zhí)行區(qū)域。
如圖1.1所示。
圖1.1 進(jìn)程信息
看下面圖1.1所示,里面是有很多的應(yīng)用程序的,每個(gè)應(yīng)用程序都使用一塊內(nèi)存區(qū)域,這個(gè)內(nèi)存區(qū)域可以稱(chēng)為一個(gè)進(jìn)程,內(nèi)存區(qū)域中是需要執(zhí)行代碼的,具體執(zhí)行代碼就是線程去執(zhí)行的。
注意:進(jìn)程只是負(fù)責(zé)開(kāi)辟內(nèi)存空間的,線程才是負(fù)責(zé)執(zhí)行代碼邏輯的執(zhí)行單元。
圖1.2 進(jìn)程-線程
線程:是進(jìn)程中的一個(gè)執(zhí)行控制單元,執(zhí)行路徑。
一個(gè)進(jìn)程中至少有一個(gè)線程在負(fù)責(zé)控制程序的執(zhí)行。
一個(gè)進(jìn)程中如果只有一個(gè)執(zhí)行路徑,這個(gè)程序稱(chēng)為單線程程序。
一個(gè)進(jìn)程中如果有多個(gè)執(zhí)行路徑時(shí),這個(gè)程序就稱(chēng)為多線程程序。
單線程和多線程有什么區(qū)別呢?
舉一個(gè)火車(chē)站賣(mài)票的例子。
一個(gè)窗口賣(mài)票的時(shí)候效率就太低了,如果同時(shí)有上百個(gè)窗口賣(mài)票,這個(gè)時(shí)候效率就高了。
多線程最明顯的效率就是提高執(zhí)行效率。
多線程的出現(xiàn)可以有多條執(zhí)行路徑,讓多部分代碼可以同時(shí)執(zhí)行,來(lái)提高效率。
Java中的jvm虛擬機(jī)是單線程還是多線程呢?
如圖1.3中這個(gè)例子,在執(zhí)行里面的代碼的時(shí)候會(huì)在堆內(nèi)存中產(chǎn)生很多垃圾,如果是單線程處理,并且后面有很多代碼的話(huà),就可能會(huì)造成內(nèi)存溢出,因?yàn)閱尉€程是需要這個(gè)代碼執(zhí)行完才會(huì)去調(diào)用內(nèi)存回收機(jī)制的,所以這樣就不合理了。
我們想實(shí)現(xiàn)這樣的功能,一個(gè)線程負(fù)責(zé)執(zhí)行主程序,另一個(gè)線程負(fù)責(zé)垃圾的回收。
也就是在程序運(yùn)行的同時(shí),也進(jìn)行垃圾回收,其實(shí)java就是這樣做的,
所以java虛擬機(jī)也是多線程的。
圖1.3 代碼
如圖2.1中顯示的這個(gè)異常發(fā)生在主線程上。
圖2.1 代碼
再看下面這個(gè)例子中的代碼,如圖2.2所示。
這個(gè)代碼執(zhí)行的結(jié)果是先打印one,最后再打印two。
圖2.2 代碼
在圖2.2這個(gè)例子中,只有一個(gè)主線程在控制代碼執(zhí)行的流程,當(dāng)d1.show();沒(méi)有執(zhí)行完的時(shí)候,d2.show()是不可能執(zhí)行的,如果d1執(zhí)行時(shí),遇到了較多的運(yùn)算,那么d2就只能等d1結(jié)束。
這兩個(gè)函數(shù)之間也沒(méi)有什么依賴(lài)關(guān)系,可不可以實(shí)現(xiàn)讓d1和d2同時(shí)執(zhí)行呢?
可以!這時(shí)就需要由一個(gè)線程控制d1,另一個(gè)線程控制d2,那如何創(chuàng)建一個(gè)線程呢?
其實(shí)java中對(duì)線程這類(lèi)事物已經(jīng)進(jìn)行了封裝,并提供了相對(duì)應(yīng)的對(duì)象,這個(gè)對(duì)象就是Thread。
查看API文檔中Thread的介紹:如圖2.3所示。
圖2.3 線程的介紹
讓Demo3類(lèi),繼承Thread類(lèi),覆蓋run方法。代碼實(shí)現(xiàn)如圖2.4所示。
圖2.4 線程代碼
為什么要繼承thread類(lèi),覆蓋run方法呢?
其實(shí)直接建立Thread類(lèi)對(duì)象,并開(kāi)啟線程執(zhí)行就可以了,但是雖然線程執(zhí)行了,可是執(zhí)行的代碼是Thread類(lèi)里面run方法中默認(rèn)的代碼。
可是我們定義線程的目的是為了執(zhí)行自定義的代碼,而線程運(yùn)行的代碼必須是在run方法中的,所以只有覆蓋run方法,才可以運(yùn)行自定義的內(nèi)容,想要覆蓋run方法,必須先要繼承Thread類(lèi)。
注意:主線程運(yùn)行的代碼都在main函數(shù)中,自定義線程運(yùn)行的代碼都在對(duì)應(yīng)的run方法中。
如何調(diào)用Demo3這個(gè)線程子類(lèi)中的代碼去執(zhí)行呢,如圖2.5所示。
圖2.5代碼
這樣執(zhí)行的時(shí)候,會(huì)發(fā)現(xiàn)打印的結(jié)果和之前沒(méi)有改為多線程的時(shí)候一樣
這個(gè)程序,其實(shí)還是只有一個(gè)主線程真正執(zhí)行。
如果直接調(diào)用該對(duì)象的run方法,這時(shí),底層資源并沒(méi)有完成線程的創(chuàng)建和執(zhí)行,僅僅是簡(jiǎn)單的對(duì)象調(diào)用方法的過(guò)程,所以這時(shí)執(zhí)行控制流程的只有主線程。
如果想要真正開(kāi)啟線程,需要去調(diào)用Thread類(lèi)中的另一個(gè)方法來(lái)完成。
start方法:
該方法做了兩件事情:
1. 開(kāi)啟線程
2. 調(diào)用了線程的run方法
修改下面的代碼執(zhí)行,這個(gè)執(zhí)行效果就是多個(gè)線程同時(shí)執(zhí)行。如圖2.6所示。
圖2.6 多線程代碼
當(dāng)創(chuàng)建了兩個(gè)對(duì)象d1,d2后,這時(shí)程序就有了3個(gè)線程在同時(shí)執(zhí)行(d1,d2,main)。
當(dāng)主函數(shù)執(zhí)行完d1.start(),d2.start()后,這時(shí)三個(gè)線程同時(shí)打印,結(jié)果比較雜亂,這時(shí)因?yàn)榫€程的隨機(jī)性造成的。
隨機(jī)性的原理是:
windows中的多任務(wù)同時(shí)執(zhí)行,其實(shí)就是多個(gè)應(yīng)用程序在同時(shí)執(zhí)行,而每一個(gè)應(yīng)用程序都由線程來(lái)負(fù)責(zé)控制的,所以windows就是一個(gè)多線程的操作系統(tǒng),CPU是負(fù)責(zé)提供程序運(yùn)算的設(shè)備。
CPU的特點(diǎn):在某一個(gè)時(shí)刻,一個(gè)CPU,只能執(zhí)行一個(gè)程序,所以多個(gè)程序同時(shí)執(zhí)行其實(shí)并不是真正的同時(shí)執(zhí)行,其實(shí)就是CPU在做著快速的切換完成的,只是我們感覺(jué)上是同時(shí)而已。
能不能真正意義上的同時(shí)執(zhí)行呢?
可以的,就是需要多個(gè)CPU,也就是現(xiàn)在所見(jiàn)的多核cpu。
如圖2.7所示。
圖2.7 CPU執(zhí)行
再把代碼改一下,看一下多個(gè)線程的名稱(chēng),在這我們還沒(méi)學(xué)習(xí)到如何獲取線程的名稱(chēng),所以我們通過(guò)其他方式來(lái)查看線程的名稱(chēng),如圖2.8所示。
可以看到打印的錯(cuò)誤信息main、Thread0和Thread1
圖2.8線程信息
如果我想通過(guò)代碼獲取線程的名稱(chēng)該怎么獲取呢?
查看API文檔可以發(fā)現(xiàn) Thread類(lèi)有一個(gè)方法叫getName,可以獲取當(dāng)前線程的名稱(chēng)。
看下面這個(gè)例子,如圖2.9所示。
圖2.9線程信息
注意:因?yàn)?/span>Demo3這個(gè)類(lèi)是Thread的子類(lèi),所以可以直接使用Thread類(lèi)中的getName()方法,獲取當(dāng)前線程的名字。
多線程的名稱(chēng)默認(rèn)是以Thread-開(kāi)頭,后面的編號(hào)是從0開(kāi)始的。
我們知道main函數(shù)也是由一個(gè)主線程執(zhí)行的,那我在這也使用getName()能不能獲取到主線程的名稱(chēng)呢?
不可以的,因?yàn)檫@個(gè)類(lèi)并不是Thread的子類(lèi),所以無(wú)法使用這個(gè)方法。
這個(gè)主線程是虛擬機(jī)創(chuàng)建的。
但是我們可以通過(guò)Thead類(lèi)中的currentThread方法獲取當(dāng)前線程對(duì)象,再通過(guò)當(dāng)前線程對(duì)象來(lái)調(diào)用getName方法獲取當(dāng)前線程的名稱(chēng)。
下面這個(gè)代碼執(zhí)行完之后就可以看到主線程的名稱(chēng)就是main。
如圖2.10所示。
圖2.10 獲取線程對(duì)象
線程默認(rèn)的名稱(chēng)不容易識(shí)別,所以就想給線程起名字
看API發(fā)現(xiàn)還有一個(gè)setName方法,如圖2.11所示。
圖2.11 設(shè)置線程名稱(chēng)
查看API文檔發(fā)現(xiàn)這個(gè)Thread對(duì)象的名稱(chēng)還可以在創(chuàng)建線程的時(shí)候通過(guò)構(gòu)造函數(shù)傳遞過(guò)去。
但是我們之前也傳遞了參數(shù),線程的名稱(chēng)也沒(méi)有發(fā)生變化
那是因?yàn)槲覀冏远x的子類(lèi)沒(méi)有這個(gè)功能,所以需要在子類(lèi)的有參構(gòu)造函數(shù)中調(diào)用父類(lèi)的有參構(gòu)造函數(shù)。
改成下面這樣就行了,如圖2.12所示。
圖2.12線程代碼
總結(jié)下剛才我們說(shuō)的那幾個(gè)方法
static Thread currentThread():獲取當(dāng)前線程對(duì)象
String getName():獲取線程名稱(chēng)
void setname():設(shè)置線程的名稱(chēng)
Thread(String name):構(gòu)造函數(shù),在建立線程對(duì)象的時(shí)候指定名稱(chēng)
畫(huà)圖分析一下線程運(yùn)行時(shí)的不同狀態(tài)。如圖2.13所示。
有時(shí)候cpu在執(zhí)行這個(gè)線程,有時(shí)候CPU不在執(zhí)行這個(gè)線程
線程首先被創(chuàng)建,再運(yùn)行,被創(chuàng)建的線程如何到運(yùn)行狀態(tài)呢?
調(diào)用start方法就可以了
還有一種狀態(tài),凍結(jié)狀態(tài)。
還有一種狀態(tài),消亡狀態(tài)
運(yùn)行狀態(tài)到消亡狀態(tài)需要調(diào)用stop方法,或者run方法運(yùn)行結(jié)束。
運(yùn)行狀態(tài)怎么到凍結(jié)狀態(tài)呢?
有時(shí)候我們需要讓線程在執(zhí)行的時(shí)候暫時(shí)停一會(huì),讓別的線程去執(zhí)行。
通過(guò)凍結(jié)狀態(tài)控制線程的執(zhí)行。
讓正在運(yùn)行的線程調(diào)用sleep(time)可以讓當(dāng)前線程睡一會(huì)。
具體睡多久,我們通過(guò)參數(shù)來(lái)執(zhí)行,單位是毫秒。
如何從凍結(jié)狀態(tài)恢復(fù)到運(yùn)行狀態(tài)呢?
sleep(time)時(shí)間到的話(huà)線程就會(huì)自動(dòng)恢復(fù)到運(yùn)行狀態(tài)了。
通過(guò)調(diào)用wait()方法也可以讓運(yùn)行的線程進(jìn)入到凍結(jié)狀態(tài),但是wait不用指定時(shí)間,而sleep必須要指定時(shí)間。如何恢復(fù)呢?這個(gè)時(shí)候就需要讓另外一個(gè)線程調(diào)用notify方法(喚醒的意思)。
還有一個(gè)最重要的狀態(tài),臨時(shí)阻塞狀態(tài)
這個(gè)狀態(tài)怎么來(lái)的呢?
假設(shè)我有三個(gè)線程,A線程和B線程還有C線程,當(dāng)這3個(gè)線程都調(diào)用了start方法后,叫做3個(gè)線程具備了執(zhí)行資格,處于臨時(shí)阻塞狀態(tài)。當(dāng)某一個(gè)線程A正在被CPU執(zhí)行,說(shuō)明A線程處于運(yùn)行狀態(tài),即具備了執(zhí)行資格,也具備了CPU的執(zhí)行權(quán)。
而B,C處于臨時(shí)阻塞狀態(tài),當(dāng)CPU切換到B線程時(shí),B就具備了執(zhí)行權(quán),這時(shí)A和C就處理臨時(shí)阻塞狀態(tài),只具備執(zhí)行資格,不具備執(zhí)行權(quán)。
所以,臨時(shí)阻塞狀態(tài):該狀態(tài)中的線程,具備執(zhí)行資格的,但是不具備執(zhí)行權(quán)。
臨時(shí)阻塞狀態(tài)時(shí)由CPU控制的,凍結(jié)狀態(tài)是人為控制的。
圖2.13 線程的四種運(yùn)行狀態(tài)
需求:
火車(chē)站售票,一共100章,通過(guò)4個(gè)窗口賣(mài)完。
因?yàn)?/span>4個(gè)窗口售票動(dòng)作被同時(shí)執(zhí)行,所以需要用到多線程技術(shù)。代碼如圖2.14所示。
開(kāi)啟四個(gè)窗口賣(mài)票:
圖2.14 賣(mài)票代碼
本來(lái)100張票,現(xiàn)在卻賣(mài)出了400張票,這樣就出大事了。
現(xiàn)在我創(chuàng)建了4個(gè)對(duì)象,每個(gè)對(duì)象都有一個(gè)ticket變量,所以就是400張了。
這個(gè)時(shí)候把這個(gè)變量設(shè)置為靜態(tài)的就可以了。這樣再執(zhí)行,打印結(jié)果就正常了。
如圖2.15所示。
圖2.15 代碼
但是我們不建議使用靜態(tài),因?yàn)榧由响o態(tài)之后,對(duì)象的生命周期變得過(guò)長(zhǎng),我們還有其他解決方案來(lái)解決多線程數(shù)據(jù)共享的問(wèn)題,所以把static關(guān)鍵字取消掉。
在main函數(shù)中new一個(gè)線程,調(diào)用4次start行嗎?
注意:這樣是會(huì)報(bào)錯(cuò)的。因?yàn)槎啻螁?dòng)一個(gè)線程是會(huì)報(bào)錯(cuò)的。
線程已經(jīng)開(kāi)啟了,再調(diào)用開(kāi)啟是不合適的,如圖2.16所示。
圖2.16 代碼
如果你要處理的資源和你的動(dòng)作封裝到一起了,可以怎么做呢?
繼承搞不定的話(huà)我們就使用另外一種方式來(lái)搞定
創(chuàng)建線程的另一種方法是實(shí)現(xiàn)runnable接口,然后實(shí)現(xiàn)run方法,在創(chuàng)建Thread時(shí)作為一個(gè)參數(shù)來(lái)傳遞并啟動(dòng)
到API文檔中查看一下runnable接口
把之前的代碼改造成這樣的,如圖2.17所示。
圖2.17 線程代碼
這個(gè)代碼執(zhí)行的時(shí)候是沒(méi)有任何輸出的,因?yàn)槟J(rèn)Thread的run方法什么都沒(méi)做。
可是我開(kāi)啟多線程的目的是為了讓他執(zhí)行我指定的run方法
所說(shuō)義在這我們要首先明確run方法所屬的對(duì)象。
我只要在線程類(lèi)建立對(duì)象的同時(shí),把要執(zhí)行run方法的對(duì)象傳進(jìn)去即可。
這樣Thread線程在開(kāi)啟的時(shí)候就有了明確的run方法。
把這個(gè)ticket對(duì)象傳給四個(gè)線程對(duì)象,如圖2.18所示。
圖2.18 多線程代碼
在這執(zhí)行的時(shí)候如果想要獲取線程的名稱(chēng),就不能在TicketWin類(lèi)中直接使用getName了,因?yàn)楝F(xiàn)在這個(gè)類(lèi)不是線程的子類(lèi)了,
在這我們使用線程的currentThread方法獲取線程名稱(chēng),如圖2.19所示。
2.19線程代碼
那么這一種和第一種比到底有什么好處呢?
第一種方式都繼承子類(lèi)之后會(huì)造成資源不共享,
第二種的話(huà),就很方便了,實(shí)現(xiàn)一個(gè)接口,讓多個(gè)線程去運(yùn)行即可。這樣就可以實(shí)現(xiàn)資源的共享了。
一:繼承Thread類(lèi)。
步驟:
1.定義類(lèi)繼承Thread。
2.覆蓋Thread類(lèi)中的run方法,run方法用于存儲(chǔ)多線程要運(yùn)行的代碼。
3.創(chuàng)建Thread類(lèi)的子類(lèi)對(duì)象創(chuàng)建線程。
4.調(diào)用Thread類(lèi)中的start方法開(kāi)啟線程,并執(zhí)行子類(lèi)中的run方法。
特點(diǎn):
1.當(dāng)類(lèi)去描述事物,事物中有屬性和行為。
如果行為中有部分代碼需要被多線程所執(zhí)行,同時(shí)還在操作屬性。
就需要該類(lèi)繼承Thread類(lèi),產(chǎn)生該類(lèi)的對(duì)象作為線程對(duì)象。
可是這樣做會(huì)導(dǎo)致每一個(gè)對(duì)象中都存儲(chǔ)一份屬性數(shù)據(jù)。
無(wú)法在多個(gè)線程中共享該數(shù)據(jù)。加上靜態(tài),雖然實(shí)現(xiàn)了共享但是生命周期過(guò)長(zhǎng)。
2.如果一個(gè)類(lèi)明確了自己的父類(lèi),那么很遺憾,它就不可以在繼承Thread。
因?yàn)?/span>java不允許類(lèi)的多繼承。
二:實(shí)現(xiàn)Runnable接口:
步驟:
1.定義類(lèi)實(shí)現(xiàn)Runnable接口。
2.覆蓋接口中的run方法,將多線程要運(yùn)行的代碼定義在方法中。
3.通過(guò)Thread類(lèi)創(chuàng)建線程對(duì)象,并將實(shí)現(xiàn)了Runnable接口的子類(lèi)對(duì)象
作為實(shí)際參數(shù)傳遞給Thread類(lèi)的構(gòu)造函數(shù)。
為什么非要被Runnable接口的子類(lèi)對(duì)象傳遞給Thread類(lèi)的構(gòu)造函數(shù)呢?
是因?yàn)榫€程對(duì)象在建立時(shí),必須要明確自己要運(yùn)行的run方法,而這個(gè)run方法
定義在了Runnable接口的子類(lèi)中,所以要將該run方法所屬的對(duì)象傳遞給Thread類(lèi)的構(gòu)造函數(shù)。
讓線程對(duì)象一建立,就知道運(yùn)行哪個(gè)run方法。
4.調(diào)用Thread類(lèi)中的start方法,開(kāi)啟線程,并執(zhí)行Runanble接口子類(lèi)中的run方法。
特點(diǎn):
1.描述事物的類(lèi)中封裝了屬性和行為,如果有部分代碼需要被多線程所執(zhí)行。
同時(shí)還在操作屬性。那么可以通過(guò)實(shí)現(xiàn)Runnable接口的方式。
因?yàn)樵摲绞绞嵌x一個(gè)Runnable接口的子類(lèi)對(duì)象,可以被多個(gè)線程所操作
實(shí)現(xiàn)了數(shù)據(jù)的共享。
2.實(shí)現(xiàn)了Runnable接口的好處,避免了單繼承的局限性。
也就說(shuō),一個(gè)類(lèi)如果已經(jīng)有了自己的父類(lèi)是不可以繼承Thread類(lèi)的。
但是該類(lèi)中還有需要被多線程執(zhí)行的代碼。這時(shí)就可以通過(guò)在該類(lèi)上功能擴(kuò)展的形式。
實(shí)現(xiàn)一個(gè)Runnable接口。
所以在創(chuàng)建線程時(shí),建議使用第二種方式。
線程安全問(wèn)題:因?yàn)榫€程執(zhí)行的隨機(jī)性,有可能會(huì)導(dǎo)致多線程在操作數(shù)據(jù)時(shí)發(fā)生數(shù)據(jù)錯(cuò)誤的情況產(chǎn)生。
分析下面這個(gè)代碼,理論上是存在線程安全的問(wèn)題的,賣(mài)出去的票可能大于100張,代碼如圖3.1所示。
圖3.1 線程代碼
如果電腦開(kāi)的程序比較多,出現(xiàn)問(wèn)題的概率就比較大,我們現(xiàn)在開(kāi)的程序少,還沒(méi)出現(xiàn)這個(gè)現(xiàn)象
下面模擬一下,如圖3.2所示。
在代碼中讓程序睡一會(huì)。調(diào)用sleep,因?yàn)?/span>sleep拋出的有異常,所以需要在這進(jìn)行處理,只能try catch,不能throws,因?yàn)槲覀冞@個(gè)類(lèi)實(shí)現(xiàn)了runnable接口,runnable接口中的run方法并沒(méi)有向外拋出異常。
圖3.2 線程代碼
這時(shí)就出現(xiàn)了線程安全的問(wèn)題。打印出來(lái)的票號(hào)有0和負(fù)數(shù),并且票的張數(shù)也超過(guò)了100張。
線程安全問(wèn)題產(chǎn)生的原因:
當(dāng)線程中多條代碼在操作同一個(gè)共享數(shù)據(jù)時(shí),一個(gè)線程將部分代碼執(zhí)行完,還沒(méi)有基礎(chǔ)執(zhí)行其他代碼時(shí),被另一個(gè)線程獲取到了CPU執(zhí)行權(quán),這時(shí),共享數(shù)據(jù)操作就有可能出現(xiàn)數(shù)據(jù)錯(cuò)誤。
簡(jiǎn)答說(shuō):多條操作數(shù)據(jù)的代碼被多個(gè)線程分來(lái)執(zhí)行造成的。
在我們這個(gè)案例里面就是判斷和--操作被多個(gè)線程分開(kāi)執(zhí)行了,
安全問(wèn)題涉及的內(nèi)容:
1. 共享數(shù)據(jù)
2. 是否被多條語(yǔ)句操作
這也是判斷多線程程序是否存在安全隱患的依據(jù)。
注意:下面這兩個(gè)操作沒(méi)有被一個(gè)線程執(zhí)行完,而是被多線程分開(kāi)來(lái)執(zhí)行了,這樣就容易引發(fā)線程安全問(wèn)題。如圖3.3所示。
圖3.3 代碼
如何解決這個(gè)線程安全問(wèn)題呢?
java中提供了一個(gè)同步機(jī)制,解決的原理是讓多條操作共享數(shù)據(jù)的代碼在某一時(shí)間段,被一個(gè)線程執(zhí)行完,在執(zhí)行過(guò)程中,其他線程不可以參與運(yùn)算。
同步的格式看下面,這個(gè)代碼塊可以保證一次只有一個(gè)線程在里面執(zhí)行。
里面需要一個(gè)對(duì)象,這個(gè)對(duì)象可以是任意對(duì)象,就算是new 一個(gè)類(lèi)也可以,但是我還需要定義這個(gè)類(lèi),比較麻煩,所以可以直接在這個(gè)類(lèi)里面new一個(gè)Object。
同步的格式(同步代碼塊):
synchronized(對(duì)象){ // 該對(duì)象可以是任意對(duì)象
需要被同步的代碼;
}
代碼案例如圖3.4所示。
圖3.4 代碼
這樣改完之后,再執(zhí)行,就不會(huì)出現(xiàn)負(fù)號(hào)票了。
同步代碼塊到底是如何解決線程安全問(wèn)題的呢?
看這段代碼,如圖3.5所示,假設(shè)有四個(gè)線程會(huì)執(zhí)行,第一個(gè)線程過(guò)來(lái)之后,執(zhí)行到synchronized代碼,在這里我們?yōu)榱朔奖憷斫猓梢园?/span>obj認(rèn)為是只有0和1的兩個(gè)狀態(tài),當(dāng)?shù)谝粋€(gè)線程過(guò)來(lái)的時(shí)候,判斷obj的值,如果是1,則向下執(zhí)行,在向下執(zhí)行的時(shí)候會(huì)把這個(gè)值改為0,這樣其他線程過(guò)來(lái)的話(huà)就進(jìn)不來(lái)這個(gè)代碼塊了,這樣我的第一個(gè)線程就繼續(xù)向下執(zhí)行,當(dāng)執(zhí)行到最后的時(shí)候再把obj的值從0改為1,這個(gè)時(shí)候其他線程才可以進(jìn)入這個(gè)代碼塊。
圖3.5 代碼
舉個(gè)例子,火車(chē)上的衛(wèi)生間。
你去上廁所的時(shí)候會(huì)看一下里面是否有人,如果沒(méi)人,直接進(jìn)去把門(mén)反鎖上,這樣就顯示廁所有人了,其他人就進(jìn)不來(lái)了。
其實(shí)剛才我們說(shuō)的obj就相當(dāng)于是一把鎖。
誰(shuí)執(zhí)行到這個(gè)同步,就持有這把鎖,誰(shuí)執(zhí)行完了就釋放這個(gè)鎖。
同步的原理:
通過(guò)一個(gè)對(duì)象鎖,將多條操作共享數(shù)據(jù)的代碼進(jìn)行了封裝并加鎖。這樣只有持有這個(gè)鎖的線程才能操作同步中的代碼
在這個(gè)線程執(zhí)行期間,即使其他線程獲得了執(zhí)行權(quán),因?yàn)闆](méi)有獲得鎖,就只能在同步代碼塊外面等
只有當(dāng)同步中的線程執(zhí)行完同步代碼塊中的代碼,才會(huì)釋放這個(gè)鎖,這個(gè)時(shí)候其他線程才有機(jī)會(huì)去獲取這個(gè)鎖
并只能有一個(gè)線程獲取到鎖而且進(jìn)入到同步中。
同步的好處:
同步的出現(xiàn)解決了多線程的安全問(wèn)題。
同步的弊端:
因?yàn)槎鄠€(gè)線程每次都要判斷這個(gè)鎖,所以效率會(huì)降低。
以后我們?cè)趯?xiě)同步代碼的時(shí)候會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題,如果出現(xiàn)了安全問(wèn)題,加入了同步,安全問(wèn)題依然存在,因?yàn)橥绞怯星疤岬模欢ㄒ_認(rèn)是哪塊的問(wèn)題;
同步前提:
1. 同步需要兩個(gè)或者兩個(gè)以上的線程
2. 多個(gè)線程使用的是同一個(gè)鎖
未滿(mǎn)足這兩個(gè)條件,不能稱(chēng)其為同步。
如果出現(xiàn)了加上同步代碼 安全問(wèn)題依然存在的情況,就按照這兩個(gè)前提來(lái)排查問(wèn)題。
注意:
同步前提里面的1:如果單線程也使用同步的話(huà),這樣既不存在安全性,效率還低。
同步前提里面的2:如果一個(gè)線程使用A鎖,一個(gè)線程使用B鎖,這樣的話(huà)和不使用鎖沒(méi)什么區(qū)別
注意:這種寫(xiě)法是錯(cuò)誤的,相當(dāng)于給每一個(gè)線程都使用一個(gè)不同的鎖,所以輸出結(jié)果還是有負(fù)數(shù),如圖3.6所示。
圖3.6 代碼
看這個(gè)例子
有兩個(gè)儲(chǔ)戶(hù),到同一個(gè)銀行存錢(qián),每次存100,存3次,兩個(gè)儲(chǔ)戶(hù)是隨機(jī)存入的。
銀行有一個(gè)金庫(kù),提供一個(gè)存錢(qián)的功能。
代碼實(shí)現(xiàn)如圖3.7所示。
圖3.7 銀行存款
這樣實(shí)現(xiàn)的話(huà),打印的是100 200 300 ,沒(méi)有出現(xiàn)600.
因?yàn)?/span>newBank是在run方法內(nèi)部調(diào)用的,這樣兩次調(diào)用就會(huì)創(chuàng)建兩個(gè)bank對(duì)象,所以需要把這個(gè)對(duì)象提到run方法外面,和run方法平級(jí)。
把Cus類(lèi)改成這樣,如圖3.8所示。
圖3.8 代碼
改過(guò)之后看看代碼有沒(méi)有線程安全問(wèn)題。
根據(jù)線程安全的判斷原則,有共享變量,有多個(gè)線程操作。所以是存在的,
在這個(gè)代碼的位置添加sleep,演示下效果。如圖3.9所示。
圖3.9 代碼
執(zhí)行的效果如下圖3.10所示。
圖3.10 執(zhí)行的結(jié)果
分析下為什么沒(méi)打印100,因?yàn)榫€程1過(guò)來(lái)的時(shí)候sum變成了100,線程1休息一會(huì),線程2過(guò)來(lái),sum就變成了200,最后線程1和線程2都打印的是200
所以,我們發(fā)現(xiàn)sum是共享數(shù)據(jù),有兩條語(yǔ)句在操作這個(gè)共享數(shù)據(jù),如果這兩條語(yǔ)句被多個(gè)線程分開(kāi)執(zhí)行,也就是一個(gè)線程沒(méi)有執(zhí)行完,其他線程就參與執(zhí)行了,就容易發(fā)生線程安全問(wèn)題。
解決辦法:加入同步機(jī)制,將需要被一個(gè)線程一次執(zhí)行完的代碼存儲(chǔ)到同步代碼塊中。
那么使用前面學(xué)習(xí)的synchroized代碼塊,代碼如圖3.11所示。
圖3.11 代碼
注意:Cus類(lèi)中的for循環(huán)中的x是不涉及線程安全問(wèn)題的,他是一個(gè)局部變量。
在這里我們發(fā)現(xiàn),同步代碼塊是用于封裝代碼的,而函數(shù)也是用來(lái)封裝代碼的,所不同之處是同步帶有鎖機(jī)制。那么如果讓函數(shù)具備同步的特性,不就可以取代同步代碼塊了嗎
怎么讓函數(shù)具備同步性呢?
其實(shí)很簡(jiǎn)單,只要在函數(shù)上加上一個(gè)同步關(guān)鍵字修飾即可,這就是同步的另一個(gè)體現(xiàn)形式,同步函數(shù)。代碼如圖3.12所示。
圖 3.12 同步函數(shù)
同步函數(shù)用的是哪個(gè)鎖呢?
修改前面賣(mài)票的代碼,如圖3.13所示。
發(fā)現(xiàn)賣(mài)的票重復(fù)了,因?yàn)楝F(xiàn)在用的鎖不是同一個(gè)。
圖3.13 代碼
把同步代碼塊的鎖換成this,驗(yàn)證一下效果。如圖3.14所示。
圖3.14 代碼
將同步代碼塊的鎖換成this.發(fā)現(xiàn)同步安全問(wèn)題解決了,所以可以確認(rèn)同步函數(shù)使用的同步鎖是this。
同步函數(shù)和同步代碼塊的區(qū)別:
同步代碼塊使用的鎖可以是任意對(duì)象
同步函數(shù)使用的鎖是固定對(duì)象 this
所以一般定義同步時(shí),建議使用同步代碼塊。如果鎖對(duì)象可以使用this,那么就可以使用同步函數(shù)。
單例模式我們前面講了有兩種實(shí)現(xiàn)形式,一種是餓漢式、一種是懶漢式,如圖3.15所示。
圖3.15 單例模式
針對(duì)第二種懶漢式這種形式,當(dāng)多個(gè)線程并發(fā)執(zhí)行getInstance方法時(shí),容易發(fā)生線程安全問(wèn)題,因?yàn)?/span>s是共享數(shù)據(jù),有多條語(yǔ)句在操作共享數(shù)據(jù)。
解決方式很簡(jiǎn)單,只要讓getInstance方法具備同步性即可,如圖3.16所示。
圖3.16 懶漢式
這樣雖然解決了線程安全問(wèn)題,但是多個(gè)線程每一次獲取該實(shí)例都要調(diào)用這個(gè)方法,這樣效率會(huì)比較低。為了保證安全,同時(shí)提高效率,可以通過(guò)雙重判斷的形式來(lái)完成,其實(shí)就是減少線程判斷鎖的次數(shù)。如圖3.17所示。
通過(guò)雙重判斷來(lái)提高效率,當(dāng)后續(xù)執(zhí)行到第一個(gè)判斷語(yǔ)句的時(shí)候,就會(huì)發(fā)現(xiàn)s!=null,這個(gè)時(shí)候就不需要再判斷同步鎖的代碼了。
圖3.17 懶漢式代碼
多線程的異步執(zhí)行方式,雖然能夠最大限度發(fā)揮多核計(jì)算機(jī)的計(jì)算能力,但是如果不加控制,反而會(huì)對(duì)系統(tǒng)造成負(fù)擔(dān)。線程本身也要占用內(nèi)存空間,大量的線程會(huì)占用內(nèi)存資源并且可能會(huì)導(dǎo)致Out of Memory。即便沒(méi)有這樣的情況,大量的線程回收也會(huì)給GC帶來(lái)很大的壓力。
為了避免重復(fù)的創(chuàng)建線程,線程池的出現(xiàn)可以讓線程進(jìn)行復(fù)用。通俗點(diǎn)講,當(dāng)有工作來(lái),就會(huì)向線程池拿一個(gè)線程,當(dāng)工作完成后,并不是直接關(guān)閉線程,而是將這個(gè)線程歸還給線程池供其他任務(wù)使用。
java中提供的線程池大致有下面這4種:
1. newFixedThreadPool
2. newSingleThreadExecutor
3. newCachedThreadPool
4. newScheduledThreadPool
其中常用的是newFixedThreadPool,在這里我們就以這個(gè)為例進(jìn)行分析演示。
固定大小的線程池,可以指定線程池的大小,該線程池中的線程數(shù)量始終不變,當(dāng)有新任務(wù)提交時(shí),線程池中有空閑線程則會(huì)立即執(zhí)行,如果沒(méi)有,則會(huì)暫存到阻塞隊(duì)列。對(duì)于固定大小的線程池,不存在線程數(shù)量的變化。缺點(diǎn)是在線程池空閑時(shí),即線程池中沒(méi)有可運(yùn)行任務(wù)時(shí),它也不會(huì)釋放工作線程,還會(huì)占用一定的系統(tǒng)資源。
代碼案例如圖4.1所示。
圖4.1 線程池代碼
線程池的大小決定著系統(tǒng)的性能,過(guò)大或者過(guò)小的線程池?cái)?shù)量都無(wú)法發(fā)揮最優(yōu)的系統(tǒng)性能。
當(dāng)然線程池的大小也不需要做的太過(guò)于精確,只需要避免過(guò)大和過(guò)小的情況。一般來(lái)說(shuō),確定線程池的大小需要考慮CPU的數(shù)量,內(nèi)存大小,任務(wù)是計(jì)算密集型還是IO密集型等因素
NCPU = CPU的數(shù)量
UCPU = 期望對(duì)CPU的使用率 0 ≤UCPU ≤ 1
W/C = 等待時(shí)間與計(jì)算時(shí)間的比率
如果希望處理器達(dá)到理想的使用率,那么線程池的最優(yōu)大小為:
線程池大?。?/span>Nthreads=Ncpu* Ucpu * (1+W/C)
下面分析一下IO密集型的任務(wù)下線程池大小的設(shè)置:
一般情況下,如果存在IO,那么肯定w/c>1(阻塞耗時(shí)一般都是計(jì)算耗時(shí)的很多倍),但是需要考慮系統(tǒng)內(nèi)存有限(每開(kāi)啟一個(gè)線程都需要內(nèi)存空間),這里需要上服務(wù)器測(cè)試具體多少個(gè)線程數(shù)適合(CPU占比、線程數(shù)、總耗時(shí)、內(nèi)存消耗)。如果不想去測(cè)試,保守點(diǎn)取1即,Nthreads=Ncpu*1*(1+1)=2Ncpu。這樣設(shè)置一般都OK。
針對(duì)計(jì)算密集型的任務(wù)下線程池大小的設(shè)置:
假設(shè)沒(méi)有等待,w=0,則W/C=0. Nthreads=Ncpu。
總結(jié):
IO密集型=2Ncpu(可以測(cè)試后自己控制大小,2Ncpu一般沒(méi)問(wèn)題,其實(shí)在實(shí)際中可以把這個(gè)值適當(dāng)調(diào)大一些)(常出現(xiàn)于線程中:數(shù)據(jù)庫(kù)數(shù)據(jù)交互、網(wǎng)絡(luò)數(shù)據(jù)傳輸、文件處理、網(wǎng)絡(luò)爬蟲(chóng)等等)
計(jì)算密集型=Ncpu(常出現(xiàn)于線程中:復(fù)雜算法)
對(duì)于計(jì)算密集型的任務(wù),在擁有N個(gè)處理器的系統(tǒng)上,當(dāng)線程池的大小為N+1時(shí),通常能實(shí)現(xiàn)最優(yōu)的效率。即使當(dāng)計(jì)算密集型的線程偶爾由于缺失故障或者其他原因而暫停時(shí),這個(gè)額外的線程也能確保CPU的時(shí)鐘周期不會(huì)被浪費(fèi)。
java中:int Ncpu = Runtime.getRuntime().availableProcessors();
聯(lián)系客服