Java 5.0 加入了新的上鎖工作:ReentrantLock馁菜,它和同步(Synchronized)方法的內(nèi)置鎖不同,這是一種顯式鎖。顯式鎖作為一種高級的上鎖工作引润, 是同步方法的一種補充和擴展,用來實現(xiàn)同步代碼塊無法完成的功能宗弯。
1 Lock和ReentrantLock
Lock作為顯式鎖脯燃,其提供了一種無條件的、可輪詢和定時的蒙保、可中斷的鎖操作辕棚,其獲得鎖和釋放鎖的操作都是顯示。
Lock是Java 5.0 中加入的接口邓厕,表示顯式鎖的功能逝嚎,其接口定義如下:
public interface Lock {
void lock(); //獲取鎖
void lockInterruptibly() throws InterruptedException; //可中斷的獲取鎖操作
boolean tryLock(); //嘗試獲取鎖,不會被擁塞详恼,如果失敗立刻返回
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //在一定時間內(nèi)嘗試獲得鎖补君,如果超時則失敗
void unlock(); // 釋放鎖
Condition newCondition();
}
前文中,我們已經(jīng)討論過昧互,顯式鎖和同步代碼塊中的內(nèi)置鎖有著相同的互斥性和內(nèi)存可見性挽铁。ReentrantLock是Lock的一種實現(xiàn),提供對于線程的重入機制敞掘。和同步方法(Synchronized)相比叽掘,有著更強性能和靈活性。
雖然同步方法的內(nèi)置鎖已經(jīng)很強大和完備了玖雁,但是在功能上還有一定的局限性:不能實現(xiàn)非擁塞的鎖操作够掠。比如不能提供響應中斷的獲得鎖操作,不能提供支持超時的獲得鎖操作等等茄菊。因此疯潭,在某些情況下需要使用更為靈活的加鎖方式,也就是顯式鎖面殖。
在Java官方的注解中竖哩,給出了這樣的代碼示例:
Lock l = new ReentrantLock();
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
顯式鎖需要在手動調(diào)用lock方法來獲得鎖,并在使用后在finally代碼塊中調(diào)用unlock方法釋放鎖脊僚,以保證無論操作是否成功都能釋放掉鎖相叁。
顯式鎖支持非擁塞的鎖操作,具體的功能有:支持可輪詢和定時的辽幌、以及可中斷的鎖獲得操作增淹。
1.1 輪詢鎖和定時鎖
使用tryLock方法可以用于實現(xiàn)輪詢鎖和定時鎖。和無條件的獲得鎖操作相比乌企,tryLock方法具有更完善的錯誤恢復機制虑润,可以避免死鎖的放生。相比之下加酵,同步方法發(fā)生死鎖拳喻,其恢復方法就只能重新啟動程序哭当。
避免死鎖的方式之一為打破“請求與保持條件”(死鎖的四個條件),比如在要獲得多個鎖才能工作的情況下冗澈,如果不能獲得全部的鎖钦勘,就會釋放掉已經(jīng)持有的鎖,一段時間之后再去重新嘗試獲得所有的鎖亚亲。也就是說要么獲得所有鎖彻采,要么一個鎖都不占有。
下面的代碼中以轉賬為例捌归,演示了輪詢鎖的工作機制肛响。
public class DeadlockAvoidance {
private static Random rnd = new Random();
// 轉賬
public boolean transferMoney(Account fromAcct, //轉出賬戶
Account toAcct, //轉入賬戶
DollarAmount amount, //金額
long timeout, //超時時間
TimeUnit unit)
throws InsufficientFundsException, InterruptedException {
long fixedDelay = getFixedDelayComponentNanos(timeout, unit);
long randMod = getRandomDelayModulusNanos(timeout, unit);
long stopTime = System.nanoTime() + unit.toNanos(timeout);
while (true) {
// 嘗試獲得fromAcct的鎖
if (fromAcct.lock.tryLock()) {
try {
// 嘗試獲得toAcct的鎖
if (toAcct.lock.tryLock()) {
try {
if (fromAcct.getBalance().compareTo(amount) < 0) //余額不足
throw new InsufficientFundsException();
else { // 余額滿足,轉賬
fromAcct.debit(amount);
toAcct.credit(amount);
return true;
}
} finally { //釋放toAcct鎖
toAcct.lock.unlock();
}
}
} finally { //釋放fromAcct鎖
fromAcct.lock.unlock();
}
}
// 獲得鎖失敗
// 判斷是否超時 如果超時則立刻失敗
if (System.nanoTime() < stopTime)
return false;
// 如果沒有超時陨溅,隨機睡眠一段時間
NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);
}
}
class Account {
//顯示鎖
public Lock lock;
void debit(DollarAmount d) {
}
void credit(DollarAmount d) {
}
DollarAmount getBalance() {
return null;
}
}
class InsufficientFundsException extends Exception {
}
}
只有同時獲得轉出賬戶和轉入賬戶的鎖后终惑,才會進行轉賬绍在。如果不能同時獲得兩個鎖门扇,就釋放掉已經(jīng)獲得的鎖,并隨機隨眠一段時間偿渡,再去嘗試獲得全部的鎖臼寄,循環(huán)這個過程直到超時。
除了輪詢申請獲得鎖之外溜宽,也可以使用帶有時間限制的定時鎖操作吉拳,即獲得鎖的操作具有時間限制,超過一定時間后仍沒有獲得鎖就會返回失敗适揉。示例如下:
public class TimedLocking {
private Lock lock = new ReentrantLock();
public boolean trySendOnSharedLine(String message,
long timeout, TimeUnit unit)
throws InterruptedException {
// 設定超時時間
long nanosToLock = unit.toNanos(timeout)
- estimatedNanosToSend(message);
// 在規(guī)定時間內(nèi)等待鎖 否者就會返回false
if (!lock.tryLock(nanosToLock, NANOSECONDS))
return false;
try {
return sendOnSharedLine(message);
} finally {
lock.unlock();
}
}
private boolean sendOnSharedLine(String message) {
/* send something */
return true;
}
long estimatedNanosToSend(String message) {
return message.length();
}
}
1.2 中斷鎖
如果要將顯式鎖應用到可以取消的任務重留攒,就需要讓獲得鎖的操作是支持中斷。 lockInterruptibly方法可以應用到這樣情況中嫉嘀,其不僅能獲得鎖炼邀,還能保持對于中斷的響應。
public class InterruptibleLocking {
private Lock lock = new ReentrantLock();
public boolean sendOnSharedLine(String message)
throws InterruptedException {
// 可以響應中斷的鎖
lock.lockInterruptibly();
try {
return cancellableSendOnSharedLine(message);
} finally {
lock.unlock();
}
}
// 可能會拋出中斷異常
private boolean cancellableSendOnSharedLine(String message) throws InterruptedException {
/* send something */
return true;
}
}
1.3 非塊結構的加鎖
在內(nèi)置鎖中剪侮,鎖的獲得和鎖的釋放都是在同一塊代碼的拭宁,這樣簡潔清楚還便于使用,不用考慮如何退出代碼塊瓣俯。但是加鎖的位置不一定只有代碼塊杰标,比如之前談過的分段鎖。ConcurrentHashMap中利用了分段鎖對散列表中的元素分段上鎖彩匕,實現(xiàn)了并發(fā)訪問容器元素的功能腔剂。如果是這種非塊結構的加鎖,就不能應用內(nèi)置鎖驼仪,而是需要使用顯式鎖控制桶蝎。同樣驻仅,鏈表類的容器可以應用分段鎖,來支持并發(fā)訪問不同鏈表元素登渣。
2 性能因素考慮
前文中曾經(jīng)提過噪服,ConcurrentHashMap和同步的HashMap相比,其性能優(yōu)勢在于利用了分段鎖對散列表中的元素分段上鎖胜茧,故而支持并發(fā)訪問容器中不同的元素粘优。同理,和內(nèi)置鎖相比呻顽,顯式鎖都優(yōu)勢在于更好的性性雹顺。鎖的實現(xiàn)方式越好,就越可以避免不必要的系統(tǒng)調(diào)用和上下文切換廊遍,以提高效率嬉愧。
線程間的切換,涉及線程掛起和恢復等一系列操作喉前,這樣的線程上下文的切換很是消耗性能没酣,所以要避免不必要的線程切換。
Java 6中對內(nèi)置鎖的進行了優(yōu)化卵迂,現(xiàn)在內(nèi)置鎖和顯式鎖相比性能已經(jīng)很接近裕便,只略低一些。
3. 公平鎖
ReentrantLock的構造函數(shù)中提供兩種鎖的類型:
- 公平鎖:線程將按照它們請求鎖的順序來獲得鎖见咒;
- 非公平鎖:允許插隊偿衰,如果一個線程請求非公平鎖的那個時刻,鎖的狀態(tài)正好為可用改览,則該線程將跳過所有等待中的線程獲得該鎖下翎。
非公平鎖在線程間競爭鎖資源激烈的情況下,性能更高宝当,這是由于:在恢復一個被掛起線程與該線程真正開始運行之間视事,存在著一個很嚴重的延遲,這是由于線程間上下文切換帶來的今妄。正是這個延遲郑口,造成了公平鎖在使用中出現(xiàn)CPU空閑。非公平鎖正是將這個延遲帶來的時間差利用起來盾鳞,優(yōu)先讓正在運行的線程獲得鎖犬性,避免線程的上下文切換。
如果每個線程獲得鎖的時間都很長腾仅,或者請求鎖的競爭很稀疏或不頻繁乒裆,則公平鎖更為適合。
內(nèi)置鎖和顯式鎖都是默認使用非公平鎖推励,但是顯式鎖可以設置公平鎖鹤耍,內(nèi)置鎖無法做到肉迫。
4. 同步方法和顯式鎖的選擇
顯式鎖雖然更為靈活,提供更為豐富的功能稿黄,且性能更好喊衫,但是還是推薦先使用同步(Synchronized)方法,這是因為同步方法的內(nèi)置鎖杆怕,使用起來更為方便族购,簡潔緊湊 ,還便于理解陵珍,也更為開發(fā)人員所熟悉寝杖。
建議只有在一些內(nèi)置鎖無法滿足的情況下,再將顯式鎖ReentrantLock作為高級工具使用互纯,比如要使用輪詢鎖瑟幕、定時鎖、可中斷鎖或者是公平鎖留潦。除此之外只盹,還應該優(yōu)先使用synchronized方法。
5. 讀-寫鎖
無論是顯式鎖還是內(nèi)置鎖愤兵,都是互斥鎖鹿霸,也就是同一時刻只能有一個線程得到鎖排吴「讶椋互斥鎖是保守的加鎖策略,可以避免“寫-寫”沖突钻哩、“寫-讀”沖突”和"讀-讀"沖突屹堰。但是有時候不需要這么嚴格 ,同時多個任務讀取數(shù)據(jù)是被允許街氢,這有助于提升效率扯键,不需要避免“讀-讀”操作。為此珊肃,Java 5.0 中出現(xiàn)了讀-寫鎖ReadWriteLock荣刑。
ReadWriteLock可以提供兩種鎖:
- 讀鎖readLock:允許多個線程同時執(zhí)行讀操作,但是同時只能有一個線程執(zhí)行寫操作伦乔;
- 寫鎖writeLock:正常的互斥鎖厉亏,同一時刻只能有一個線程執(zhí)行讀寫操作。
ReentrantReadWriteLock是讀寫鎖支持重入的實現(xiàn)烈和,下面的例子中利用讀寫鎖實現(xiàn)了支持并發(fā)讀取元素的多線程安全Map:
public class ReadWriteMap <K,V> {
private final Map<K, V> map;
// 讀寫鎖
private final ReadWriteLock lock = new ReentrantReadWriteLock();
// 讀鎖
private final Lock r = lock.readLock();
// 寫鎖
private final Lock w = lock.writeLock();
public ReadWriteMap(Map<K, V> map) {
this.map = map;
}
public V put(K key, V value) {
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}
}
public V get(Object key) {
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
.....
}
不過需要注意的是爱只,雖然讀寫鎖的出現(xiàn)是為了提高效率,但只適用于對多線程頻繁并發(fā)執(zhí)行讀操作的情況招刹。如果是在正常的情況下使用讀寫鎖恬试,反而會降低效率窝趣,因為ReadWriteLock需要額外的開銷維護分別維護讀鎖和寫鎖,得不償失训柴。
擴展閱讀: