多線程系列教程:java多線程-概念&創(chuàng)建啟動(dòng)&中斷&守護(hù)線程&優(yōu)先級(jí)&線程狀態(tài)(多線程編程之一) java多線程同步以及線程間通信詳解&消費(fèi)者生產(chǎn)者模式&死鎖&Thread.join()(多線程編程之二) java&android線程池-Executor框架之ThreadPoolExcutor&ScheduledThreadPoolExecutor淺析(多線程編程之三) Java多線程:Callable、Future和FutureTask淺析(多線程編程之四) 本篇我們將討論以下知識(shí)點(diǎn): 1.線程同步問(wèn)題的產(chǎn)生什么是線程同步問(wèn)題,我們先來(lái)看一段賣票系統(tǒng)的代碼,然后再分析這個(gè)問(wèn)題:
從運(yùn)行結(jié)果,我們就可以看出我們4個(gè)售票窗口同時(shí)賣出了1號(hào)票,這顯然是不合邏輯的,其實(shí)這個(gè)問(wèn)題就是我們前面所說(shuō)的線程同步問(wèn)題。不同的線程都對(duì)同一個(gè)數(shù)據(jù)進(jìn)了操作這就容易導(dǎo)致數(shù)據(jù)錯(cuò)亂的問(wèn)題,也就是線程不同步。那么這個(gè)問(wèn)題該怎么解決呢?在給出解決思路之前我們先來(lái)分析一下這個(gè)問(wèn)題是怎么產(chǎn)生的?我們聲明一個(gè)線程類Ticket,在這個(gè)類中我們又聲明了一個(gè)成員變量num也就是票的數(shù)量,然后我們通過(guò)run方法不斷的去獲取票數(shù)并輸出,最后我們?cè)谕獠款怲icketDemo中創(chuàng)建了四個(gè)線程同時(shí)操作這個(gè)數(shù)據(jù),運(yùn)行后就出現(xiàn)我們剛才所說(shuō)的線程同步問(wèn)題,從這里我們可以看出產(chǎn)生線程同步(線程安全)問(wèn)題的條件有兩個(gè):1.多個(gè)線程在操作共享的數(shù)據(jù)(num),2.操作共享數(shù)據(jù)的線程代碼有多條(4條線程);既然原因知道了,那該怎么解決? 解決思路:將多條操作共享數(shù)據(jù)的線程代碼封裝起來(lái),當(dāng)有線程在執(zhí)行這些代碼的時(shí)候,其他線程時(shí)不可以參與運(yùn)算的。必須要當(dāng)前線程把這些代碼都執(zhí)行完畢后,其他線程才可以參與運(yùn)算。 好了,思路知道了,我們就用java代碼的方式來(lái)解決這個(gè)問(wèn)題。 2.解決線程同步的兩種典型方案 在java中有兩種機(jī)制可以防止線程安全的發(fā)生,Java語(yǔ)言提供了一個(gè)synchronized關(guān)鍵字來(lái)解決這問(wèn)題,同時(shí)在Java SE5.0引入了Lock鎖對(duì)象的相關(guān)類,接下來(lái)我們分別介紹這兩種方法 2.1通過(guò)鎖(Lock)對(duì)象的方式解決線程安全問(wèn)題 在給出解決代碼前我們先來(lái)介紹一個(gè)知識(shí)點(diǎn):Lock,鎖對(duì)象。在java中鎖是用來(lái)控制多個(gè)線程訪問(wèn)共享資源的方式,一般來(lái)說(shuō),一個(gè)鎖能夠防止多個(gè)線程同時(shí)訪問(wèn)共享資源(但有的鎖可以允許多個(gè)線程并發(fā)訪問(wèn)共享資源,比如讀寫鎖,后面我們會(huì)分析)。在Lock接口出現(xiàn)之前,java程序是靠synchronized關(guān)鍵字(后面分析)實(shí)現(xiàn)鎖功能的,而JAVA SE5.0之后并發(fā)包中新增了Lock接口用來(lái)實(shí)現(xiàn)鎖的功能,它提供了與synchronized關(guān)鍵字類似的同步功能,只是在使用時(shí)需要顯式地獲取和釋放鎖,缺點(diǎn)就是缺少像synchronized那樣隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性,可中斷的獲取鎖以及超時(shí)獲取鎖等多種synchronized關(guān)鍵字所不具備的同步特性。接下來(lái)我們就來(lái)介紹Lock接口的主要API方便我們學(xué)習(xí)
這里先介紹一下API,后面我們將結(jié)合Lock接口的實(shí)現(xiàn)子類ReentrantLock使用某些方法。 ReentrantLock(重入鎖): 重入鎖,顧名思義就是支持重新進(jìn)入的鎖,它表示該鎖能夠支持一個(gè)線程對(duì)資源的重復(fù)加鎖,也就是說(shuō)在調(diào)用lock()方法時(shí),已經(jīng)獲取到鎖的線程,能夠再次調(diào)用lock()方法獲取鎖而不被阻塞,同時(shí)還支持獲取鎖的公平性和非公平性。這里的公平是在絕對(duì)時(shí)間上,先對(duì)鎖進(jìn)行獲取的請(qǐng)求一定先被滿足,那么這個(gè)鎖是公平鎖,反之,是不公平的。那么該如何使用呢?看范例代碼: 1.同步執(zhí)行的代碼跟synchronized類似功能:
TicketDemo類無(wú)需變化,運(yùn)行結(jié)果正常(太多不貼了),線程安全問(wèn)題就此解決。 2.2通過(guò)synchronied關(guān)鍵字的方式解決線程安全問(wèn)題 在Java中內(nèi)置了語(yǔ)言級(jí)的同步原語(yǔ)-synchronized,這個(gè)可以大大簡(jiǎn)化了Java中多線程同步的使用。從JAVA SE1.0開始,java中的每一個(gè)對(duì)象都有一個(gè)內(nèi)部鎖,如果一個(gè)方法使用synchronized關(guān)鍵字進(jìn)行聲明,那么這個(gè)對(duì)象將保護(hù)整個(gè)方法,也就是說(shuō)調(diào)用該方法線程必須獲得內(nèi)部的對(duì)象鎖。
嗯,同步代碼塊解決,運(yùn)行結(jié)果也正常。到此同步問(wèn)題也就解決了,當(dāng)然代碼同步也是要犧牲效率為前提的: 同步的好處:解決了線程的安全問(wèn)題。 同步的弊端:相對(duì)降低了效率,因?yàn)橥酵獾木€程的都會(huì)判斷同步鎖。 同步的前提:同步中必須有多個(gè)線程并使用同一個(gè)鎖。 3.線程間的通信機(jī)制 線程開始運(yùn)行,擁有自己的棧空間,但是如果每個(gè)運(yùn)行中的線程,如果僅僅是孤立地運(yùn)行,那么沒(méi)有一點(diǎn)兒價(jià)值,或者是價(jià)值很小,如果多線程能夠相互配合完成工作的話,這將帶來(lái)巨大的價(jià)值,這也就是線程間的通信啦。在java中多線程間的通信使用的是等待/通知機(jī)制來(lái)實(shí)現(xiàn)的。 3.1synchronied關(guān)鍵字等待/通知機(jī)制:是指一個(gè)線程A調(diào)用了對(duì)象O的wait()方法進(jìn)入等待狀態(tài),而另一個(gè)線程B調(diào)用了對(duì)象O的notify()或者notifyAll()方法,線程A收到通知后從對(duì)象O的wait()方法返回,進(jìn)而執(zhí)行后續(xù)操作。上述的兩個(gè)線程通過(guò)對(duì)象O來(lái)完成交互,而對(duì)象上的wait()和notify()/notifyAll()的關(guān)系就如同開關(guān)信號(hào)一樣,用來(lái)完成等待方和通知方之間的交互工作。 等待/通知機(jī)制主要是用到的函數(shù)方法是notify()/notifyAll(),wait()/wait(long),wait(long,int),這些方法在上一篇文章都有說(shuō)明過(guò),這里就不重復(fù)了。當(dāng)然這是針對(duì)synchronied關(guān)鍵字修飾的函數(shù)或代碼塊,因?yàn)橐褂?/span>notify()/notifyAll(),wait()/wait(long),wait(long,int)這些方法的前提是對(duì)調(diào)用對(duì)象加鎖,也就是說(shuō)只能在同步函數(shù)或者同步代碼塊中使用。 3.2條件對(duì)象的等待/通知機(jī)制:所謂的條件對(duì)象也就是配合前面我們分析的Lock鎖對(duì)象,通過(guò)鎖對(duì)象的條件對(duì)象來(lái)實(shí)現(xiàn)等待/通知機(jī)制。那么條件對(duì)象是怎么創(chuàng)建的呢?
就這樣我們創(chuàng)建了一個(gè)條件對(duì)象。注意這里返回的對(duì)象是與該鎖(ticketLock)相關(guān)的條件對(duì)象。下面是條件對(duì)象的API:
上述方法的過(guò)程分析:一個(gè)線程A調(diào)用了條件對(duì)象的await()方法進(jìn)入等待狀態(tài),而另一個(gè)線程B調(diào)用了條件對(duì)象的signal()或者signalAll()方法,線程A收到通知后從條件對(duì)象的await()方法返回,進(jìn)而執(zhí)行后續(xù)操作。上述的兩個(gè)線程通過(guò)條件對(duì)象來(lái)完成交互,而對(duì)象上的await()和signal()/signalAll()的關(guān)系就如同開關(guān)信號(hào)一樣,用來(lái)完成等待方和通知方之間的交互工作。當(dāng)然這樣的操作都是必須基于對(duì)象鎖的,當(dāng)前線程只有獲取了鎖,才能調(diào)用該條件對(duì)象的await()方法,而調(diào)用后,當(dāng)前線程將縮放鎖。 這里有點(diǎn)要特別注意的是,上述兩種等待/通知機(jī)制中,無(wú)論是調(diào)用了signal()/signalAll()方法還是調(diào)用了notify()/notifyAll()方法并不會(huì)立即激活一個(gè)等待線程。它們僅僅都只是解除等待線程的阻塞狀態(tài),以便這些線程可以在當(dāng)前線程解鎖或者退出同步方法后,通過(guò)爭(zhēng)奪CPU執(zhí)行權(quán)實(shí)現(xiàn)對(duì)對(duì)象的訪問(wèn)。到此,線程通信機(jī)制的概念分析完,我們下面通過(guò)生產(chǎn)者消費(fèi)者模式來(lái)實(shí)現(xiàn)等待/通知機(jī)制。 4.生產(chǎn)者消費(fèi)者模式 4.1單生產(chǎn)者單消費(fèi)者模式 顧名思義,就是一個(gè)線程消費(fèi),一個(gè)線程生產(chǎn)。我們先來(lái)看看等待/通知機(jī)制下的生產(chǎn)者消費(fèi)者模式:我們假設(shè)這樣一個(gè)場(chǎng)景,我們是賣北京烤鴨店鋪,我們現(xiàn)在只有一條生產(chǎn)線也只有一條消費(fèi)線,也就是說(shuō)只能生產(chǎn)線程生產(chǎn)完了,再通知消費(fèi)線程才能去賣,如果消費(fèi)線程沒(méi)烤鴨了,就必須通知生產(chǎn)線程去生產(chǎn),此時(shí)消費(fèi)線程進(jìn)入等待狀態(tài)。在這樣的場(chǎng)景下,我們不僅要保證共享數(shù)據(jù)(烤鴨數(shù)量)的線程安全,而且還要保證烤鴨數(shù)量在消費(fèi)之前必須有烤鴨。下面我們通過(guò)java代碼來(lái)實(shí)現(xiàn): 北京烤鴨生產(chǎn)資源類KaoYaResource:
在這個(gè)類中我們有兩個(gè)synchronized的同步方法,一個(gè)是生產(chǎn)烤鴨的,一個(gè)是消費(fèi)烤鴨的,之所以需要同步是因?yàn)槲覀儾僮髁斯蚕頂?shù)據(jù)count,同時(shí)為了保證生產(chǎn)烤鴨后才能消費(fèi)也就是生產(chǎn)一只烤鴨后才能消費(fèi)一只烤鴨,我們使用了等待/通知機(jī)制,wait()和notify()。當(dāng)?shù)谝淮芜\(yùn)行生產(chǎn)現(xiàn)場(chǎng)時(shí)調(diào)用生產(chǎn)的方法,此時(shí)有一只烤鴨,即flag=false,無(wú)需等待,因此我們?cè)O(shè)置可消費(fèi)的烤鴨名稱然后改變flag=true,同時(shí)通知消費(fèi)線程可以消費(fèi)烤鴨了,即使此時(shí)生產(chǎn)線程再次搶到執(zhí)行權(quán),因?yàn)閒lag=true,所以生產(chǎn)線程會(huì)進(jìn)入等待阻塞狀態(tài),消費(fèi)線程被喚醒后就進(jìn)入消費(fèi)方法,消費(fèi)完成后,又改變標(biāo)志flag=false,通知生產(chǎn)線程可以生產(chǎn)烤鴨了.........以此循環(huán)。 生產(chǎn)消費(fèi)執(zhí)行類Single_Producer_Consumer.java:
很顯然的情況就是生產(chǎn)一只烤鴨然后就消費(fèi)一只烤鴨。運(yùn)行情況完全正常,嗯,這就是單生產(chǎn)者單消費(fèi)者模式。上面使用的是synchronized關(guān)鍵字的方式實(shí)現(xiàn)的,那么接下來(lái)我們使用對(duì)象鎖的方式實(shí)現(xiàn):KaoYaResourceByLock.java
代碼變化不大,我們通過(guò)對(duì)象鎖的方式去實(shí)現(xiàn),首先要?jiǎng)?chuàng)建一個(gè)對(duì)象鎖,我們這里使用的重入鎖ReestrantLock類,然后通過(guò)手動(dòng)設(shè)置lock()和unlock()的方式去獲取鎖以及釋放鎖。為了實(shí)現(xiàn)等待/通知機(jī)制,我們還必須通過(guò)鎖對(duì)象去創(chuàng)建一個(gè)條件對(duì)象Condition,然后通過(guò)鎖對(duì)象的await()和signalAll()方法去實(shí)現(xiàn)等待以及通知操作。Single_Producer_Consumer.java代碼替換一下資源類即可,運(yùn)行結(jié)果就不貼了,有興趣自行操作即可。 4.2多生產(chǎn)者多消費(fèi)者模式 分析完了單生產(chǎn)者單消費(fèi)者模式,我們?cè)賮?lái)聊聊多生產(chǎn)者多消費(fèi)者模式,也就是多條生產(chǎn)線程配合多條消費(fèi)線程。既然這樣的話我們先把上面的代碼Single_Producer_Consumer.java類修改成新類,大部分代碼不變,僅新增2條線程去跑,一條t1的生產(chǎn) 共享資源類KaoYaResource不作更改,代碼如下:
不對(duì)呀,我們才生產(chǎn)一只烤鴨,怎么就被消費(fèi)了3次啊,有的烤鴨生產(chǎn)了也沒(méi)有被消費(fèi)???難道共享數(shù)據(jù)源沒(méi)有進(jìn)行線程同步?我們?cè)倏纯粗暗腒aoYaResource.java
共享數(shù)據(jù)count的獲取方法都進(jìn)行synchronized關(guān)鍵字同步了呀!那怎么還會(huì)出現(xiàn)數(shù)據(jù)混亂的現(xiàn)象啊? 分析:確實(shí),我們對(duì)共享數(shù)據(jù)也采用了同步措施,而且也應(yīng)用了等待/通知機(jī)制,但是這樣的措施只在單生產(chǎn)者單消費(fèi)者的情況下才能正確應(yīng)用,但從運(yùn)行結(jié)果來(lái)看,我們之前的單生產(chǎn)者單消費(fèi)者安全處理措施就不太適合多生產(chǎn)者多消費(fèi)者的情況了。那么問(wèn)題出在哪里?可以明確的告訴大家,肯定是在資源共享類,下面我們就來(lái)分析問(wèn)題是如何出現(xiàn),又該如何解決?直接上圖 解決后的資源代碼如下只將if改為了while:
到此,多消費(fèi)者多生產(chǎn)者模式也完成,不過(guò)上面用的是synchronied關(guān)鍵字實(shí)現(xiàn)的,而鎖對(duì)象的解決方法也一樣將之前單消費(fèi)者單生產(chǎn)者的資源類中的if判斷改為while判斷即可代碼就不貼了哈。不過(guò)下面我們將介紹一種更有效的鎖對(duì)象解決方法,我們準(zhǔn)備使用兩組條件對(duì)象(Condition也稱為監(jiān)視器)來(lái)實(shí)現(xiàn)等待/通知機(jī)制,也就是說(shuō)通過(guò)已有的鎖獲取兩組監(jiān)視器,一組監(jiān)視生產(chǎn)者,一組監(jiān)視消費(fèi)者。有了前面的分析這里我們直接上代碼:
從代碼中可以看到,我們創(chuàng)建了producer_con 和consumer_con兩個(gè)條件對(duì)象,分別用于監(jiān)聽生產(chǎn)者線程和消費(fèi)者線程,在product()方法中,我們獲取到鎖后, 如果此時(shí)flag為true的話,也就是此時(shí)還有烤鴨未被消費(fèi),因此生產(chǎn)線程需要等待,所以我們調(diào)用生產(chǎn)線程的監(jiān)控器producer_con的 await()的方法進(jìn)入阻塞等待池;但如果此時(shí)的flag為false的話,就說(shuō)明烤鴨已經(jīng)消費(fèi)完,需要生產(chǎn)線程去生產(chǎn)烤鴨,那么生產(chǎn)線程將進(jìn)行烤 鴨生產(chǎn)并通過(guò)消費(fèi)線程的監(jiān)控器consumer_con的signal()方法去通知消費(fèi)線程對(duì)烤鴨進(jìn)行消費(fèi)。consume()方法也是同樣的道理,這里就不 過(guò)多分析了。我們可以發(fā)現(xiàn)這種方法比我們之前的synchronized同步方法或者是單監(jiān)視器的鎖對(duì)象都來(lái)得高效和方便些,之前都是使用 notifyAll()和signalAll()方法去喚醒池中的線程,然后讓池中的線程又進(jìn)入 競(jìng)爭(zhēng)隊(duì)列去搶占CPU資源,這樣不僅喚醒了無(wú)關(guān)的線程而且又讓全 部線程進(jìn)入了競(jìng)爭(zhēng)隊(duì)列中,而我們最后使用兩種監(jiān)聽器分別監(jiān)聽生產(chǎn)者線程和消費(fèi)者線程,這樣的方式恰好解決前面兩種方式的問(wèn)題所在, 我們每次喚醒都只是生產(chǎn)者線程或者是消費(fèi)者線程而不會(huì)讓兩者同時(shí)喚醒,這樣不就能更高效得去執(zhí)行程序了嗎?好了,到此多生產(chǎn)者多消 費(fèi)者模式也分析完畢。 5.線程死鎖 現(xiàn)在我們?cè)賮?lái)討論一下線程死鎖問(wèn)題,從上面的分析,我們知道鎖是個(gè)非常有用的工具,運(yùn)用的場(chǎng)景非常多,因?yàn)樗褂闷饋?lái)非常簡(jiǎn)單,而 且易于理解。但同時(shí)它也會(huì)帶來(lái)一些不必要的麻煩,那就是可能會(huì)引起死鎖,一旦產(chǎn)生死鎖,就會(huì)造成系統(tǒng)功能不可用。我們先通過(guò)一個(gè)例 子來(lái)分析,這個(gè)例子會(huì)引起死鎖,使得線程t1和線程t2互相等待對(duì)方釋放鎖。
同步嵌套是產(chǎn)生死鎖的常見情景,從上面的代碼中我們可以看出,當(dāng)t1線程拿到鎖A后,睡眠2秒,此時(shí)線程t2剛好拿到了B鎖,接著要獲取A鎖,但是此時(shí)A鎖正好被t1線程持有,因此只能等待t1線程釋放鎖A,但遺憾的是在t1線程內(nèi)又要求獲取到B鎖,而B鎖此時(shí)又被t2線程持有,到此結(jié)果就是t1線程拿到了鎖A同時(shí)在等待t2線程釋放鎖B,而t2線程獲取到了鎖B也同時(shí)在等待t1線程釋放鎖A,彼此等待也就造成了線程死鎖問(wèn)題。雖然我們現(xiàn)實(shí)中一般不會(huì)向上面那么寫出那樣的代碼,但是有些更為復(fù)雜的場(chǎng)景中,我們可能會(huì)遇到這樣的問(wèn)題,比如t1拿了鎖之后,因?yàn)橐恍┊惓G闆r沒(méi)有釋放鎖(死循環(huán)),也可能t1拿到一個(gè)數(shù)據(jù)庫(kù)鎖,釋放鎖的時(shí)候拋出了異常,沒(méi)有釋放等等,所以我們應(yīng)該在寫代碼的時(shí)候多考慮死鎖的情況,這樣才能有效預(yù)防死鎖程序的出現(xiàn)。下面我們介紹一下避免死鎖的幾個(gè)常見方法: 1.避免一個(gè)線程同時(shí)獲取多個(gè)鎖。 2.避免在一個(gè)資源內(nèi)占用多個(gè) 資源,盡量保證每個(gè)鎖只占用一個(gè)資源。 3.嘗試使用定時(shí)鎖,使用tryLock(timeout)來(lái)代替使用內(nèi)部鎖機(jī)制。 4.對(duì)于數(shù)據(jù)庫(kù)鎖,加鎖和解鎖必須在一個(gè)數(shù)據(jù)庫(kù)連接里,否則會(huì)出現(xiàn)解鎖失敗的情況。 5.避免同步嵌套的發(fā)生 6.Thread.join() 如果一個(gè)線程A執(zhí)行了thread.join()語(yǔ)句,其含義是:當(dāng)前線程A等待thread線程終止之后才能從thread.join()返回。線程Thread除了提供join()方法之外,還提供了join(long millis)和join(long millis,int nanos)兩個(gè)具備超時(shí)特性的方法。這兩個(gè)超時(shí)的方法表示,如果線程在給定的超時(shí)時(shí)間里沒(méi)有終止,那么將會(huì)從該超時(shí)方法中返回。下面給出一個(gè)例子,創(chuàng)建10個(gè)線程,編號(hào)0~9,每個(gè)線程調(diào)用錢一個(gè)線程的join()方法,也就是線程0結(jié)束了,線程1才能從join()方法中返回,而0需要等待main線程結(jié)束。
好了,到此本篇結(jié)束。 |
|