九色国产,午夜在线视频,新黄色网址,九九色综合,天天做夜夜做久久做狠狠,天天躁夜夜躁狠狠躁2021a,久久不卡一区二区三区

打開(kāi)APP
userphoto
未登錄

開(kāi)通VIP,暢享免費(fèi)電子書(shū)等14項(xiàng)超值服

開(kāi)通VIP
徐葳【2019版最新】40小時(shí)掌握J(rèn)ava語(yǔ)言之05多線程

1章:多線程

1.1 多線程簡(jiǎn)介

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)提高效率。

1.2 jvm中的多線程

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章:線程的創(chuàng)建

2.1 線程創(chuàng)建的方式一-繼承thread類(lèi)

如圖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)讓d1d2同時(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 多線程代碼

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)用程序都由線程來(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、Thread0Thread1

2.8線程信息

2.3 線程對(duì)象的獲取和名稱(chēng)的定義

如果我想通過(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)

2.4 線程運(yùn)行狀態(tài)圖例

畫(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í)AC就處理臨時(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接口

需求:

火車(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)用4start行嗎?

注意:這樣是會(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)Threadrun方法什么都沒(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)資源的共享了。

2.6 線程兩種創(chuàng)建方式的區(qū)別

 一:繼承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í),建議使用第二種方式。

3章:線程安全問(wèn)題

3.1線程安全問(wèn)題出現(xiàn)的原因

線程安全問(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 代碼

3.2 同步代碼塊-synchronized

如何解決這個(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)票了。

3.3 線程同步的原理

同步代碼塊到底是如何解決線程安全問(wèn)題的呢?

看這段代碼,如圖3.5所示,假設(shè)有四個(gè)線程會(huì)執(zhí)行,第一個(gè)線程過(guò)來(lái)之后,執(zhí)行到synchronized代碼,在這里我們?yōu)榱朔奖憷斫猓梢园?/span>obj認(rèn)為是只有01的兩個(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 代碼

3.4 線程同步的另一種體現(xiàn)-同步函數(shù)

看這個(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ù)

3.5 同步函數(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ù)。

3.6 單例設(shè)計(jì)模式之懶漢式的多線程操作

單例模式我們前面講了有兩種實(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 懶漢式代碼

4章:線程池

4.1 線程池簡(jiǎn)介

多線程的異步執(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ù)使用。

4.2 常用的線程池

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 線程池代碼

4.3 如何選擇線程池?cái)?shù)量

線程池的大小決定著系統(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();

本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶(hù)發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)。
打開(kāi)APP,閱讀全文并永久保存 查看更多類(lèi)似文章
猜你喜歡
類(lèi)似文章
Java多線程詳解【內(nèi)含面試題】
Java多線程程序設(shè)計(jì)詳細(xì)解析
Thread的run()與start()的區(qū)別
深入淺出:JAVA多線程編程實(shí)戰(zhàn)-基礎(chǔ)篇
Java多線程學(xué)習(xí)(吐血超詳細(xì)總結(jié))
Java學(xué)習(xí)路線分享多線程概念
更多類(lèi)似文章 >>
生活服務(wù)
熱點(diǎn)新聞
分享 收藏 導(dǎo)長(zhǎng)圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服