這是該系列教程的第二篇.其中用到了一個工具類,和其中的兩個方法.如下圖所示:
Synchronized
在第一篇教程中,我們已經(jīng)介紹了如何通過** Executor Service**來并行執(zhí)行任務(wù).但是,這也引入了一個新的問題.即我們?nèi)绾尾l(fā)的訪問那些共享的變量.假設(shè)我們打算用多個線程來并發(fā)地增加一個數(shù)字.我們使用下面的代碼:
我們可以看到,其結(jié)果不是正確的結(jié)果,即10000.那為什么會出現(xiàn)這種情況呢?這是因為我們不恰當?shù)淖尪鄠€線程并發(fā)的訪問并設(shè)置共享變量而造成的.本例中,共享變量即為** count**.
當執(zhí)行一個加法操作時,需要按三步進行:1.讀取當前值2.使當前值加一3.將這個新值寫入到變量中.如果有兩個線程同時執(zhí)行第一步,即讀取當前值,它們獲得了相同的值,就會造成寫丟失.也就是說,第二個線程在執(zhí)行第三步時,會覆蓋掉第一個進程寫入的結(jié)果.
Java中,我們可以通過使用** synchronized**關(guān)鍵字,來防止這種情況的出現(xiàn).
我們使用synchronized關(guān)鍵字來改寫上面的執(zhí)行加法操作的函數(shù):
現(xiàn)在我們讓線程通過執(zhí)行這個函數(shù)來并發(fā)地進行加法操作:
現(xiàn)在我們可以看到,結(jié)果是正確的.不管你執(zhí)行多少次,結(jié)果都是正確的.
** synchronized**關(guān)鍵字,不僅可以用在函數(shù)上,還可以用在代碼塊中:
在** synchronized關(guān)鍵字的內(nèi)部, Java使用一個叫做 monitor的東西來管理它, monitor也被稱作 monitor lock 或 intrinsic lock. monitor是和對象關(guān)聯(lián)在一起的,每個 synchronized的方法,針對一個對象,都使用同一個monitor**.
** synchronized**也有可重入的特性,也就是說,即使當前線程已經(jīng)占有了鎖,它還是可以請求相同的鎖的,這就避免了死鎖的產(chǎn)生.
Locks
除了通過使用** synchronized**這種隱式鎖來進行同步,Concurrency API中,還提供了大量顯式鎖.通過使用這些顯式鎖,我們能對并發(fā)進行更好的控制.
下面我們會一個個的介紹這些顯式鎖.
ReentrantLock
這個鎖是一個互斥鎖.它也實現(xiàn)了** synchronized**中的隱式鎖的基本特性,當然,它還是有一些自己的特性的.這個鎖也是可重入的.
我們使用** ReentrantLock**來實現(xiàn)上面的例子:
我們通過** lock()方法來獲得鎖,而通過 unlock()**方法,來釋放鎖.我們要用try/catch來包裝我們的代碼,來防止鎖得不到釋放.這個函數(shù)也是線程安全的.如果其他線程在這個鎖沒有釋放之前,想要獲得鎖,就會被阻塞.只有一個線程能夠同時占有鎖.
除此之外,鎖還提供了其他的函數(shù),如下圖所示:
第一個任務(wù)獲得鎖,然后暫停一秒鐘.而第二個任務(wù)則獲取鎖的當前狀態(tài),并將其輸出出來.
** tryLock()會嘗試獲取鎖,而不會像 lock()**方法一樣,阻塞線程.我們需要在執(zhí)行那些需要訪問共享變量的操作之前,檢查一下其返回值,以防止不同步的情況.
ReadWriteLock
** ReadWriteLock包含一對鎖,分別用于對共享變量的讀和寫操作. ReadWriteLock**背后的原理是,如果當前沒有線程來修改共享變量,那么允許多個線程來訪問共享變量,而沒有什么危險.所以說,當沒有線程持有寫鎖的時候,可以有多個線程持有讀鎖.這在那些讀操作遠大于寫操作的場景中,極大的提高了性能和吞吐量.
在你執(zhí)行上面的代碼時,你會注意到,只有當寫鎖被釋放之后,后面的線程才會同時獲取到讀鎖.而不需要等第一個線程的讀鎖釋放之后,第二個線程才能獲取讀鎖.
StampedLock
Java8中,還增加了一種叫做** StampedLock的鎖,這個鎖也有讀鎖和寫鎖,就跟上面的 ReadWriteLock一樣.但是,和 ReadWriteLock不同,它會返回一個 long**類型的值,我們可以通過這個值來釋放鎖,檢查鎖是否有效.另外,它還包含一種叫做樂觀鎖的鎖.
我們有下面的代碼:
我們通過** readLock()方法來獲取讀鎖,通過 writeLock()方法來獲取寫鎖.需要注意的是, StampedLock**并沒有可重入的特性.如果沒有鎖可用,則調(diào)用上面的方法,會導致返回一個值,并阻塞線程,即使當前線程已經(jīng)有鎖了.所以你使用這個鎖的時候,要小心,別出現(xiàn)死鎖的情況.
跟** ReadWriteLock**鎖一樣,要獲得讀鎖,必須等待寫鎖被釋放.
我們使用下面的這個例子,來了解樂觀鎖:
** tryOptimisticRead()方法,會獲得一個樂觀讀鎖.它總會返回一個值,而不會阻塞當前線程.如果有線程持有寫鎖,則返回值是0.所以,我們需要通過 lock.validate()**方法來檢查一下返回值,來確定是否真的有讀鎖可用.
上面的代碼,輸出如下:
我們應(yīng)當在獲取到樂觀鎖之后,就立即使用它.因為它隨時可能無效.與平常的讀鎖不同,樂觀鎖不會阻止其他的線程獲得寫鎖.也就是說,在一個線程占有樂觀鎖的時候,其他的線程還是可以獲取到寫鎖的,而不需要等待樂觀鎖被釋放.當其他線程獲得了寫鎖之后,樂觀鎖就失效了.即使那個線程后來又釋放了寫鎖.
所以,如果使用樂觀鎖的話,我們需要時刻驗證樂觀鎖是否還有效.特別是在執(zhí)行對共享變量的寫操作之前.
有時,我們需要將一個讀鎖轉(zhuǎn)換成寫鎖.** StampedLock提供了 tryConvertToWriteLock()**這個方法,來進行轉(zhuǎn)換.
上面的那個任務(wù),會先獲取讀鎖,并嘗試輸出count變量的值.但是當其等于0時,它會將讀鎖轉(zhuǎn)換成寫鎖,如果轉(zhuǎn)換成功,則將23賦給count.如果不成功,則直接通過** lock.writeLock()**來以阻塞的方式獲取寫鎖.然后將23賦給count.最后,再釋放鎖.
Semaphores
除了鎖,Concurrency API也支持信號量.鎖通常用于互斥的訪問某些資源,而信號量用于限制一定的線程可以同時訪問某些資源.
下面這個例子,演示了如何使用信號量:
Executor允許同時執(zhí)行十個線程,但是因為我們將信號量設(shè)置成了5.所以實際上,只有5個線程有機會來執(zhí)行操作.
輸出如下:
只有前五個線程,能夠獲得一個信號量,然后來執(zhí)行暫停五分鐘的操作.其他的線程,因為沒有信號量可以獲取了,就只能在控制臺打印處** Could not acquire semaphore**.