Java并發(fā)學(xué)習(xí)2【面試+工作】
??關(guān)鍵字synchronized的作用是實現(xiàn)進(jìn)程間的同步。它的工作是對同步的代碼加鎖,使得每一次,只能有一個線程進(jìn)入同步塊,從而保證線程間的安全性(即同步塊每次應(yīng)該只有一個線程可以執(zhí)行)。
??關(guān)鍵字synchronized可以有多重用法,這里做一個簡單的整理。
制定加鎖對象(同一對象)。對給定對象加鎖,進(jìn)入同步代碼前需要獲得給定對象的鎖。
直接作用于實例方法。相當(dāng)于對當(dāng)前示例加鎖(同一對象),進(jìn)入同步代碼前要獲得當(dāng)前示例的鎖。
直接作用于靜態(tài)方法。相當(dāng)于對當(dāng)前類加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類的鎖。
??學(xué)習(xí)過《操作系統(tǒng)》的,這里非常容易理解,不在舉具體的例子。
??除了用于線程同步、確保線程安全外,synchronized還可以保證線程間的可見性和有序性。從可見性的角度講,synchronized可以完全替代volatile的功能,只是使用上沒有那么方便而已。就有序性而言,被synchronized限制的多線程其實是串行的執(zhí)行同步代碼的。
??用一句話概括volatile,它能夠使變量在值發(fā)生改變時能盡快地讓其他線程知道.
??首先我們要先意識到有這樣的現(xiàn)象,編譯器為了加快程序運(yùn)行的速度,對一些變量的寫操作會先在寄存器或者是CPU緩存上進(jìn)行,最后才寫入內(nèi)存.
而在這個過程,變量的新值對其他線程是不可見的.而volatile的作用就是使它修飾的變量的讀寫操作都必須在內(nèi)存中進(jìn)行!
volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器中的值是不確定的,需要從主存中讀取,synchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問該變量,其他線程被阻塞住.
volatile僅能使用在變量級別,synchronized則可以使用在變量,方法.
volatile僅能實現(xiàn)變量的修改可見性,但不具備原子特性,而synchronized則可以保證變量的修改可見性和原子性.
volatile不會造成線程的阻塞,而synchronized可能會造成線程的阻塞.
volatile標(biāo)記的變量不會被編譯器優(yōu)化,而synchronized標(biāo)記的變量可以被編譯器優(yōu)化.
??因此,在使用volatile關(guān)鍵字時要慎重,并不是只要簡單類型變量使用volatile修飾,對這個變量的所有操作都是原來操作,當(dāng)變量的值由自身的上一個決定時,如n=n+1、n++ 等,volatile關(guān)鍵字將失效,只有當(dāng)變量的值和自身上一個值無關(guān)時對該變量的操作才是原子級別的,如n = m + 1,這個就是原級別的。所以在使用volatile關(guān)鍵時一定要謹(jǐn)慎,如果自己沒有把握,可以使用synchronized來代替volatile。
??總結(jié):volatile本質(zhì)是在告訴JVM當(dāng)前變量在寄存器中的值是不確定的,需要從主存中讀取??梢詫崿F(xiàn)synchronized的部分效果,但當(dāng)n=n+1,n++等時,volatile關(guān)鍵字將失效,不能起到像synchronized一樣的線程同步的效果
??這里介紹一下synchronized、wait、notify方法的替代品(或者說是增強(qiáng)版)-重入鎖。重入鎖是可以完全替代以上的內(nèi)容的。并且重入鎖的性能是遠(yuǎn)高于synchronized的,但是jdk6.0開始,jdk對synchronized做了大量的優(yōu)化,使得兩者性能差距不大。
??重入鎖使用java.util.concurrent.locks.ReentrantLock類來實現(xiàn)。它的幾個重要方法如下:
注意:把解鎖操作lock.unlock()放到finally子句非常重要。這樣保證即使在臨界區(qū)的代碼拋出了異常,鎖也必須釋放,否則,其他線程將永遠(yuǎn)阻塞.
??上述代碼使用重入鎖保護(hù)臨界區(qū)資源i,確保了多線程對i操作的安全性。從這段代碼可以看到,與synchronized相比,重入鎖有著顯示的操作過程。開發(fā)人員必須手動指定何時加鎖,何時釋放鎖。也正因為這樣,重入鎖對邏輯控制的靈活性要遠(yuǎn)遠(yuǎn)好于synchronized,但值得注意的是,在提出臨界區(qū)時,必須記得釋放鎖,否則其他線程就沒有機(jī)會再訪問臨界區(qū)了。
??對于重入鎖,同一個線程可以多次獲得鎖,但是釋放鎖的時候,也必須釋放相同次數(shù)。否則會產(chǎn)生異常。
??如果大家理解了obj.wait和obj.notify方法的話,那么就很容易理解Condition對象了。它和wait和notify方法的作用是大致相同的。但是wait和notify方法是和synchronized關(guān)鍵字合作使用的,而Condition是與重入鎖相關(guān)聯(lián)的。通過Condition的newCondition()方法可以生成一個與當(dāng)前重入鎖綁定的Condition實例。利用Condition對象,我們就可以讓線程在合適的時間等待,或者在某一個特定的時間得到通知,繼續(xù)執(zhí)行。
Condition提供的基本方法如下:
以上方法的具體含義如下:
await() 方法會是當(dāng)前線程等待,同時釋放當(dāng)前鎖,當(dāng)其他線程中使用signal() 或signalAll() 方法時,線程會重新獲得鎖并繼續(xù)執(zhí)行。或者當(dāng)線程被中斷時,也能跳出等待。這和obj.wait方法很相似。
awaitUninterruptibly() 方法與await() 方法基本相同,只不過它不會在等待過程中響應(yīng)中斷。
signal() 方法用于喚醒一個在等待中的線程,這和obj.notify方法很類似。
??代碼中,聽過lock生成一個與之綁定的Condition對象。代碼15行要求線程在Condition對象上進(jìn)行等待。代碼32行,由主線程發(fā)起通知,告知等待在Condition上的線程可以繼續(xù)執(zhí)行了。
??和obj.wait和notify方法一樣,當(dāng)線程使用Condition.await時,要求線程持有相關(guān)的重入鎖,在Condition.await調(diào)用后,這個線程會釋放這把鎖。同理,在Condition.signal方法調(diào)用時,也要求線程先獲得相關(guān)的鎖。在signal方法調(diào)用后,系統(tǒng)會從當(dāng)前Condition對象的等待隊列中,喚醒一個線程。一旦線程被喚醒,它會重新嘗試獲得與之綁定的重入鎖,一旦成功獲取,就可以繼續(xù)執(zhí)行了。因此,在signal方法調(diào)用之后,一般需要釋放相關(guān)的鎖,謙讓給被喚醒的線程,讓他可以繼續(xù)執(zhí)行。比如,在本例中,第33行就釋放了重入鎖,如果省略第24行,那么,雖然已經(jīng)喚醒了線程t1,但是由于它無法重新獲得鎖,因而也就無法真正的繼續(xù)執(zhí)行。
??在jdk內(nèi)部,重入鎖和Condition對象被廣泛的使用,后面講到的線程安全的容器,他們的內(nèi)容時候都有重入鎖和Condition對象的影子。
五.信號量
信號量為多線程協(xié)作提供了更為強(qiáng)大的控制方法。廣義上說,信號量是對鎖的擴(kuò)展。無論是內(nèi)部鎖synchronized還是重入鎖ReentrantLock,一次都只允許一個線程訪問一個資源,而信號量卻可以指定多個線程,同時訪問某一個資源。信號量主要提供了一下構(gòu)造函數(shù):
??在構(gòu)造信號量對象時,必須要指定信號量的準(zhǔn)入數(shù),即同時能申請多少個許可。當(dāng)每個線程每次只申請一個許可時,這就相當(dāng)于指定了同時有多少個線程可以訪問某一個資源。信號量的主要邏輯方法有:
這里只講幾個常用的方法:
acquire() 方法嘗試獲得一個準(zhǔn)入的許可。若無法獲得,則線程會等待,直到有線程釋放一個許可或者當(dāng)前線程被中斷。
acquireUninterruptibly方法和acquire方法類似,但是不響應(yīng)中斷。
tryAcquire嘗試獲得一個許可,如果成功返回true,失敗返回false,它不會進(jìn)行等待,立即返回。
release用于在線程訪問資源結(jié)束后,釋放一個許可,以使其他等待許可的線程可以進(jìn)行資源訪問。
??上述代碼中,15、16行為臨界區(qū),程序會限制執(zhí)行這段代碼的線程數(shù)。這里在第7行,聲明了一個包含5個許可的信號量。這就意味著同時可以有5個線程進(jìn)入代碼段15,16行。申請信號量使用semp.acquire,在離開時,務(wù)必使用semp.release釋放信號量。這就和釋放鎖一個道理。
聯(lián)系客服