線程同步是什么兜叨?
多個(gè)線程訪問同一份資源(共享資源)的時(shí)候,線程之間相同協(xié)調(diào)的過程成為線程同步衩侥。
在一般情況下国旷,創(chuàng)建一個(gè)線程是不能提高程序的執(zhí)行效率的,所以要?jiǎng)?chuàng)建多個(gè)線程茫死。但是多個(gè)線程同時(shí)運(yùn)行的時(shí)候可能調(diào)用線程函數(shù)跪但,在多個(gè)線程同時(shí)對同一個(gè)內(nèi)存地址進(jìn)行寫入,由于CPU時(shí)間調(diào)度上的問題峦萎,寫入數(shù)據(jù)會(huì)被多次的覆蓋屡久,所以就要使線程同步。
synchronized關(guān)鍵字
synchronized可以保證方法或者代碼快在運(yùn)行時(shí)骨杂,同一時(shí)刻只有一個(gè)方法可以進(jìn)入到臨界區(qū)涂身,同時(shí)它還可以保證共享變量的內(nèi)存可見性。
每個(gè)Java對象都有用作一個(gè)實(shí)現(xiàn)同步的鎖搓蚪,這些鎖成為java內(nèi)置鎖蛤售,可以理解為,在object對象中有個(gè)lock對象妒潭。
當(dāng)你使用synchronized(this)相當(dāng)于synchronized(this.lock)
當(dāng)你使用synchronized(obj)相當(dāng)于synchronized(obj.lock)悴能,因此我們說是持有某個(gè)對象的鎖。
sleep()方法雳灾,睡眠過程中漠酿,并不釋放當(dāng)前持有的鎖。
三種應(yīng)用方式:
1谎亩、普通同步方法(實(shí)例方法)炒嘲,鎖是當(dāng)前實(shí)例對象宇姚,進(jìn)入同步方法前要獲得當(dāng)前實(shí)例的鎖
2、靜態(tài)同步方法夫凸,鎖是當(dāng)前類的class對象浑劳,進(jìn)入同步代碼前要獲得當(dāng)前類對象的鎖
3、同步方法塊夭拌,鎖是括號里面的對象魔熏,對給定對象加鎖,進(jìn)入同步代碼庫前要獲得給定對象的鎖
保護(hù)共享資源
要保護(hù)好需要同步的對象鸽扁,需要對訪問共享資源的所有方法或代碼塊都要考慮是否需要加入鎖蒜绽。因?yàn)閯e的線程可以自由訪問非同步(即:未加鎖)的方法,這樣可能會(huì)對同步的方法產(chǎn)生影響桶现。
生產(chǎn)者和消費(fèi)者
下面使用最經(jīng)典的生產(chǎn)者消費(fèi)者的例子躲雅,講解線程同步。
生產(chǎn)者->做饅頭
消費(fèi)者->吃饅頭
/**
* 饅頭封裝類
*/
class ManTou {
// 給饅頭一個(gè)id
int id;
public ManTou(int id) {
this.id = id;
}
@Override
public String toString() {
return "ManTou{" +
"id=" + id +
'}';
}
}
使用一個(gè)數(shù)組(籃子)來存放饅頭骡和,然后維護(hù)一個(gè)棧頂指針
/**
* 籃子
*/
class Basket {
// 棧頂指針, 該裝第幾個(gè)了
int index = 0;
// 容量
ManTou[] arrayManTou = new ManTou[6];
}
提供一個(gè)向籃子里扔饅頭(push)和從籃子里取饅頭(pop)的方法吏夯。
public void push(ManTou manTou) {
arrayManTou[index] = woTou;
index++;
}
public synchronized ManTou pop() {
index--;
ManTou mt = arrayManTou[index];
arrayManTou[index] = null;
return mt;
}
但是我們這樣會(huì)不會(huì)有問題呢?
對于每一個(gè)做饅頭的人和吃饅頭吃的人即横,都相當(dāng)于是一個(gè)線程噪生。
每個(gè)做饅頭的人都會(huì)調(diào)用push方法,向籃子里扔饅頭东囚。每個(gè)吃饅頭的人跺嗽,都會(huì)調(diào)用pop方法,從籃子里取出饅頭页藻。
當(dāng)做饅頭的人小A(A線程)調(diào)用push方法向筐里扔饅頭的過程中桨嫁,在 arrayManTou[index] = woTou;這一句之后,很不幸份帐,此時(shí)CPU時(shí)間片分給了做饅頭的小B(B線程執(zhí)行)璃吧。 那問題就來了:index 還沒來得及++。小B做好了饅頭也往籃子里面扔废境,就把剛才小A丟進(jìn)去的饅頭給覆蓋了畜挨。這個(gè)問題的關(guān)鍵就在于這兩條語句之間不能被打斷,因此要在push方法上加synchronized噩凹。
同樣的巴元,pop方法也有這樣的問題,因此也要在pop方法上添加synchronized關(guān)鍵字驮宴。
那接下來還有其他的問題:
對于做饅頭的人而言逮刨,籃子滿了怎么辦?因?yàn)槲覀兓@子里面數(shù)組的容量只有6堵泽。
既然籃子只有這么大修己,那就等會(huì)在做饅頭吧恢总,等籃子里的饅頭被吃掉了,再往籃子里面扔饅頭睬愤。
public synchronized void push(ManTou manTou) {
while (index == arrayManTou.length) { // 滿了
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
arrayManTou[index] = manTou;
index++;
notify();// 喚醒一個(gè)正在wait在當(dāng)前對象上的線程 notify無法喚醒自己
// notifyAll();
}
注意:這個(gè)wait是Object的的wait方法
this.wait()是啥意思? 是指當(dāng)前執(zhí)行這個(gè)代碼塊的線程wait离熏, 也就是已經(jīng)持有了synchronized(this)語句中的this對象的鎖的線程等待。等待在哪里戴涝?等待在this對象上。等待其他線程調(diào)用這個(gè)(this)對象的notify方法的時(shí)候喚醒自己钻蔑。
一個(gè)線程進(jìn)入push方法的時(shí)候啥刻, 已經(jīng)拿到了鎖了。在它執(zhí)行的過程中咪笑,遇到一個(gè)事件可帽,必須阻塞。也就是說做饅頭的人窗怒,在往籃子里扔的時(shí)候映跟,先檢查了一下籃子滿了,他就只能等著了扬虚,不能再往里扔了努隙,再扔就冒出來了。要等到有人吃了辜昵,才能再繼續(xù)往里扔荸镊。
調(diào)用wait()或者notify()之前,必須使用synchronized關(guān)鍵字持有被wait/notify的對象的鎖堪置。只有持有了鎖躬存,才有資格wait。如果你壓根拿不到鎖舀锨,就根本無法wait岭洲。
也就是你 synchronized(XX) 和 XX.wait 必須是同一個(gè)對象, 否則拋出 java.lang.IllegalMonitorStateException
那何時(shí)醒來坎匿?等籃子里的饅頭被別人吃了盾剩,讓吃饅頭的人把他叫醒就行啦, 即:等待別的線程調(diào)用同一個(gè)Basket對象的notify/nofityAll 方法的時(shí)候替蔬, 就會(huì)醒來啦彪腔。
對于吃饅頭的人而言,籃子空了咋整进栽?很簡單德挣,等著唄。等人家做好了咱再吃快毛。
public synchronized ManTou pop() {
while (index == 0) { // 空了
try {
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
index--;
ManTou mt = arrayManTou[index];
notify();
return mt;
}
細(xì)心的小伙伴可能已經(jīng)發(fā)現(xiàn):我們在push方法和pop方法的最后都調(diào)用了notify方法格嗅。
notify() 喚醒一個(gè)正在wait在當(dāng)前對象上的線程 notify無法喚醒自己
notifyAll() 喚醒所有正在wait在當(dāng)前對象上的線程
顯然番挺,剛才已經(jīng)有線程wait在了Basket對象上。
在push方法中:如果籃子里沒有滿的話屯掖,我們還是向往常一樣往籃子里扔饅頭玄柏,但是扔完了,記得叫醒等著吃饅頭的人贴铜。因?yàn)榭赡苡腥嗽诘戎浴?br>
在pop方法中:如果籃子不是空的粪摘,取出了一個(gè)就趕緊通知做饅頭的人, 說:“現(xiàn)在籃子已經(jīng)不是滿的了绍坝,有空間了徘意,你們可以做起來啦⌒郑”
這個(gè)籃子椎咧,我們終于是封裝好了,現(xiàn)在我們把做饅頭的人和吃饅頭的人也封裝起來把介。
/**
* 做饅頭的人
*/
class Producer implements Runnable {
Basket basket;
public Producer(Basket basket) {
this.basket = basket;
}
@Override
public void run() {
for (int i=0; i<20; i++) {
ManTou manTou = new ManTou(i);
basket.push(manTou);
System.out.println("生產(chǎn)了:" + manTou);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
生產(chǎn)者(做饅頭的人)需要知道往哪個(gè)籃子里扔饅頭勤讽,所以要持有籃子的引用。
當(dāng)創(chuàng)建生產(chǎn)者的時(shí)候拗踢,就告訴他要往哪個(gè)籃子里扔脚牍。因此我們提供一個(gè)構(gòu)造方法,為Basket賦值
生產(chǎn)的過程巢墅,也就是我們的 run()方法啦莫矗。在run()方法中,不斷做饅頭砂缩,不斷往籃子里扔作谚。
/**
* 消費(fèi)者(吃饅頭的人)
*/
class Consumer implements Runnable {
Basket basket;
public Consumer(Basket basket) {
this.basket = basket;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
ManTou manTou = basket.pop();
System.out.println("消費(fèi)了:" + manTou);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
消費(fèi)者( 吃饅頭的人),需要知道從哪個(gè)籃子里拿饅頭吃庵芭。所以也要持有筐的引用妹懒。我們在構(gòu)造方法中,為Basket賦值双吆。
消費(fèi)的過程眨唬, 也就是run()方法。不斷從籃子里取出饅頭好乐。
有個(gè)細(xì)節(jié)匾竿,我們要注意。在push方法判斷籃子滿的時(shí)候蔚万, 以及在pop方法判斷籃子空的時(shí)候岭妖,我們都用了while,為什么用while 而不用if呢?
考慮下面這樣一種情況:在push方法中昵慌,如果在wait的時(shí)候被打斷假夺,將進(jìn)入catch 代碼塊去處理異常,異常處理之后斋攀,就跳出了if已卷, 繼續(xù)下面的執(zhí)行。如果此時(shí)籃子還是滿的呢淳蔼? 就有問題了侧蘸。
所以要用while。即便是發(fā)生了Exception鹉梨, 仍要要回頭先檢查是否已經(jīng)滿了讳癌, 如果滿了, 還要繼續(xù)wait俯画。如果不滿了,才能繼續(xù)向下執(zhí)行司草。
總結(jié):
存放饅頭的籃子艰垂,就是所謂的共享資源,對于共享資源的保護(hù)埋虹,就是需要對訪問共享資源的所有方法和代碼塊都要考慮加入鎖猜憎,也就是Baseket類中的push和pop方法。
wait()和sleep()的區(qū)別
1搔课、wait是Object的方法胰柑,sleep是Thread的方法。
2爬泥、wait的時(shí)候不再持有那個(gè)鎖柬讨,等notify醒來才重新獲取鎖,sleep是睡著袍啡,還是擁有鎖踩官,并不釋放
3、調(diào)用wait的時(shí)候必須鎖定對象境输,如果沒有進(jìn)入synchronized代碼塊蔗牡,則沒有wait的資格