Java中多線程同步問題、生產(chǎn)者與消費(fèi)者、守護(hù)線程和volatile關(guān)鍵字(附帶相關(guān)面試題)

1.多線程同步問題(關(guān)鍵字Synchronized)
問題:多線程訪問同一個資源時候可能就會出現(xiàn)資源完整性的問題
所以引入關(guān)鍵字synchronized(同步)
synchronized關(guān)鍵字的作用機(jī)制是給對象加鎖,并為每個線程提供了一個計數(shù)器,初始值為0。當(dāng)?shù)谝粋€線程獲得鎖時,計數(shù)器變?yōu)?,其他線程被阻塞。當(dāng)?shù)谝粋€線程執(zhí)行完代碼并釋放鎖時,計數(shù)器歸零,意味著資源可用,所有被阻塞的線程將恢復(fù)執(zhí)行。
一個通俗的比喻是廁所的使用情況。假設(shè)只有一個廁所位置但有很多人需要使用。當(dāng)?shù)谝粋€人進(jìn)入廁所并鎖上門時,其他人不得不在外面等待。當(dāng)?shù)谝粋€人使用完畢并打開門鎖時,表示廁所空閑可用,所有等待的人可以繼續(xù)使用。
關(guān)于synchronized有兩種用法
1.設(shè)置同步代碼塊
意味著只有同步代碼塊內(nèi)部的代碼需要同步,其他操作無需同步
2.設(shè)置同步代碼方法
這個方法就是整個方法內(nèi)的代碼都是同步的
注意:在生產(chǎn)案例中不要隨意使用同步方法,因為一旦同步,整個程序的運(yùn)行效率就會非常低,比如10個學(xué)生想要去學(xué)校上廁所,那么最好的操作就是讓10個學(xué)生一起先到學(xué)校再同步操作上廁所,而不是10個學(xué)生其中某一個去學(xué)校上完廁所,其他學(xué)生才去學(xué)校上廁所

關(guān)于同步案例:(多個售票員售賣固定數(shù)量的票)
在上一篇文章的代碼中就實(shí)現(xiàn)到這一步
這一步實(shí)現(xiàn)了售票員之間的售賣間隔,就不會一下子就把所有票賣光。
現(xiàn)在通過同步代碼塊方法實(shí)現(xiàn)同步:
由于票太多所以為了方便顯示就改成5了

面試題: Synchronized 用過嗎,其原理是什么?
(1)可重入性
synchronized的鎖對象中有一個計數(shù)器(recursions變量)會記錄線程獲得幾次鎖;
??? 可重入的好處:
??? 可以避免死鎖;
??? 可以讓我們更好的封裝代碼;synchronized是可重入鎖,每部鎖對象會有一個計數(shù)器記錄線程獲取幾次鎖,在執(zhí)行完同步代碼塊時,計數(shù)器的數(shù)量會-1,直到計數(shù)器的數(shù)量為0,就釋放這個鎖。
(2)不可中斷性
??? 一個線程獲得鎖后,另一個線程想要獲得鎖,必須處于阻塞或等待狀態(tài),如果第一個線程不釋放鎖,第二個線程會一直阻塞或等待,不可被中斷;
??? synchronized 屬于不可被中斷;
??? Lock lock方法是不可中斷的;
??? Lock tryLock方法是可中斷的;
?面試題:為什么說 Synchronized 是非公平鎖?
當(dāng)鎖被釋放后,任何一個線程都有機(jī)會競爭得到鎖,這樣做的目的是提高效率,但缺點(diǎn)是可能產(chǎn)生線程饑餓現(xiàn)象。
面試題:為什么說 Synchronized 是一個悲觀鎖?樂觀鎖的實(shí)現(xiàn)原理又是什么?什么是 CAS,它有什么特性?
Synchronized的并發(fā)策略是悲觀的,不管是否產(chǎn)生競爭,任何數(shù)據(jù)的操作都必須加鎖。
樂觀鎖的核心是CAS,CAS包括內(nèi)存值、預(yù)期值、新值,只有當(dāng)內(nèi)存值等于預(yù)期值時,才會將內(nèi)存值修改為新值。

2.?Object線程的等待與喚醒方法
注意:
這些方法必須在同步塊或同步方法中使用,因為它們會改變對象的內(nèi)部鎖狀態(tài)。調(diào)用
wait()
方法將釋放當(dāng)前線程持有的對象鎖,并使線程進(jìn)入等待狀態(tài)。而調(diào)用notify()
或notifyAll()
方法會喚醒等待在該對象上的線程,并將其重新放入可運(yùn)行狀態(tài)。
案例要求:設(shè)置一個圖書類,一個圖書管理員可以放圖書的書名和作者,一個讀者可以看圖書的書名和作者

?在網(wǎng)絡(luò)延遲的情況下可能會出現(xiàn)問題:
1.數(shù)據(jù)不匹配(解決需要-》結(jié)果能夠一一對應(yīng))
2.重復(fù)取同一個數(shù)據(jù)(解決需要--》每次都只取一次,只有更改后再?。?/p>
解決方法:
1.數(shù)據(jù)錯亂,根本原因在于多線程下,圖書管理員線程在設(shè)置圖書信息到一半的時候,讀者就讀取圖書信息造成圖書信息錯亂,解決方法很簡單,只需要在book下將所有g(shù)et和set方法設(shè)置為同步代碼方法就可以解決數(shù)據(jù)錯亂了
2.其原理也很簡單就是因為同步代碼塊,所以在完成Manger在執(zhí)行完set前不會執(zhí)行g(shù)et。Reader在執(zhí)行完get前也不會執(zhí)行set

雖然解決了數(shù)據(jù)錯亂的問題但是這樣的數(shù)據(jù)出現(xiàn)有重復(fù),我們的目標(biāo)是需要西游記后輸出天龍八部的交替輸出。這就是最基礎(chǔ)的生產(chǎn)者消費(fèi)者模型了。
那么如何具體實(shí)現(xiàn)呢?就需要用到前面給的方法,等待喚醒機(jī)制,即wait();與notify();
其工作原理是設(shè)置一個標(biāo)志位(資源量)當(dāng)一個Reader讀取時候就設(shè)置為true,maneger就是false并且進(jìn)入沉睡。當(dāng)Reader執(zhí)行完任務(wù)后又將標(biāo)志位設(shè)置為false 并讓執(zhí)行notify()喚醒其他熟睡的線程
案例實(shí)現(xiàn)代碼:

面試題: Java 如何實(shí)現(xiàn)多線程之間的通訊和協(xié)作?
1.可以通過中斷 和 共享變量的方式實(shí)現(xiàn)線程間的通訊和協(xié)作
比如說最經(jīng)典的生產(chǎn)者-消費(fèi)者模型:當(dāng)隊列滿時,生產(chǎn)者需要等待隊列有空間才能繼續(xù)往里面放入商品,而在等待的期間內(nèi),生產(chǎn)者必須釋放對臨界資源(即隊列)的占用權(quán)。因為生產(chǎn)者如果不釋放對臨界資源的占用權(quán),那么消費(fèi)者就無法消費(fèi)隊列中的商品,就不會讓隊列有空間,那么生產(chǎn)者就會一直無限等待下去。因此,一般情況下,當(dāng)隊列滿時,會讓生產(chǎn)者交出對臨界資源的占用權(quán),并進(jìn)入掛起狀態(tài)。然后等待消費(fèi)者消費(fèi)了商品,然后消費(fèi)者通知生產(chǎn)者隊列有空間了。同樣地,當(dāng)隊列空時,消費(fèi)者也必須等待,等待生產(chǎn)者通知它隊列中有商品了。這種互相通信的過程就是線程間的協(xié)作。
Java中線程通信協(xié)作的最常見的兩種方式:
1、syncrhoized加鎖的線程的Object類的wait()/notify()/notifyAll()
2、ReentrantLock類加鎖的線程的Condition類的await()/signal()/signalAll()
線程間直接的數(shù)據(jù)交換:
通過管道進(jìn)行線程間通信:1)字節(jié)流;2)字符流

?3.模擬生產(chǎn)者與消費(fèi)者
模擬生產(chǎn)者與消費(fèi)者
通過上面一個案例應(yīng)該就有對生產(chǎn)者消費(fèi)者模型有初步了解,下面將舉一個十分經(jīng)典的消費(fèi)者生產(chǎn)者模型讓大家有更深層次的理解
案例目標(biāo):設(shè)計一個生產(chǎn)計算機(jī)類與一個搬運(yùn)計算機(jī)類,要求是生產(chǎn)者生產(chǎn)一臺計算機(jī)就要搬走一臺計算機(jī),如果沒有新的計算機(jī)那么搬運(yùn)工就要等待新的計算機(jī)產(chǎn)出,如果生產(chǎn)出的計算機(jī)沒有被搬走就要等待搬運(yùn)者將計算機(jī)搬走,最后搬運(yùn)統(tǒng)計搬運(yùn)走的電腦個數(shù)
案例代碼:


4.守護(hù)線程
一個進(jìn)程的運(yùn)行往往可能需要十分多的子進(jìn)程輔助運(yùn)行,比如聊天軟件,主線程是軟件的使用。而所有的聊天對象都是子線程可以分別接收消息。當(dāng)軟件關(guān)閉主線程時候即關(guān)閉軟件使用時候,此時子線程的存在就沒有了意義。就會自動關(guān)閉。這樣的子線程就叫做守護(hù)線程
設(shè)置守護(hù)線程
Thread 對象.setDeamon(true/false);true-》開啟守護(hù)線程,false-關(guān)閉守護(hù)線程
具體應(yīng)用案例:


5.volatile關(guān)鍵字
一般情況下比如之前的售票員售票其調(diào)用ticket時候是進(jìn)行先復(fù)制其數(shù)據(jù)副本再通過加載-》使用-》賦值-》存儲-》寫入才對內(nèi)存的數(shù)據(jù)ticket進(jìn)行同步,就是線程操作的數(shù)據(jù)都只是原始數(shù)據(jù)的備份,在操作完成后再和原始數(shù)據(jù)進(jìn)行替換。而volatile則不需要這些數(shù)據(jù)備份直接操作內(nèi)存的原始數(shù)據(jù)
好處:volatile關(guān)鍵字可以直接對內(nèi)存進(jìn)行操作,就不需要同步數(shù)據(jù)了,所以可以減少程序運(yùn)行的時間
使用案例代碼:
*面試題:volatile 關(guān)鍵字的作用
對于可見性,Java 提供了 volatile 關(guān)鍵字來保證可見性和禁止指令重排。 volatile 提供 happens-before 的保證,確保一個線程的修改能對其他線程是可見的。當(dāng)一個共享變量被 volatile 修飾時,它會保證修改的值會立即被更新到主存,當(dāng)有其他線程需要讀取時,它會去內(nèi)存中讀取新值。
從實(shí)踐角度而言,volatile 的一個重要作用就是和 CAS 結(jié)合,保證了原子性,詳細(xì)的可以參見 java.util.concurrent.atomic 包下的類,比如 AtomicInteger。
volatile 常用于多線程環(huán)境下的單次操作(單次讀或者單次寫)。
?*面試題:既然 volatile 能夠保證線程間的變量可見性,是不是就意味著基于 volatile 變量的運(yùn)算就是并發(fā)安全的?
volatile修飾的變量在各個線程的工作內(nèi)存中不存在一致性的問題(在各個線程工作的內(nèi)存中,volatile修飾的變量也會存在不一致的情況,但是由于每次使用之前都會先刷新主存中的數(shù)據(jù)到工作內(nèi)存,執(zhí)行引擎看不到不一致的情況,因此可以認(rèn)為不存在不一致的問題),但是java的運(yùn)算并非原子性的操作,導(dǎo)致volatile在并發(fā)下并非是線程安全的。
??*面試題:請談?wù)?volatile 有什么特點(diǎn),為什么它能保證變量對所有線程的可見性?
volatile只能作用于變量,保證了操作可見性和有序性,不保證原子性。
在Java的內(nèi)存模型中分為主內(nèi)存和工作內(nèi)存,Java內(nèi)存模型規(guī)定所有的變量存儲在主內(nèi)存中,每條線程都有自己的工作內(nèi)存。
主內(nèi)存和工作內(nèi)存之間的交互分為8個原子操作:
??? lock
??? unlock
??? read
??? load
??? assign
??? use
??? store
??? writevolatile修飾的變量,只有對volatile進(jìn)行assign操作,才可以load,只有l(wèi)oad才可以use,,這樣就保證了在工作內(nèi)存操作volatile變量,都會同步到主內(nèi)存中。