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