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