Java 多線程的同步代碼塊詳解
火車站搶票問(wèn)題
由于現(xiàn)實(shí)中買票也不會(huì)是零延遲的,為了真實(shí)性加入了延遲機(jī)制,也就是線程休眠語(yǔ)句
package test.MyThread.ticketDemo; public class RunnableThread implements Runnable{ private int ticket = 100; @Override public void run(){ while(true){ if(ticket>0){ try { Thread.sleep(100); //語(yǔ)句一 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"正在出售第 "+ticket+" 張票"); //語(yǔ)句二 ticket--; //語(yǔ)句三 } } } }
package test.MyThread.ticketDemo; public class ticketDemo1 { public static void main(String[] args) { RunnableThread r1 = new RunnableThread(); Thread t1 = new Thread(r1,"窗口一"); Thread t2 = new Thread(r1,"窗口二"); Thread t3 = new Thread(r1,"窗口三"); t1.start(); t2.start(); t3.start(); } }
但是結(jié)果和我們想象中的不一樣,三個(gè)窗口賣出了同樣的票
這是因?yàn)椋珻PU的操作具有原子性,單獨(dú)執(zhí)行一條指令或者說(shuō)語(yǔ)句,在執(zhí)行完畢前不會(huì)被中斷。
三個(gè)線程被啟動(dòng)后,都會(huì)處于就緒狀態(tài),然后開(kāi)始搶奪CPU執(zhí)行語(yǔ)句。
1.語(yǔ)句一:Thread.sleep(100);
2.語(yǔ)句二: System.out.println(Thread.currentThread().getName()+“正在出售第 “+ticket+” 張票”);
3.語(yǔ)句三: ticket–;
我將程序中需要執(zhí)行的三條主要語(yǔ)句列了出來(lái)
三條線程中,加入線程一先搶到了CPU,這時(shí)就會(huì)開(kāi)始執(zhí)行語(yǔ)句,也就是至少會(huì)完成一條語(yǔ)句一,然后進(jìn)入休眠。
注:如果語(yǔ)句一不是休眠語(yǔ)句,而是別的語(yǔ)句,那么線程一就可以繼續(xù)往下執(zhí)行,因?yàn)樵有?,正在?zhí)行的語(yǔ)句不會(huì)被打斷,所以只會(huì)在一條語(yǔ)句結(jié)束,下一條語(yǔ)句未開(kāi)始時(shí),被搶走CPU或者中斷,導(dǎo)致線程退出運(yùn)行狀態(tài),轉(zhuǎn)為就緒或者阻塞狀態(tài)。所以線程一可以一次性完成多條語(yǔ)句,也有可能剛完成一條語(yǔ)句就被搶走了CPU。
接著,線程二,線程三也搶到了CPU,也開(kāi)始執(zhí)行語(yǔ)句一,然后也進(jìn)入休眠狀態(tài)。之后線程一二三從休眠中醒來(lái),開(kāi)始爭(zhēng)搶CPU完成語(yǔ)句二,但是三者都在完成語(yǔ)句三之前被搶走了CPU,導(dǎo)致一直沒(méi)有執(zhí)行ticket–語(yǔ)句,ticket也就沒(méi)有減少,因此三條線程一共打印三條輸出語(yǔ)句,里面的ticket都是相同。
然后三條線程又開(kāi)始爭(zhēng)搶CPU來(lái)完成語(yǔ)句三,一個(gè)線程讓ticket減一,三個(gè)線程減少三張票。完成語(yǔ)句三后,又開(kāi)始新的循環(huán),三個(gè)線程開(kāi)始爭(zhēng)搶CPU完成語(yǔ)句一。
因此,看到的結(jié)果會(huì)是,三條語(yǔ)句的ticket都相同,然后ticket突然減三,接著又輸出三條ticket相同的輸出語(yǔ)句。
那么,該如何解決這種情況呢?
這種延遲賣票的問(wèn)題被稱為線程安全問(wèn)題,要發(fā)生線程安全問(wèn)題需要滿足三個(gè)條件(任何一共條件不滿足都不會(huì)造成線程安全問(wèn)題):
1.是否存在多線程環(huán)境
2.是否存在共享數(shù)據(jù)/共享變量
3.是否有多條語(yǔ)句操作著共享數(shù)據(jù)/共享變量
火車站延遲賣票問(wèn)題滿足這三個(gè)條件,因此造成了線程安全問(wèn)題,而前兩條都不可避免,那么就可以著手于破壞掉第三個(gè)條件,讓線程安全問(wèn)題不成立。
思路是將多條語(yǔ)句包裝成一個(gè)同步代碼塊,當(dāng)某個(gè)線程執(zhí)行這個(gè)同步代碼塊的時(shí)候,就跟原子性一樣,其他的線程不能搶占CPU,只能等這個(gè)同步代碼塊執(zhí)行完畢。
解決辦法:
1.synchronized —— 自動(dòng)鎖
2.lock —— 手動(dòng)鎖
synchronized
synchronized(對(duì)象){ //可能會(huì)發(fā)生線程安全問(wèn)題的代碼 } //這里的對(duì)象可以是任意對(duì)象,我們可以用 Object obj = new Object()里面的obj放入括號(hào)中
使用synchronized的條件:
1.必須有兩個(gè)或兩個(gè)以上的線程同一時(shí)間只有一個(gè)線程能夠執(zhí)行同步代碼塊多個(gè)線程想要同步時(shí),必須共用同一把鎖
synchronized(對(duì)象)括號(hào)里面的對(duì)象就是一把鎖
使用synchronized的過(guò)程:
1.只有搶到鎖的線程才可以執(zhí)行同步代碼塊,其余的線程即使搶到了CPU執(zhí)行權(quán),也只能等待,等待鎖的釋放。
2.代碼執(zhí)行完畢或者程序拋出異常都會(huì)釋放鎖,然后還未執(zhí)行同步代碼塊的線程爭(zhēng)搶鎖,誰(shuí)搶到誰(shuí)就能運(yùn)行同步代碼塊。
同步代碼塊
因此,修改后的代碼為:
package test.MyThread.ticketDemo; public class RunnableThread implements Runnable{ private int ticket = 100; Object obj = new Object(); @Override public void run(){ while(true){ synchronized (obj) { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第 " + ticket + " 張票"); ticket--; } } } } }
package test.MyThread.ticketDemo; public class ticketDemo1 { public static void main(String[] args) { //這里沒(méi)有改動(dòng),只是在上一個(gè)代碼中加了一把鎖 RunnableThread r1 = new RunnableThread(); Thread t1 = new Thread(r1,"窗口一"); Thread t2 = new Thread(r1,"窗口二"); Thread t3 = new Thread(r1,"窗口三"); t1.start(); t2.start(); t3.start(); } }
可以看出來(lái)結(jié)果符合我們的預(yù)期,是正確的
現(xiàn)在又有了新的問(wèn)題,那就是如果我在構(gòu)造線程的RunnableThread類里面加入方法呢?同步代碼塊里面出現(xiàn)方法時(shí),我們應(yīng)該怎么“上鎖”呢?
同步方法(this鎖)
同步方法,在public的后面加上synchronized關(guān)鍵字
package test.MyThread.ticketDemo; public class RunnableThread1 implements Runnable{ private int ticket = 100; Object obj = new Object(); public boolean flag = true; @Override public void run(){ if(flag==true){ while(ticket>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } SellTicket1(); } } } //同步方法,在public的后面加上synchronized關(guān)鍵字 public synchronized void SellTicket1(){ if(ticket>0){ System.out.println(Thread.currentThread().getName()+"正在出售第 "+ticket+" 張票"); ticket--; } } }
package test.MyThread.ticketDemo; public class ticketDemo2 { public static void main(String[] args) throws InterruptedException { RunnableThread1 r = new RunnableThread1(); Thread t1 = new Thread(r,"窗口一"); Thread t2 = new Thread(r,"窗口二"); t1.start(); t2.start(); } }
this鎖
先來(lái)看看,如果有兩條路徑,一條路徑是使用同步代碼塊,但是對(duì)象是obj,另一條路徑是使用同步方法
package test.MyThread.ticketDemo; public class TicketWindow2 implements Runnable{ //定義100張票 private static int tickets = 100; Object obj = new Object(); int i =0; @Override public void run() { while (true){ if(i%2==0){ synchronized (obj){ if(tickets>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 張票"); } } }else { sellTicket(); } i++; } } public synchronized void sellTicket(){ if(tickets>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 張票"); } } }
結(jié)果出錯(cuò),說(shuō)明同步方法的用的對(duì)象鎖不能是任意的對(duì)象,不同的線程應(yīng)該用相同的鎖。同步方法是屬于對(duì)象,而在這個(gè)類里面調(diào)用方法的是this對(duì)象,也就是this.sellTicket(),因此把this提取出來(lái)作為對(duì)象鎖中的對(duì)象。這樣多個(gè)線程都用的是this鎖
package test.MyThread.ticketDemo; public class TicketWindow2 implements Runnable{ //定義100張票 private static int tickets = 100; Object obj = new Object(); int i =0; @Override public void run() { while (true){ if(i%2==0){ synchronized (this){ if(tickets>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 張票"); } } }else { sellTicket(); } i++; } } public synchronized void sellTicket(){ if(tickets>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 張票"); } } }
修改完成后再運(yùn)行代碼,發(fā)現(xiàn)沒(méi)有錯(cuò)誤
注:
1.一個(gè)線程使用同步方法,另一個(gè)線程使用同步代碼塊this鎖,可以實(shí)現(xiàn)同步
2.一個(gè)線程使用同步方法,另一個(gè)線程使用同步代碼塊,但是不是this鎖。這種情況不能實(shí)現(xiàn)同步。
靜態(tài)同步方法
同步方法的鎖對(duì)象是this,
靜態(tài)同步方法的鎖對(duì)象是:這個(gè)靜態(tài)同步方法所屬的類的字節(jié)碼文件
下面代碼挺長(zhǎng)的,但其實(shí)就修改了上面同步方法的代碼的兩處地方
1.public synchronized void sellTicket(){}改為
public synchronized static void sellTicket(){}
2.synchronized (this){}改為synchronized (TicketWindow2.class){}
package test.MyThread.ticketDemo; public class TicketWindow2 implements Runnable{ //定義100張票 private static int tickets = 100; Object obj = new Object(); int i =0; @Override public void run() { while (true){ if(i%2==0){ synchronized (TicketWindow2.class){ if(tickets>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 張票"); } } }else { sellTicket(); } i++; } } public synchronized static void sellTicket(){ if(tickets>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 張票"); } } }
main()方法里面創(chuàng)建進(jìn)程和啟動(dòng)進(jìn)程的代碼,和上面同步方法里面的代碼相同
結(jié)果也和上面的一樣,都不再列出來(lái)了
死鎖問(wèn)題
package test.MyThread.ticketDemo; //兩個(gè)不同的鎖對(duì)象 public class LockObject { public static final Object lock1 = new Object(); public static final Object lock2 = new Object(); }
package test.MyThread.ticketDemo; public class DieLockThread extends Thread{ public boolean flag; public DieLockThread(boolean flag){ this.flag = flag; } @Override public void run() { if(flag){ synchronized(LockObject.lock1){ System.out.println("lock1"); synchronized(LockObject.lock2){ System.out.println("lock2"); } } }else{ synchronized(LockObject.lock2){ System.out.println("lock2"); synchronized(LockObject.lock1){ System.out.println("lock1"); } } } } }
package test.MyThread.ticketDemo; public class DieLockDemo { public static void main(String[] args) { DieLockThread d1 = new DieLockThread(true); DieLockThread d2 = new DieLockThread(false); d1.start(); d2.start(); } }
程序會(huì)卡在這一步,不能進(jìn)行下一步也不能停止
利用有參構(gòu)造,構(gòu)造出來(lái)的線程d1應(yīng)該是先獲得鎖對(duì)象LockObject.lock1然后執(zhí)行打印語(yǔ)句。接著獲取鎖對(duì)象LockObject.lock2,然后打印lock2。
但是這里因?yàn)榫€程d2是先獲取的鎖對(duì)象LockObject.lock2,并占據(jù)這個(gè)鎖對(duì)象,然后想獲得鎖對(duì)象LockObject.lock1,但LockObject.lock1此時(shí)被線程d1占據(jù)著
兩個(gè)線程都在等待對(duì)方釋放鎖對(duì)象,然后進(jìn)行下一步,但是兩者都不釋放,導(dǎo)致程序卡死在這里。這就造成了死鎖。
lock
package test.MyThread.ticketDemo; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockThread implements Runnable{ private int ticket = 100; Lock lock = new ReentrantLock(); @Override public void run(){ while(ticket>0){ try{ lock.lock(); if(ticket>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (ticket--) + " 張票"); } }finally { lock.unlock(); } } } }
package test.MyThread.ticketDemo; public class LockDemo { public static void main(String[] args) { LockThread lt = new LockThread(); Thread t1 = new Thread(lt,"窗口一"); Thread t2 = new Thread(lt,"窗口二"); Thread t3 = new Thread(lt,"窗口三"); t1.start(); t2.start(); t3.start(); } }
結(jié)果正確
總結(jié)
本篇文章就到這里了,希望能夠給你帶來(lái)幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
springboot多項(xiàng)目結(jié)構(gòu)實(shí)現(xiàn)
本文主要介紹了springboot多項(xiàng)目結(jié)構(gòu)實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01mybatis使用foreach查詢不出結(jié)果也不報(bào)錯(cuò)的問(wèn)題
這篇文章主要介紹了mybatis使用foreach查詢不出結(jié)果也不報(bào)錯(cuò)的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03SpringMVC使用hibernate-validator進(jìn)行參數(shù)校驗(yàn)最佳實(shí)踐記錄
這篇文章主要介紹了SpringMVC使用hibernate-validator進(jìn)行參數(shù)校驗(yàn)最佳實(shí)踐,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-05-05Spring學(xué)習(xí)筆記之bean的基礎(chǔ)知識(shí)
ean在Spring和SpringMVC中無(wú)所不在,將這個(gè)概念內(nèi)化很重要,所以下面這篇文章主要給大家介紹了關(guān)于Spring學(xué)習(xí)筆記之bean基礎(chǔ)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳解,需要的朋友可以參考下。2017-12-12Java之多個(gè)線程順序循環(huán)執(zhí)行的幾種實(shí)現(xiàn)
這篇文章主要介紹了Java之多個(gè)線程順序循環(huán)執(zhí)行的幾種實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09一文帶你了解Spring的Bean初始化過(guò)程和生命周期
Spring的核心功能有三點(diǎn)IOC、DI、AOP,IOC則是基礎(chǔ),也是Spring功能的最核心的點(diǎn)之一。今天一起來(lái)總結(jié)下Spring中Bean是怎么被創(chuàng)建出來(lái)的2023-03-03IDEA實(shí)現(xiàn)添加 前進(jìn)后退 到工具欄的操作
這篇文章主要介紹了IDEA 前進(jìn) 后退 添加到工具欄的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02Java Swing JToggleButton開(kāi)關(guān)按鈕的實(shí)現(xiàn)
這篇文章主要介紹了Java Swing JToggleButton開(kāi)關(guān)按鈕的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12