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