鎖的原理:任何時間都只能有一個線程持有鎖,只有持有鎖的線程才能訪問被鎖保護(hù)的資源。
我們接下來看一下在鎖的使用上有什么最佳實踐。
避免濫用鎖
如果能不用鎖,就不用鎖;如果你不確定是不是應(yīng)該用鎖,那也不要鎖。
使用鎖后帶來的代價:
- 加鎖和解鎖過程都需要CPU時間的,這是一個性能的損失。使用鎖還可能導(dǎo)致線程等待鎖,等待鎖過程中的線程是阻塞狀態(tài),過多的鎖等待會顯著降低程序的性能。
- 如果對鎖使用不當(dāng),很容易造成死鎖,導(dǎo)致整個程序“卡死”,這是非常嚴(yán)重的問題。
我們不可以看到一個共享數(shù)據(jù),在沒有搞清楚它在并發(fā)環(huán)境中是否會出現(xiàn)爭用問題,就“為了保險,給它加個鎖吧?!?,千萬不要有這種不負(fù)責(zé)任的想法,否則你將會付出慘痛的代價。
只有在并發(fā)環(huán)境中,共享資源不支持并發(fā)訪問,或者說并發(fā)訪問共享資源會導(dǎo)致系統(tǒng)錯誤的情況下,才需要使用鎖。
鎖的用法
使用鎖的過程可以分為三步:
- 在訪問共享資源之前,先獲取鎖。
- 如果獲取鎖成功,就可以訪問共享資源了。
- 使用完共享資源后釋放鎖,以便其他線程繼續(xù)訪問共享資源。
我們在使用鎖的過程中,需要注意使用完鎖,一定要釋放它。我們需要考慮到代碼可能走到的所有正常和異常的分支,確保所有情況下,鎖都能被釋放。
死鎖
死鎖是指由于某種原因,鎖一直沒有釋放,后續(xù)需要獲取鎖的線程都將處于等解鎖狀態(tài)。
大部分編程語言都提供了可重入鎖,如果沒有特別的需求,我們也要盡量使用可重入鎖。
下面是幾條如何避免死鎖的建議:
- 避免濫用鎖。
- 對于同一把鎖,加鎖和解鎖必須要放在同一個方法中,這樣一次加鎖對應(yīng)一次解鎖,代碼清晰簡單,便于分析問題。
- 盡量避免在持有一把鎖的情況下,去獲取另外一把鎖,就是要盡量避免同時持有多把鎖。
- 如果需要持有多把鎖,一定要注意加解鎖的順序,解鎖的順序要和加鎖的殊勛想法,比如,獲取三把鎖的順序是A、B、C,釋放鎖的順序必須是C、B、A。
使用讀寫鎖兼顧性能和安全
對于共享數(shù)據(jù),如果我們的方法只是去讀取它,而不會修改,也是需要加鎖的,因為有可能在讀取數(shù)據(jù)的過程中,有其他線程會更新數(shù)據(jù)。
但如果只是簡單地為數(shù)據(jù)加一個鎖,對于“讀多寫少”的場景,性能會受到影響。針對數(shù)據(jù)的讀寫操作,我們希望能夠做到:1)讀操作可以并發(fā)執(zhí)行,2)寫的同時不能并發(fā)讀,也不能并發(fā)寫。
Java中的ReadWriteLock可以用來解決這個問題,看下面的代碼框架:
ReadWriteLock rwlock = new ReentrantReadWriteLock();
public void read() {
rwlock.readLock().lock();
try {
// 在這兒讀取共享數(shù)據(jù)
} finally {
rwlock.readLock().unlock();
}
}
public void write() {
rwlock.writeLock().lock();
try {
// 在這兒更新共享數(shù)據(jù)
} finally {
rwlock.writeLock().unlock();
}
}
在這段代碼中,需要讀數(shù)據(jù)的時候,我們獲取鎖,這個鎖不是一個互斥鎖,即read()方法可以支持多個線程并行執(zhí)行,從而保證數(shù)據(jù)的讀性能。寫數(shù)據(jù)的時候,我們獲得寫鎖,這是一個互斥鎖,當(dāng)一個線程持有寫鎖的時候,其他線程既無法獲得讀鎖,也無法獲得寫鎖,從而達(dá)到了保護(hù)數(shù)據(jù)的目的。