本篇文章總結(jié)關(guān)于多線程編程的一些知識點(diǎn),這其中兩個重要的部分就是對于synchronized和ReentrantLock的使用和介紹。
一、線程同步問題的產(chǎn)生及解決方案
問題的產(chǎn)生:
Java允許多線程并發(fā)控制,當(dāng)多個線程同時操作一個可共享的資源變量時(如數(shù)據(jù)的增刪改查),將會導(dǎo)致數(shù)據(jù)不準(zhǔn)確,相互之間產(chǎn)生沖突。
如下例:假設(shè)有一個賣票系統(tǒng),一共有100張票,有4個窗口同時賣。
public class Ticket implements Runnable {
// 當(dāng)前擁有的票數(shù)
private int num = 100;
public void run() {
while (true) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
// 輸出賣票信息
System.out.println(Thread.currentThread().getName() + ".....sale...." + num--);
}
}
}
}
public static void main(String[] args) {
Ticket t = new Ticket();//創(chuàng)建一個線程任務(wù)對象。
//創(chuàng)建4個線程同時賣票
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//啟動線程
t1.start();
t2.start();
t3.start();
t4.start();
}
輸出部分結(jié)果:
Thread-1.....sale....2
Thread-0.....sale....3
Thread-2.....sale....1
Thread-0.....sale....0
Thread-1.....sale....0
Thread-3.....sale....1
顯然上述結(jié)果是不合理的,對于同一張票進(jìn)行了多次售出。這就是多線程情況下,出現(xiàn)了數(shù)據(jù)“臟讀”情況。即多個線程訪問余票num時,當(dāng)一個線程獲得余票的數(shù)量,要在此基礎(chǔ)上進(jìn)行-1的操作之前,其他線程可能已經(jīng)賣出多張票,導(dǎo)致獲得的num不是最新的,然后-1后更新的數(shù)據(jù)就會有誤。這就需要線程同步的實(shí)現(xiàn)了。
問題的解決:
因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程的調(diào)用,從而保證了該變量的唯一性和準(zhǔn)確性。
一共有兩種鎖,來實(shí)現(xiàn)線程同步問題,分別是:synchronized和ReentrantLock。下面我們就帶著上述問題,看看這兩種鎖是如何解決的。
二、synchronized關(guān)鍵字
1.synchronized簡介
synchronized實(shí)現(xiàn)同步的基礎(chǔ):Java中每個對象都可以作為鎖。當(dāng)線程試圖訪問同步代碼時,必須先獲得對象鎖,退出或拋出異常時必須釋放鎖。Synchronzied實(shí)現(xiàn)同步的表現(xiàn)形式分為:代碼塊同步和方法同步。
2.synchronized原理
JVM基于進(jìn)入和退出Monitor對象來實(shí)現(xiàn)代碼塊同步和方法同步,兩者實(shí)現(xiàn)細(xì)節(jié)不同。
代碼塊同步:在編譯后通過將monitorenter指令插入到同步代碼塊的開始處,將monitorexit指令插入到方法結(jié)束處和異常處,通過反編譯字節(jié)碼可以觀察到。任何一個對象都有一個monitor與之關(guān)聯(lián),線程執(zhí)行monitorenter指令時,會嘗試獲取對象對應(yīng)的monitor的所有權(quán),即嘗試獲得對象的鎖。
方法同步:synchronized方法在method_info結(jié)構(gòu)有ACC_synchronized標(biāo)記,線程執(zhí)行時會識別該標(biāo)記,獲取對應(yīng)的鎖,實(shí)現(xiàn)方法同步。
兩者雖然實(shí)現(xiàn)細(xì)節(jié)不同,但本質(zhì)上都是對一個對象的監(jiān)視器(monitor)的獲取。任意一個對象都擁有自己的監(jiān)視器,當(dāng)同步代碼塊或同步方法時,執(zhí)行方法的線程必須先獲得該對象的監(jiān)視器才能進(jìn)入同步塊或同步方法,沒有獲取到監(jiān)視器的線程將會被阻塞,并進(jìn)入同步隊(duì)列,狀態(tài)變?yōu)锽LOCKED。當(dāng)成功獲取監(jiān)視器的線程釋放了鎖后,會喚醒阻塞在同步隊(duì)列的線程,使其重新嘗試對監(jiān)視器的獲取。
對象、監(jiān)視器、同步隊(duì)列和執(zhí)行線程間的關(guān)系如下圖:
3.synchronized的使用場景
①方法同步
public synchronized void method1
鎖住的是該對象,類的其中一個實(shí)例,當(dāng)該對象(僅僅是這一個對象)在不同線程中執(zhí)行這個同步方法時,線程之間會形成互斥。達(dá)到同步效果,但如果不同線程同時對該類的不同對象執(zhí)行這個同步方法時,則線程之間不會形成互斥,因?yàn)樗麄儞碛械氖遣煌逆i。
②代碼塊同步
synchronized(this){ //TODO }
描述同①
③方法同步
public synchronized static void method3
鎖住的是該類,當(dāng)所有該類的對象(多個對象)在不同線程中調(diào)用這個static同步方法時,線程之間會形成互斥,達(dá)到同步效果。
④代碼塊同步
synchronized(Test.class){ //TODO}
同③
⑤代碼塊同步
synchronized(o) {}
這里面的o可以是一個任何Object對象或數(shù)組,并不一定是它本身對象或者類,誰擁有o這個鎖,誰就能夠操作該塊程序代碼。
4.解決線程同步的實(shí)例
針對上述方法,具體的解決方式如下:
public class Ticket implements Runnable {
// 當(dāng)前擁有的票數(shù)
private int num = 100;
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
synchronized (this) {
// 輸出賣票信息
if(num>0){
System.out.println(Thread.currentThread().getName() + ".....sale...." + num--);
}
}
}
}
}
輸出部分結(jié)果:
Thread-2.....sale....10
Thread-1.....sale....9
Thread-3.....sale....8
Thread-0.....sale....7
Thread-2.....sale....6
Thread-1.....sale....5
Thread-2.....sale....4
Thread-1.....sale....3
Thread-3.....sale....2
Thread-0.....sale....1
可以看出實(shí)現(xiàn)了線程同步。同時改了一下邏輯,在進(jìn)入到同步代碼塊時,先判斷現(xiàn)在是否有沒有票,然后再買票,防止出現(xiàn)沒票還要售出的情況。通過同步代碼塊實(shí)現(xiàn)了線程同步,其他方法也一樣可以實(shí)現(xiàn)該效果。
三、ReentrantLock鎖
1.Lock接口
Lock,鎖對象。在Java中鎖是用來控制多個線程訪問共享資源的方式,一般來說,一個鎖能夠防止多個線程同時訪問共享資源(但有的鎖可以允許多個線程并發(fā)訪問共享資源,比如讀寫鎖,后面我們會分析)。在Lock接口出現(xiàn)之前,Java程序是靠synchronized關(guān)鍵字(后面分析)實(shí)現(xiàn)鎖功能的,而JAVA SE5.0之后并發(fā)包中新增了Lock接口用來實(shí)現(xiàn)鎖的功能,它提供了與synchronized關(guān)鍵字類似的同步功能,只是在使用時需要顯式地獲取和釋放鎖,缺點(diǎn)就是缺少像synchronized那樣隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性,可中斷的獲取鎖以及超時獲取鎖等多種synchronized關(guān)鍵字所不具備的同步特性。
Lock接口的主要方法(還有兩個方法比較復(fù)雜,暫不介紹):
void lock(): 執(zhí)行此方法時, 如果鎖處于空閑狀態(tài), 當(dāng)前線程將獲取到鎖. 相反, 如果鎖已經(jīng)被其他線程持有, 將禁用當(dāng)前線程, 直到當(dāng)前線程獲取到鎖.
boolean tryLock():如果鎖可用, 則獲取鎖, 并立即返回true, 否則返回false. 該方法和lock()的區(qū)別在于, tryLock()只是"試圖"獲取鎖, 如果鎖不可用, 不會導(dǎo)致當(dāng)前線程被禁用, 當(dāng)前線程仍然繼續(xù)往下執(zhí)行代碼. 而lock()方法則是一定要獲取到鎖, 如果鎖不可用, 就一直等待, 在未獲得鎖之前,當(dāng)前線程并不繼續(xù)向下執(zhí)行. 通常采用如下的代碼形式調(diào)用tryLock()方法:
void unlock():執(zhí)行此方法時, 當(dāng)前線程將釋放持有的鎖. 鎖只能由持有者釋放, 如果線程并不持有鎖, 卻執(zhí)行該方法, 可能導(dǎo)致異常的發(fā)生.
Condition newCondition():條件對象,獲取等待通知組件。該組件和當(dāng)前的鎖綁定,當(dāng)前線程只有獲取了鎖,才能調(diào)用該組件的await()方法,而調(diào)用后,當(dāng)前線程將縮放鎖。
ReentrantLock,一個可重入的互斥鎖,它具有與使用synchronized方法和語句所訪問的隱式監(jiān)視器鎖相同的一些基本行為和語義,但功能更強(qiáng)大。(重入鎖后面介紹)
2.ReentrantLock的使用
關(guān)于ReentrantLock的使用很簡單,只需要顯示調(diào)用,獲得同步鎖,釋放同步鎖即可。
ReentrantLock lock = new ReentrantLock(); //參數(shù)默認(rèn)false,不公平鎖
.....................
lock.lock(); //如果被其它資源鎖定,會在此等待鎖釋放,達(dá)到暫停的效果
try {
//操作
} finally {
lock.unlock(); //釋放鎖
}
3.解決線程同步的實(shí)例
針對上述方法,具體的解決方式如下:
public class Ticket implements Runnable {
// 當(dāng)前擁有的票數(shù)
private int num = 100;
ReentrantLock lock = new ReentrantLock();
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
lock.lock();
// 輸出賣票信息
if (num > 0) {
System.out.println(Thread.currentThread().getName() + ".....sale...." + num--);
}
lock.unlock();
}
}
}
四、重入鎖
當(dāng)一個線程得到一個對象后,再次請求該對象鎖時是可以再次得到該對象的鎖的。
具體概念就是:自己可以再次獲取自己的內(nèi)部鎖。
Java里面內(nèi)置鎖(synchronized)和Lock(ReentrantLock)都是可重入的。
public class SynchronizedTest {
public void method1() {
synchronized (SynchronizedTest.class) {
System.out.println("方法1獲得ReentrantTest的鎖運(yùn)行了");
method2();
}
}
public void method2() {
synchronized (SynchronizedTest.class) {
System.out.println("方法1里面調(diào)用的方法2重入鎖,也正常運(yùn)行了");
}
}
public static void main(String[] args) {
new SynchronizedTest().method1();
}
}
上面便是synchronized的重入鎖特性,即調(diào)用method1()方法時,已經(jīng)獲得了鎖,此時內(nèi)部調(diào)用method2()方法時,由于本身已經(jīng)具有該鎖,所以可以再次獲取。
public class ReentrantLockTest {
private Lock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
System.out.println("方法1獲得ReentrantLock鎖運(yùn)行了");
method2();
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock();
try {
System.out.println("方法1里面調(diào)用的方法2重入ReentrantLock鎖,也正常運(yùn)行了");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
new ReentrantLockTest().method1();
}
}
上面便是ReentrantLock的重入鎖特性,即調(diào)用method1()方法時,已經(jīng)獲得了鎖,此時內(nèi)部調(diào)用method2()方法時,由于本身已經(jīng)具有該鎖,所以可以再次獲取。
五、公平鎖
CPU在調(diào)度線程的時候是在等待隊(duì)列里隨機(jī)挑選一個線程,由于這種隨機(jī)性所以是無法保證線程先到先得的(synchronized控制的鎖就是這種非公平鎖)。但這樣就會產(chǎn)生饑餓現(xiàn)象,即有些線程(優(yōu)先級較低的線程)可能永遠(yuǎn)也無法獲取CPU的執(zhí)行權(quán),優(yōu)先級高的線程會不斷的強(qiáng)制它的資源。那么如何解決饑餓問題呢,這就需要公平鎖了。公平鎖可以保證線程按照時間的先后順序執(zhí)行,避免饑餓現(xiàn)象的產(chǎn)生。但公平鎖的效率比較低,因?yàn)橐獙?shí)現(xiàn)順序執(zhí)行,需要維護(hù)一個有序隊(duì)列。
ReentrantLock便是一種公平鎖,通過在構(gòu)造方法中傳入true就是公平鎖,傳入false,就是非公平鎖。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
以下是使用公平鎖實(shí)現(xiàn)的效果:
public class LockFairTest implements Runnable{
//創(chuàng)建公平鎖
private static ReentrantLock lock=new ReentrantLock(true);
public void run() {
while(true){
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"獲得鎖");
}finally{
lock.unlock();
}
}
}
public static void main(String[] args) {
LockFairTest lft=new LockFairTest();
Thread th1=new Thread(lft);
Thread th2=new Thread(lft);
th1.start();
th2.start();
}
}
輸出結(jié)果:
Thread-1獲得鎖
Thread-0獲得鎖
Thread-1獲得鎖
Thread-0獲得鎖
Thread-1獲得鎖
Thread-0獲得鎖
Thread-1獲得鎖
Thread-0獲得鎖
Thread-1獲得鎖
Thread-0獲得鎖
Thread-1獲得鎖
Thread-0獲得鎖
Thread-1獲得鎖
Thread-0獲得鎖
Thread-1獲得鎖
Thread-0獲得鎖
這是截取的部分執(zhí)行結(jié)果,分析結(jié)果可看出兩個線程是交替執(zhí)行的,幾乎不會出現(xiàn)同一個線程連續(xù)執(zhí)行多次。
六、synchronized和ReentrantLock的比較
1.區(qū)別:
1)Lock是一個接口,而synchronized是Java中的關(guān)鍵字,synchronized是內(nèi)置的語言實(shí)現(xiàn);
2)synchronized在發(fā)生異常時,會自動釋放線程占有的鎖,因此不會導(dǎo)致死鎖現(xiàn)象發(fā)生;而Lock在發(fā)生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現(xiàn)象,因此使用Lock時需要在finally塊中釋放鎖;
3)Lock可以讓等待鎖的線程響應(yīng)中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應(yīng)中斷;
4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
5)Lock可以提高多個線程進(jìn)行讀操作的效率。
總結(jié):ReentrantLock相比synchronized,增加了一些高級的功能。但也有一定缺陷。
在ReentrantLock類中定義了很多方法,比如:
isFair() //判斷鎖是否是公平鎖
isLocked() //判斷鎖是否被任何線程獲取了
isHeldByCurrentThread() //判斷鎖是否被當(dāng)前線程獲取了
hasQueuedThreads() //判斷是否有線程在等待該鎖
2.兩者在鎖的相關(guān)概念上區(qū)別:
1)可中斷鎖
顧名思義,就是可以相應(yīng)中斷的鎖。
在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。如果某一線程A正在執(zhí)行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由于等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。
lockInterruptibly()的用法體現(xiàn)了Lock的可中斷性。
2)公平鎖
公平鎖即盡量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖,當(dāng)這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該鎖(并不是絕對的,大體上是這種順序),這種就是公平鎖。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進(jìn)行的。這樣就可能導(dǎo)致某個或者一些線程永遠(yuǎn)獲取不到鎖。
在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。ReentrantLock可以設(shè)置成公平鎖。
3)讀寫鎖
讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。
正因?yàn)橛辛俗x寫鎖,才使得多個線程之間的讀操作可以并發(fā)進(jìn)行,不需要同步,而寫操作需要同步進(jìn)行,提高了效率。
ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實(shí)現(xiàn)了這個接口。
可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。
4)綁定多個條件
一個ReentrantLock對象可以同時綁定多個Condition對象,而在synchronized中,鎖對象的wait()和notify()或notifyAll()方法可以實(shí)現(xiàn)一個隱含的條件,如果要和多余一個條件關(guān)聯(lián)的時候,就不得不額外地添加一個鎖,而ReentrantLock則無須這么做,只需要多次調(diào)用new Condition()方法即可。
3.性能比較
在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當(dāng)競爭資源非常激烈時(即有大量線程同時競爭),此時ReentrantLock的性能要遠(yuǎn)遠(yuǎn)優(yōu)于synchronized。所以說,在具體使用時要根據(jù)適當(dāng)情況選擇。
在JDK1.5中,synchronized是性能低效的。因?yàn)檫@是一個重量級操作,它對性能最大的影響是阻塞的是實(shí)現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,這些操作給系統(tǒng)的并發(fā)性帶來了很大的壓力。相比之下使用Java提供的ReentrankLock對象,性能更高一些。到了JDK1.6,發(fā)生了變化,對synchronize加入了很多優(yōu)化措施,有自適應(yīng)自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導(dǎo)致在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他們也更支持synchronize,在未來的版本中還有優(yōu)化余地,所以還是提倡在synchronized能實(shí)現(xiàn)需求的情況下,優(yōu)先考慮使用synchronized來進(jìn)行同步。