Lock顯式鎖
在Java 5.0之前报腔,在協調對共享對象的訪問時可以使用的機制只有synchronized
和volatile
癞己。Java 5.0增加了一種新的機制:Lock
台囱。與之前提到過的機制相反菠隆,Lock
并不是一種替代內置加鎖的方法氏淑,而是當內置加鎖機制不適用時勃蜘,作為一種可選擇的高級功能。
synchronized的缺陷
synchronized是java中的一個關鍵字假残,也就是Java語言內置的特性缭贡。那么為什么會出現Lock呢?
在上面一篇文章中辉懒,我們了解到:如果一個代碼塊被synchronized修飾了阳惹,當一個線程獲取了對應的鎖,并執(zhí)行該代碼塊時眶俩,其他線程便只能一直等待莹汤,等待獲取鎖的線程釋放鎖,而這里獲取鎖的線程釋放鎖只會有兩種情況:
1)獲取鎖的線程執(zhí)行完了該代碼塊颠印,然后線程釋放對鎖的占有纲岭;
2)線程執(zhí)行發(fā)生異常,此時JVM會讓線程自動釋放鎖线罕。
那么如果這個獲取鎖的線程由于要等待I/O或者其他原因(比如調用sleep方法)被阻塞了止潮,但是又沒有釋放鎖,其他線程便只能干巴巴地等待钞楼,試想一下喇闸,這多么影響程序執(zhí)行效率。
因此就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間或者能夠響應中斷),通過Lock就可以辦到仅偎。
再舉個例子:當有多個線程讀寫文件時跨蟹,讀操作和寫操作會發(fā)生沖突現象,寫操作和寫操作會發(fā)生沖突現象橘沥,但是讀操作和讀操作不會發(fā)生沖突現象窗轩。
但是采用synchronized關鍵字來實現同步的話,就會導致一個問題:
如果多個線程都只是進行讀操作座咆,所以當一個線程在進行讀操作時痢艺,其他線程便只能等待無法進行讀操作。
因此就需要一種機制來使得多個線程都只是進行讀操作時介陶,線程之間不會發(fā)生沖突堤舒,通過Lock就可以辦到。
另外哺呜,通過Lock可以知道線程有沒有成功獲取到鎖舌缤。這個是synchronized無法辦到的。
總結一下某残,也就是說Lock提供了比synchronized更多的功能国撵。但是要注意以下幾點:
1)Lock不是Java語言內置的,synchronized是Java語言的關鍵字玻墅,因此是內置特性介牙。Lock是一個類,通過這個類可以實現同步訪問澳厢;
2)Lock和synchronized有一點非常大的不同环础,采用synchronized不需要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執(zhí)行完之后剩拢,系統會自動讓線程釋放對鎖的占用线得;而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖裸扶,就有可能導致出現死鎖現象框都。
java.util.concurrent.locks包下常用的類
下面我們就來探討一下java.util.concurrent.locks包中常用的類和接口。
Lock
首先要說明的就是Lock呵晨,通過查看Lock的源碼可知魏保,Lock是一個接口:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
lock()、tryLock()摸屠、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的谓罗。unLock()方法是用來釋放鎖的。newCondition()這個方法暫且不在此講述季二,會在后面的線程協作一文中講述檩咱。
在Lock中聲明了四個方法來獲取鎖揭措,那么這四個方法有何區(qū)別呢?
- lock()
lock()方法是平常使用得最多的一個方法刻蚯,就是用來獲取鎖绊含。如果鎖已被其他線程獲取,則進行等待炊汹。
在前面講到如果采用Lock躬充,必須主動去釋放鎖,并且在發(fā)生異常時讨便,不會自動釋放鎖充甚。因此一般來說,使用Lock必須在try{}catch{}塊中進行霸褒,并且將釋放鎖的操作放在finally塊中進行伴找,以保證鎖一定被釋放,防止死鎖的發(fā)生废菱。通常使用Lock來進行同步的話技矮,是以下面這種形式去使用的:
Lock lock = ...;
lock.lock();
try {
//處理任務
} catch (Exception ex) {
} finally {
lock.unlock(); //釋放鎖
}
- tryLock()
tryLock()方法是有返回值的,它表示用來嘗試獲取鎖昙啄,如果獲取成功穆役,則返回true,如果獲取失斒崃荨(即鎖已被其他線程獲取)梳杏,則返回false韧拒,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待十性。
- tryLock(long time, TimeUnit unit)
tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的叛溢,只不過區(qū)別在于這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖劲适,就返回false楷掉。如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true霞势。
所以烹植,一般情況下通過tryLock來獲取鎖時是這樣使用的:
Lock lock = ...;
if (lock.tryLock()) {
try {
//處理任務
} catch (Exception ex) {
} finally {
lock.unlock(); //釋放鎖
}
} else {
//如果不能獲取鎖,則直接做其他事情
}
- lockInterruptibly()
lockInterruptibly()方法比較特殊愕贡,當通過這個方法去獲取鎖時草雕,如果線程正在等待獲取鎖,則這個線程能夠響應中斷固以,即中斷線程的等待狀態(tài)墩虹。也就使說嘱巾,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖诫钓,而線程B只有在等待旬昭,那么對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。
由于lockInterruptibly()的聲明中拋出了異常菌湃,所以lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly()的方法外聲明拋出InterruptedException稳懒。
因此lockInterruptibly()一般的使用形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
} finally {
lock.unlock();
}
}
注意:當一個線程獲取了鎖之后,是不會被interrupt()方法中斷的慢味。因為單獨調用interrupt()方法不能中斷正在運行過程中的線程场梆,只能中斷阻塞過程中的線程。
因此當通過lockInterruptibly()方法獲取某個鎖時纯路,如果不能獲取到鎖或油,線程進入阻塞的狀態(tài),是可以響應中斷的驰唬。
而用synchronized修飾的話顶岸,當一個線程處于等待某個鎖的狀態(tài),是無法被中斷的叫编,只有一直等待下去辖佣。
ReentrantLock
ReentrantLock,意思是“可重入鎖”搓逾,關于可重入鎖的概念在下一節(jié)講述卷谈。ReentrantLock是唯一實現了Lock接口的類,并且ReentrantLock提供了更多的方法霞篡。下面通過一些實例具體看一下如何使用ReentrantLock世蔗。
1)lock()的使用方法:
public class LockTest {
public static void main(String[] args) {
new Thread() {
public void run() {
test(Thread.currentThread());
}
}.start();
new Thread() {
public void run() {
test(Thread.currentThread());
}
}.start();
}
private static Lock lock = new ReentrantLock();
public static void test(Thread t) {
lock.lock();
try {
System.out.println(t.getName() + "獲得了鎖");
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < 5000);
} finally {
System.out.println(t.getName() + "釋放了鎖");
lock.unlock();
}
}
}
執(zhí)行結果:
Thread-0獲得了鎖
Thread-0釋放了鎖
Thread-1獲得了鎖
Thread-1釋放了鎖
2)tryLock()的使用方法:
public static void test(Thread t) {
if (lock.tryLock()) {
try {
System.out.println(t.getName() + "獲得了鎖");
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < 5000);
} finally {
System.out.println(t.getName() + "釋放了鎖");
lock.unlock();
}
} else {
System.out.println(t.getName() + "獲取鎖失敗...");
}
}
執(zhí)行結果:
Thread-0獲得了鎖
Thread-1獲取鎖失敗...
Thread-0釋放了鎖
3)lockInterruptibly()響應中斷的使用方法:
public class LockTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
public void run() {
try {
test(Thread.currentThread());
} catch (InterruptedException ex) {
System.out.println(Thread.currentThread().getName() + "被中斷....");
}
}
};
Thread t2 = new Thread() {
public void run() {
try {
test(Thread.currentThread());
} catch (InterruptedException ex) {
System.out.println(Thread.currentThread().getName() + "被中斷....");
}
}
};
t1.start();
t2.start();
Thread.sleep(2000);
t2.interrupt();
}
private static Lock lock = new ReentrantLock();
public static void test(Thread t) throws InterruptedException {
lock.lockInterruptibly();
try {
System.out.println(t.getName() + "獲得了鎖");
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < 5000);
} finally {
System.out.println(t.getName() + "釋放了鎖");
lock.unlock();
}
}
}
執(zhí)行結果:
Thread-0獲得了鎖
Thread-1被中斷....
Thread-0釋放了鎖
ReadWriteLock
ReadWriteLock也是一個接口,在它里面只定義了兩個方法:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
一個用來獲取讀鎖朗兵,一個用來獲取寫鎖污淋。也就是說將文件的讀寫操作分開,分成2個鎖來分配給線程余掖,從而使得多個線程可以同時進行讀操作寸爆。下面的ReentrantReadWriteLock實現了ReadWriteLock接口。
ReentrantReadWriteLock
ReentrantReadWriteLock里面提供了很多豐富的方法盐欺,不過最主要的有兩個方法:readLock()和writeLock()用來獲取讀鎖和寫鎖赁豆。
下面通過一個例子來看一下ReentrantReadWriteLock的具體用法:
public class ReadWriteLockTest {
public static void main(String[] args) {
new Thread() {
public void run() {
test(Thread.currentThread());
}
}.start();
new Thread() {
public void run() {
test(Thread.currentThread());
}
}.start();
}
private static ReadWriteLock lock = new ReentrantReadWriteLock();
public static void test(Thread t) {
lock.readLock().lock();
try {
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < 1000) {
System.out.println(t.getName() + "正在進行讀操作");
}
} finally {
System.out.println(t.getName() + "執(zhí)行完讀操作");
lock.readLock().unlock();
}
}
}
執(zhí)行結果:
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1執(zhí)行完讀操作
Thread-0執(zhí)行完讀操作
說明thread1和thread2在同時進行讀操作。這樣就大大提升了讀操作的效率找田。不過要注意的是:如果有一個線程已經占用了讀鎖歌憨,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待讀鎖的釋放墩衙。如果有一個線程已經占用了寫鎖务嫡,則此時其他線程如果申請寫鎖或者讀鎖甲抖,則申請的線程會一直等待寫鎖的釋放。
公平鎖
公平鎖即盡量以請求鎖的順序來獲取鎖心铃。比如同時有多個線程在等待一個鎖准谚,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該鎖去扣,這種就是公平鎖柱衔。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的。這樣就可能導致某個或者一些線程永遠獲取不到鎖愉棱。
在Java中唆铐,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序奔滑。
而對于ReentrantLock和ReentrantReadWriteLock艾岂,它在默認情況下是非公平鎖,但是可以設置為公平鎖朋其。
我們可以在創(chuàng)建ReentrantLock對象時王浴,通過以下方式來設置鎖的公平性:
ReentrantLock lock = new ReentrantLock(true);
如果參數為true表示為公平鎖,fasle為非公平鎖梅猿。默認情況下氓辣,如果使用無參構造器,則是非公平鎖袱蚓。
非公平鎖的性能通常高于公平鎖的性能钞啸。在實際情況中,統計上的公平性保證——確保被阻塞的線程能最終獲得鎖癞松,通常已經夠用了爽撒,并且實際開銷也小得多。
ReetrankLock與synchronized比較
性能比較
在JDK1.5中响蓉,synchronized是性能低效的。因為這是一個重量級操作哨毁,它對性能最大的影響是阻塞的實現枫甲,掛起線程和恢復線程的操作都需要轉入內核態(tài)中完成,這些操作給系統的并發(fā)性帶來了很大的壓力扼褪。相比之下使用Java提供的Lock對象想幻,性能更高一些。Brian Goetz對這兩種鎖在JDK1.5话浇、單核處理器及雙Xeon處理器環(huán)境下做了一組吞吐量對比的實驗脏毯,發(fā)現多線程環(huán)境下,synchronized的吞吐量下降的非常嚴重幔崖,而ReentrankLock則能基本保持在同一個比較穩(wěn)定的水平上食店。但與其說ReetrantLock性能好渣淤,倒不如說synchronized還有非常大的優(yōu)化余地,于是到了JDK1.6吉嫩,發(fā)生了變化价认,對synchronized加入了很多優(yōu)化措施,有自適應自旋自娩,鎖消除用踩,鎖粗化,輕量級鎖忙迁,偏向鎖等等脐彩。導致在JDK1.6上synchronized的性能并不比Lock差。官方也表示姊扔,他們也更支持synchronized惠奸,在未來的版本中還有優(yōu)化余地,所以還是提倡在synchronized能實現需求的情況下旱眯,優(yōu)先考慮使用synchronized來進行同步晨川。
下面淺析以下兩種鎖機制的底層的實現策略。
互斥同步最主要的問題就是進行線程阻塞和喚醒所帶來的性能問題删豺,因而這種同步又稱為阻塞同步共虑,它屬于一種悲觀的并發(fā)策略,即線程獲得的是獨占鎖呀页。獨占鎖意味著其他線程只能依靠阻塞來等待線程釋放鎖妈拌。當掛起被阻塞的線程時,會引起CPU的上下文切換蓬蝶,當有很多線程競爭鎖的時候尘分,會引起CPU頻繁的上下文切換導致效率很低。synchronized采用的便是這種并發(fā)策略丸氛。
隨著指令集的發(fā)展培愁,我們有了另一種選擇:基于沖突檢測的樂觀并發(fā)策略,通俗地講就是先進行操作缓窜,如果沒有其他線程爭用共享數據定续,那操作就成功了,如果共享數據被爭用禾锤,產生了沖突私股,那就再進行其他的補償措施(最常見的補償措施就是不斷地重試,直到重試成功為止)恩掷,這種樂觀的并發(fā)策略的許多實現都不需要把線程掛起倡鲸,因此這種同步被稱為非阻塞同步。ReetrantLock采用的便是這種并發(fā)策略黄娘。
在樂觀的并發(fā)策略中峭状,需要操作和沖突檢測這兩個步驟具備原子性克滴,它靠硬件指令來保證,這里用的是CAS操作(Compare and Swap)宁炫。JDK1.5之后偿曙,Java程序才可以使用CAS操作。我們可以進一步研究ReentrantLock的源代碼羔巢,會發(fā)現其中比較重要的獲得鎖的一個方法是compareAndSetState望忆,這里其實就是調用的CPU提供的特殊指令。現代的CPU提供了指令竿秆,可以自動更新共享數據启摄,而且能夠檢測到其他線程的干擾,而compareAndSet() 就用這些代替了鎖定幽钢。這個算法稱作非阻塞算法歉备,意思是一個線程的失敗或者掛起不應該影響其他線程的失敗或掛起。
Java 5中引入了注入AutomicInteger匪燕、AutomicLong蕾羊、AutomicReference等特殊的原子性變量類,它們提供的如:compareAndSet()帽驯、incrementAndSet()和getAndIncrement()等方法都使用了CAS操作龟再。因此,它們都是由硬件指令來保證的原子方法尼变。
用途比較
1)Lock是一個接口利凑,而synchronized是Java中的關鍵字,synchronized是內置的語言實現嫌术;
2)synchronized在發(fā)生異常時哀澈,會自動釋放線程占有的鎖,因此不會導致死鎖現象發(fā)生度气;而Lock在發(fā)生異常時割按,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象磷籍,因此使用Lock時需要在finally塊中釋放鎖哲虾;
3)Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行择示,使用synchronized時,等待的線程會一直等待下去晒旅,不能夠響應中斷栅盲;
4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到废恋。
5)Lock可以提高多個線程進行讀操作的效率谈秫。
6)Lock可以實現公平鎖:多個線程在等待同一個鎖時扒寄,必須按照申請鎖的時間順序排隊等待,而非公平鎖則不保證這點拟烫,在鎖釋放時该编,任何一個等待鎖的線程都有機會獲得鎖。synchronized中的鎖是非公平鎖硕淑,ReentrantLock在默認情況下也是非公平鎖课竣,但可以通過構造方法ReentrantLock(ture)來要求使用公平鎖。
7)Lock可以綁定多個條件:ReentrantLock對象可以同時綁定多個Condition對象(名曰:條件變量或條件隊列)置媳,而在synchronized中于樟,鎖對象的wait()和notify()或notifyAll()方法可以實現一個隱含條件,但如果要和多于一個的條件關聯的時候拇囊,就不得不額外地添加一個鎖迂曲,而ReentrantLock則無需這么做,只需要多次調用newCondition()方法即可寥袭。而且我們還可以通過綁定Condition對象來判斷當前線程通知的是哪些線程(即與Condition對象綁定在一起的其他線程)路捧。