本篇文章總結(jié)關(guān)于多線程編程的一些知識(shí)點(diǎn)骗污,這其中兩個(gè)重要的部分就是對于synchronized和ReentrantLock的使用和介紹滤灯。
一、線程同步問題的產(chǎn)生及解決方案
問題的產(chǎn)生:
Java允許多線程并發(fā)控制,當(dāng)多個(gè)線程同時(shí)操作一個(gè)可共享的資源變量時(shí)(如數(shù)據(jù)的增刪改查)焦履,將會(huì)導(dǎo)致數(shù)據(jù)不準(zhǔn)確贸典,相互之間產(chǎn)生沖突视卢。
如下例:假設(shè)有一個(gè)賣票系統(tǒng),一共有100張票廊驼,有4個(gè)窗口同時(shí)賣据过。
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)建一個(gè)線程任務(wù)對象。
//創(chuàng)建4個(gè)線程同時(shí)賣票
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//啟動(dòng)線程
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ù)“臟讀”情況酝掩。即多個(gè)線程訪問余票num時(shí)鳞芙,當(dāng)一個(gè)線程獲得余票的數(shù)量,要在此基礎(chǔ)上進(jìn)行-1的操作之前期虾,其他線程可能已經(jīng)賣出多張票原朝,導(dǎo)致獲得的num不是最新的,然后-1后更新的數(shù)據(jù)就會(huì)有誤镶苞。這就需要線程同步的實(shí)現(xiàn)了喳坠。
問題的解決:
因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程的調(diào)用宾尚,從而保證了該變量的唯一性和準(zhǔn)確性丙笋。
一共有兩種鎖,來實(shí)現(xiàn)線程同步問題煌贴,分別是:synchronized和ReentrantLock御板。下面我們就帶著上述問題,看看這兩種鎖是如何解決的牛郑。
二怠肋、synchronized關(guān)鍵字
1.synchronized簡介
synchronized實(shí)現(xiàn)同步的基礎(chǔ):Java中每個(gè)對象都可以作為鎖。當(dāng)線程試圖訪問同步代碼時(shí)淹朋,必須先獲得對象鎖笙各,退出或拋出異常時(shí)必須釋放鎖钉答。Synchronzied實(shí)現(xiàn)同步的表現(xiàn)形式分為:代碼塊同步和方法同步。
2.synchronized原理
JVM基于進(jìn)入和退出Monitor對象來實(shí)現(xiàn)代碼塊同步和方法同步杈抢,兩者實(shí)現(xiàn)細(xì)節(jié)不同数尿。
代碼塊同步:在編譯后通過將monitorenter指令插入到同步代碼塊的開始處,將monitorexit指令插入到方法結(jié)束處和異常處惶楼,通過反編譯字節(jié)碼可以觀察到右蹦。任何一個(gè)對象都有一個(gè)monitor與之關(guān)聯(lián),線程執(zhí)行monitorenter指令時(shí)歼捐,會(huì)嘗試獲取對象對應(yīng)的monitor的所有權(quán)何陆,即嘗試獲得對象的鎖。
方法同步:synchronized方法在method_info結(jié)構(gòu)有ACC_synchronized標(biāo)記豹储,線程執(zhí)行時(shí)會(huì)識(shí)別該標(biāo)記贷盲,獲取對應(yīng)的鎖,實(shí)現(xiàn)方法同步剥扣。
兩者雖然實(shí)現(xiàn)細(xì)節(jié)不同巩剖,但本質(zhì)上都是對一個(gè)對象的監(jiān)視器(monitor)的獲取。任意一個(gè)對象都擁有自己的監(jiān)視器朦乏,當(dāng)同步代碼塊或同步方法時(shí)球及,執(zhí)行方法的線程必須先獲得該對象的監(jiān)視器才能進(jìn)入同步塊或同步方法,沒有獲取到監(jiān)視器的線程將會(huì)被阻塞呻疹,并進(jìn)入同步隊(duì)列吃引,狀態(tài)變?yōu)锽LOCKED。當(dāng)成功獲取監(jiān)視器的線程釋放了鎖后刽锤,會(huì)喚醒阻塞在同步隊(duì)列的線程镊尺,使其重新嘗試對監(jiān)視器的獲取。
對象并思、監(jiān)視器庐氮、同步隊(duì)列和執(zhí)行線程間的關(guān)系如下圖:
3.synchronized的使用場景
①方法同步
public synchronized void method1
鎖住的是該對象,類的其中一個(gè)實(shí)例,當(dāng)該對象(僅僅是這一個(gè)對象)在不同線程中執(zhí)行這個(gè)同步方法時(shí)宋彼,線程之間會(huì)形成互斥弄砍。達(dá)到同步效果,但如果不同線程同時(shí)對該類的不同對象執(zhí)行這個(gè)同步方法時(shí)输涕,則線程之間不會(huì)形成互斥音婶,因?yàn)樗麄儞碛械氖遣煌逆i。
②代碼塊同步
synchronized(this){ //TODO }
描述同①
③方法同步
public synchronized static void method3
鎖住的是該類莱坎,當(dāng)所有該類的對象(多個(gè)對象)在不同線程中調(diào)用這個(gè)static同步方法時(shí)衣式,線程之間會(huì)形成互斥,達(dá)到同步效果。
④代碼塊同步
synchronized(Test.class){ //TODO}
同③
⑤代碼塊同步
synchronized(o) {}
這里面的o可以是一個(gè)任何Object對象或數(shù)組碴卧,并不一定是它本身對象或者類弱卡,誰擁有o這個(gè)鎖,誰就能夠操作該塊程序代碼住册。
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)了線程同步。同時(shí)改了一下邏輯界弧,在進(jìn)入到同步代碼塊時(shí)凡蜻,先判斷現(xiàn)在是否有沒有票搭综,然后再買票垢箕,防止出現(xiàn)沒票還要售出的情況。通過同步代碼塊實(shí)現(xiàn)了線程同步兑巾,其他方法也一樣可以實(shí)現(xiàn)該效果条获。
三、ReentrantLock鎖
1.Lock接口
Lock蒋歌,鎖對象帅掘。在Java中鎖是用來控制多個(gè)線程訪問共享資源的方式,一般來說堂油,一個(gè)鎖能夠防止多個(gè)線程同時(shí)訪問共享資源(但有的鎖可以允許多個(gè)線程并發(fā)訪問共享資源修档,比如讀寫鎖,后面我們會(huì)分析)府框。在Lock接口出現(xiàn)之前吱窝,Java程序是靠synchronized關(guān)鍵字(后面分析)實(shí)現(xiàn)鎖功能的,而JAVA SE5.0之后并發(fā)包中新增了Lock接口用來實(shí)現(xiàn)鎖的功能迫靖,它提供了與synchronized關(guān)鍵字類似的同步功能院峡,只是在使用時(shí)需要顯式地獲取和釋放鎖,缺點(diǎn)就是缺少像synchronized那樣隱式獲取釋放鎖的便捷性系宜,但是卻擁有了鎖獲取與釋放的可操作性照激,可中斷的獲取鎖以及超時(shí)獲取鎖等多種synchronized關(guān)鍵字所不具備的同步特性。
Lock接口的主要方法(還有兩個(gè)方法比較復(fù)雜盹牧,暫不介紹):
**void lock(): **執(zhí)行此方法時(shí), 如果鎖處于空閑狀態(tài), 當(dāng)前線程將獲取到鎖. 相反, 如果鎖已經(jīng)被其他線程持有, 將禁用當(dāng)前線程, 直到當(dāng)前線程獲取到鎖.
boolean tryLock():如果鎖可用, 則獲取鎖, 并立即返回true, 否則返回false. 該方法和lock()的區(qū)別在于, tryLock()只是"試圖"獲取鎖, 如果鎖不可用, 不會(huì)導(dǎo)致當(dāng)前線程被禁用, 當(dāng)前線程仍然繼續(xù)往下執(zhí)行代碼. 而lock()方法則是一定要獲取到鎖, 如果鎖不可用, 就一直等待, 在未獲得鎖之前,當(dāng)前線程并不繼續(xù)向下執(zhí)行. 通常采用如下的代碼形式調(diào)用tryLock()方法:
void unlock():執(zhí)行此方法時(shí), 當(dāng)前線程將釋放持有的鎖. 鎖只能由持有者釋放, 如果線程并不持有鎖, 卻執(zhí)行該方法, 可能導(dǎo)致異常的發(fā)生.
Condition newCondition():條件對象俩垃,獲取等待通知組件。該組件和當(dāng)前的鎖綁定汰寓,當(dāng)前線程只有獲取了鎖口柳,才能調(diào)用該組件的await()方法,而調(diào)用后踩寇,當(dāng)前線程將縮放鎖啄清。
ReentrantLock,一個(gè)可重入的互斥鎖,它具有與使用synchronized方法和語句所訪問的隱式監(jiān)視器鎖相同的一些基本行為和語義辣卒,但功能更強(qiáng)大掷贾。(重入鎖后面介紹)
2.ReentrantLock的使用
關(guān)于ReentrantLock的使用很簡單,只需要顯示調(diào)用荣茫,獲得同步鎖想帅,釋放同步鎖即可。
ReentrantLock lock = new ReentrantLock(); //參數(shù)默認(rèn)false啡莉,不公平鎖
.....................
lock.lock(); //如果被其它資源鎖定港准,會(huì)在此等待鎖釋放,達(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)一個(gè)線程得到一個(gè)對象后,再次請求該對象鎖時(shí)是可以再次得到該對象的鎖的魄咕。
具體概念就是:自己可以再次獲取自己的內(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()方法時(shí)毛萌,已經(jīng)獲得了鎖,此時(shí)內(nèi)部調(diào)用method2()方法時(shí)喝滞,由于本身已經(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()方法時(shí)做盅,已經(jīng)獲得了鎖,此時(shí)內(nèi)部調(diào)用method2()方法時(shí)狸演,由于本身已經(jīng)具有該鎖言蛇,所以可以再次獲取。
五宵距、公平鎖
CPU在調(diào)度線程的時(shí)候是在等待隊(duì)列里隨機(jī)挑選一個(gè)線程腊尚,由于這種隨機(jī)性所以是無法保證線程先到先得的(synchronized控制的鎖就是這種非公平鎖)。但這樣就會(huì)產(chǎn)生饑餓現(xiàn)象满哪,即有些線程(優(yōu)先級(jí)較低的線程)可能永遠(yuǎn)也無法獲取CPU的執(zhí)行權(quán)婿斥,優(yōu)先級(jí)高的線程會(huì)不斷的強(qiáng)制它的資源。那么如何解決饑餓問題呢哨鸭,這就需要公平鎖了民宿。公平鎖可以保證線程按照時(shí)間的先后順序執(zhí)行,避免饑餓現(xiàn)象的產(chǎn)生像鸡。但公平鎖的效率比較低活鹰,因?yàn)橐獙?shí)現(xiàn)順序執(zhí)行,需要維護(hù)一個(gè)有序隊(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é)果可看出兩個(gè)線程是交替執(zhí)行的,幾乎不會(huì)出現(xiàn)同一個(gè)線程連續(xù)執(zhí)行多次桑涎。
六彬向、synchronized和ReentrantLock的比較
1.區(qū)別:
1)Lock是一個(gè)接口,而synchronized是Java中的關(guān)鍵字攻冷,synchronized是內(nèi)置的語言實(shí)現(xiàn)娃胆;
2)synchronized在發(fā)生異常時(shí),會(huì)自動(dòng)釋放線程占有的鎖讲衫,因此不會(huì)導(dǎo)致死鎖現(xiàn)象發(fā)生缕棵;而Lock在發(fā)生異常時(shí),如果沒有主動(dòng)通過unLock()去釋放鎖涉兽,則很可能造成死鎖現(xiàn)象,因此使用Lock時(shí)需要在finally塊中釋放鎖篙程;
3)Lock可以讓等待鎖的線程響應(yīng)中斷枷畏,而synchronized卻不行,使用synchronized時(shí)虱饿,等待的線程會(huì)一直等待下去拥诡,不能夠響應(yīng)中斷;
4)通過Lock可以知道有沒有成功獲取鎖氮发,而synchronized卻無法辦到渴肉。
5)Lock可以提高多個(gè)線程進(jìn)行讀操作的效率。
總結(jié):ReentrantLock相比synchronized爽冕,增加了一些高級(jí)的功能仇祭。但也有一定缺陷。
在ReentrantLock類中定義了很多方法颈畸,比如:
isFair() //判斷鎖是否是公平鎖
isLocked() //判斷鎖是否被任何線程獲取了
isHeldByCurrentThread() //判斷鎖是否被當(dāng)前線程獲取了
hasQueuedThreads() //判斷是否有線程在等待該鎖
2.兩者在鎖的相關(guān)概念上區(qū)別:
1)可中斷鎖
顧名思義乌奇,就是可以相應(yīng)中斷的鎖。
在Java中眯娱,synchronized就不是可中斷鎖礁苗,而Lock是可中斷鎖。如果某一線程A正在執(zhí)行鎖中的代碼徙缴,另一線程B正在等待獲取該鎖试伙,可能由于等待時(shí)間過長,線程B不想等待了,想先處理其他事情疏叨,我們可以讓它中斷自己或者在別的線程中中斷它吱抚,這種就是可中斷鎖。
lockInterruptibly()的用法體現(xiàn)了Lock的可中斷性考廉。
2)公平鎖
公平鎖即盡量以請求鎖的順序來獲取鎖秘豹。比如同是有多個(gè)線程在等待一個(gè)鎖,當(dāng)這個(gè)鎖被釋放時(shí)昌粤,等待時(shí)間最久的線程(最先請求的線程)會(huì)獲得該鎖(并不是絕對的既绕,大體上是這種順序),這種就是公平鎖涮坐。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進(jìn)行的凄贩。這樣就可能導(dǎo)致某個(gè)或者一些線程永遠(yuǎn)獲取不到鎖。
在Java中袱讹,synchronized就是非公平鎖疲扎,它無法保證等待的線程獲取鎖的順序。ReentrantLock可以設(shè)置成公平鎖捷雕。
3)讀寫鎖
讀寫鎖將對一個(gè)資源(比如文件)的訪問分成了2個(gè)鎖椒丧,一個(gè)讀鎖和一個(gè)寫鎖。
正因?yàn)橛辛俗x寫鎖救巷,才使得多個(gè)線程之間的讀操作可以并發(fā)進(jìn)行壶熏,不需要同步,而寫操作需要同步進(jìn)行浦译,提高了效率棒假。
ReadWriteLock就是讀寫鎖,它是一個(gè)接口精盅,ReentrantReadWriteLock實(shí)現(xiàn)了這個(gè)接口帽哑。
可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖叹俏。
4)綁定多個(gè)條件
一個(gè)ReentrantLock對象可以同時(shí)綁定多個(gè)Condition對象妻枕,而在synchronized中,鎖對象的wait()和notify()或notifyAll()方法可以實(shí)現(xiàn)一個(gè)隱含的條件她肯,如果要和多余一個(gè)條件關(guān)聯(lián)的時(shí)候佳头,就不得不額外地添加一個(gè)鎖,而ReentrantLock則無須這么做晴氨,只需要多次調(diào)用new Condition()方法即可康嘉。
3.性能比較
在性能上來說,如果競爭資源不激烈籽前,兩者的性能是差不多的亭珍,而當(dāng)競爭資源非常激烈時(shí)(即有大量線程同時(shí)競爭)敷钾,此時(shí)ReentrantLock的性能要遠(yuǎn)遠(yuǎn)優(yōu)于synchronized。所以說肄梨,在具體使用時(shí)要根據(jù)適當(dāng)情況選擇阻荒。
在JDK1.5中,synchronized是性能低效的众羡。因?yàn)檫@是一個(gè)重量級(jí)操作侨赡,它對性能最大的影響是阻塞的是實(shí)現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成粱侣,這些操作給系統(tǒng)的并發(fā)性帶來了很大的壓力羊壹。相比之下使用Java提供的ReentrankLock對象,性能更高一些齐婴。到了JDK1.6油猫,發(fā)生了變化,對synchronize加入了很多優(yōu)化措施柠偶,有自適應(yīng)自旋情妖,鎖消除,鎖粗化诱担,輕量級(jí)鎖毡证,偏向鎖等等。導(dǎo)致在JDK1.6上synchronize的性能并不比Lock差该肴。官方也表示情竹,他們也更支持synchronize,在未來的版本中還有優(yōu)化余地匀哄,所以還是提倡在synchronized能實(shí)現(xiàn)需求的情況下,優(yōu)先考慮使用synchronized來進(jìn)行同步雏蛮。