併發筆記傳送門:
1.0 併發編程-思維導圖
2.0 併發編程-線程安全基礎
3.0 併發編程-基礎構建模塊
4.0 併發編程-任務執行-Future
5.0 併發編程-多線程的性能與可伸縮性
6.0 併發編程-顯式鎖與synchronized
7.0 併發編程-AbstractQueuedSynchronizer
8.0 併發編程-原子變量和非阻塞同步機制
顯式鎖
Java 5之前,在協調共享對象的訪問時可以使用的機制只有
synchronized
和volatile
。Java 5增加了ReentrantLock
。ReentrantLock
並不是一種替代內置加鎖的方法,而是當內置鎖機制不適用時,作爲一種可選擇的高級功能。
Lock 與 ReentrantLock
Lock 提供了一中無條件的、可輪詢的、定時的以及可中斷的所獲取操作,所有加鎖和解鎖的方法都是顯式的。
在Lock的實現中必須提供與內部鎖相同的內存可見性語義,但在加鎖語義、調度算法、順序保證以及性能特性等方面可以有所不同。
package java.util.concurrent.locks;
/**
* @see ReentrantLock
* @see Condition
* @see ReadWriteLock
*
* @since 1.5
* @author Doug Lea
*/
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
ReentrantLock
實現了 Lock
接口,並提供了與synchronized
相同的互斥行和內存可見性。並且與synchronized
一樣,ReentrantLock
還提供了可重入(可重入就是說某個線程已經獲得某個鎖,可以再次獲取鎖而不會出現死鎖。)
的加鎖語義。
爲什麼要和創建一種與內存鎖如此相似的新加鎖機制?
在大多數情況下,內置鎖都能很好地工作,但在功能上存在一些侷限性。例如,無法中斷一個正在等待獲取鎖的線程,或者無法在請求一個鎖時無限地等待下去。
內置鎖必須在獲取該鎖的代碼塊中釋放,這就簡化了編碼工作,並且與異常處理操作實現了很好的交互,但卻無法實現非阻塞結構的加鎖規則。這些都是使用synchronized
的原因,但是在某些情況下,一種更靈活的加鎖機制通常能夠提供更好的活躍性或性能。
顯式調用Lock,必須在finally中釋放鎖,雖然在finally中釋放鎖並不困難,但也可能忘記。
輪詢鎖與定時鎖
可定時的與可輪詢的鎖獲取模式是由
tryLock
方法實現的,與無條件的鎖獲取模式相比,它具有更完善的錯誤恢復機制。
可中斷的鎖獲取操作
lockInterruptibly
方法能夠在獲得鎖的同時保持對中斷的響應。
代碼在Log下面!!!!!
lock()方法打印:線程1獲取不到鎖後會一直等待鎖的釋放,並且不會響應中斷,當線程0釋放鎖後,線程1才恢復對中斷的響應。
Thread-0:start get lock
Thread-0:already get lock
Thread-1:start get lock
Thread-0:working num 0
Thread-0:working num 1
Thread-0:working num 2
Thread-0:working num 3
Thread-0:working num 4
Thread-0:working num 5
Thread-0: release unlock
Thread-1:already get lock
Thread-1:Interrupt
Thread-1: release unlock
lockInterruptibly()方法打印:線程1在獲取不到鎖後能夠及時響應中斷。
Thread-0:start get lock
Thread-0:already get lock
Thread-1:start get lock
Thread-1:Interrupt
Thread-1: unlock failed
Thread-1: failed desc:null
Thread-0:working num 0
Thread-0:working num 1
Thread-0:working num 2
Thread-0:working num 3
Thread-0:working num 4
Thread-0:working num 5
Thread-0: release unlock
跑一下示例代碼就清楚lockInterruptibly
和lock
的區別了。
public static void main(String[] args) throws InterruptedException {
LockTest lockTest = new LockTest();
Thread t0 = new Thread(new Runnable(){
@Override
public void run() {
lockTest.doWork();
}
});
Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
lockTest.doWork();
}
});
// 啓動線程t1
t0.start();
Thread.sleep(10);
// 啓動線程t2
t1.start();
Thread.sleep(100);
// 線程t1沒有得到鎖,中斷t1的等待
t1.interrupt();
}
class LockTest {
private Lock lock = new ReentrantLock();
public void doWork() {
String name = Thread.currentThread().getName();
try {
System.out.println(name + ":start get lock");
//lock.lock();
lock.lockInterruptibly();
System.out.println(name + ":already get lock");
for (int i = 0; i < 6; i++) {
Thread.sleep(1000);
System.out.println(name + ":working num "+ i);
}
} catch (InterruptedException e) {
System.out.println(name + ":Interrupt");
}finally{
try {
lock.unlock();
System.out.println(name + ": release unlock");
} catch (Exception e) {
System.out.println(name + ": unlock failed");
System.out.println(name + ": failed desc:" + e.getMessage());
}
}
}
}
性能考慮因素
在Java 5 新增ReentrantLock
時,它能比內置鎖提供更好的競爭性能。Java 6 使用了改進後的算法來管理內置鎖,使得內置鎖與ReentrantLock
在吞吐量上相差無幾,二者的可伸縮性基本相當。
公平性
在
ReentrantLock
的構造函數中提供了兩種公平性選擇:創建一個非公平的鎖(默認)或者一個公平的鎖。
- 公平的鎖:線程按照它們發出請求的順序來獲得鎖。
- 非公平的鎖:如果線程在發出請求的同時該鎖的狀態變爲可用,那麼這個線程將跳過所有等待的線程並獲得這個鎖。
在公平鎖中,如果有另一個線程正在持有鎖或者有其他線程在隊列中等待這個鎖,那麼新發出請求的線程將被放入隊列中。在非公平的鎖中,只有當鎖被某個線程持有時,新發出請求的線程纔會被放入隊列中。
我們爲什麼不希望所有的鎖都是公平的?
當執行加鎖操作時,使用公平的鎖在掛起線程和恢復線程時產生的開銷會極大地降低性能。而且在實際情況中,統計上的公平性保證(確保被阻塞的線程能最終獲得鎖),通常已經夠用了,並且產生的開銷會小很多。有些依賴於公平的排隊算法來保證業務的正確性,但這些算法並不常見。在大多數情況下,非公平鎖的性能要高於公平鎖的性能。
在synchronized
和ReentrantLock
之間的選擇
ReentrantLock
在加鎖和內存上提供的語義與內置鎖相同,此外它還提供了一些其他功能:定時的鎖等待、可中斷的鎖等待、公平性、非塊結構的加鎖。
與顯式鎖相比,內置鎖仍然具有很大的優勢。內置鎖更爲開發者熟悉,並且簡潔緊湊。ReentrantLock
的危險性比同步機制要高,如果忘記在finally
塊中調用unlock()
,實際上已經埋下了一顆定時炸彈。
在一些內置鎖無法滿足需求的情況下,ReentrantLock
可以作爲一種高級工具。當需要一些高級功能時才應該使用ReentrantLock
,這些功能包括:可定時的、可輪詢的與可中斷的所獲取操作,公平隊列,以及非塊結構的鎖。否則,還是優先使用synchronized
。
讀-寫鎖
ReentrantLock
實現了一種標準的互斥鎖:每次最多隻有一個線程持有ReentrantLock
。但對於維護數據的完整性來說,互斥通常是一種過於強硬的加鎖規則,因此也就限制了併發性。互斥是一種保守的加鎖策略,雖然可以避免寫/寫
和寫/讀
衝突,但是也避免了讀/讀
衝突。因此如果放寬了讀/讀
情況的加鎖需求,那麼將提升程序的性能。在這種情況下就有了讀-寫鎖
:一個資源可以被多個讀操作訪問,或者被一個寫操作訪問,但兩者不能同時進行。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
在讀-寫鎖
的加鎖策略中,允許多個讀操作同時進行,但每次只允許一個寫操作。
ReentrantReadWriteLock
爲這兩種鎖都提供了可重人的加鎖語義。與ReentrantLock
類似,
ReentrantReadWriteLock
在構造時也可以選擇是一一個非公平的鎖(默認)還是一個公平的鎖。
- 在公平的鎖中,等待時間最長的線程將優先獲得鎖。如果這個鎖由讀線程持有,而另一個線程請求寫入鎖,那麼其他讀線程都不能獲得讀取鎖,直到寫線程使用完並且釋放了寫人鎖。
- 在非公平的鎖中,線程獲得訪問許可的順序是不確定的。寫線程降級爲讀線程是可以的,但從讀線程升級爲寫線程則是不可以的(這樣做會導致死鎖)。
讀寫鎖代碼示例:
public class ReadWriteMap<K,V>{
private final Map<K,V> map;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock read = lock.readLock();
private final Lock write = lock.writeLock();
public ReadWriteMap(Map<K,V> map){
this.map = map;
}
public V put(K key,V value){
write.lock();
try {
return map.put(key, value);
}finally{
write.unlock();
}
}
public V get(Object key){
read.lock();
try {
return map.get(key);
}finally{
read.unlock();
}
}
}
與內置鎖相比,顯式的Lock提供了一些擴展功能,有着更高的靈活性。靈活性,並且對隊列行有着更好的控制。但ReentrantLock
不能完全替代synchronized
,只有在synchronized
無法滿足需求時,才應該使用它。
讀-寫鎖
允許多個讀線程併發地訪問被保護的對象,當訪問以讀取操作爲主的數據結構時,它能提高程序的可伸縮性。