火車站搶票問題
由于現實中買票也不會是零延遲的,為了真實性加入了延遲機制,也就是線程休眠語句
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); //語句一 } 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) { 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(); } }
但是結果和我們想象中的不一樣,三個窗口賣出了同樣的票
這是因為,CPU的操作具有原子性,單獨執行一條指令或者說語句,在執行完畢前不會被中斷。
三個線程被啟動后,都會處于就緒狀態,然后開始搶奪CPU執行語句。
1.語句一:Thread.sleep(100);
2.語句二: System.out.println(Thread.currentThread().getName()+“正在出售第 “+ticket+” 張票”);
3.語句三: ticketC;
我將程序中需要執行的三條主要語句列了出來
三條線程中,加入線程一先搶到了CPU,這時就會開始執行語句,也就是至少會完成一條語句一,然后進入休眠。
注:如果語句一不是休眠語句,而是別的語句,那么線程一就可以繼續往下執行,因為原子性,正在執行的語句不會被打斷,所以只會在一條語句結束,下一條語句未開始時,被搶走CPU或者中斷,導致線程退出運行狀態,轉為就緒或者阻塞狀態。所以線程一可以一次性完成多條語句,也有可能剛完成一條語句就被搶走了CPU。
接著,線程二,線程三也搶到了CPU,也開始執行語句一,然后也進入休眠狀態。之后線程一二三從休眠中醒來,開始爭搶CPU完成語句二,但是三者都在完成語句三之前被搶走了CPU,導致一直沒有執行ticketC語句,ticket也就沒有減少,因此三條線程一共打印三條輸出語句,里面的ticket都是相同。
然后三條線程又開始爭搶CPU來完成語句三,一個線程讓ticket減一,三個線程減少三張票。完成語句三后,又開始新的循環,三個線程開始爭搶CPU完成語句一。
因此,看到的結果會是,三條語句的ticket都相同,然后ticket突然減三,接著又輸出三條ticket相同的輸出語句。
那么,該如何解決這種情況呢?
這種延遲賣票的問題被稱為線程安全問題,要發生線程安全問題需要滿足三個條件(任何一共條件不滿足都不會造成線程安全問題):
1.是否存在多線程環境
2.是否存在共享數據/共享變量
3.是否有多條語句操作著共享數據/共享變量
火車站延遲賣票問題滿足這三個條件,因此造成了線程安全問題,而前兩條都不可避免,那么就可以著手于破壞掉第三個條件,讓線程安全問題不成立。
思路是將多條語句包裝成一個同步代碼塊,當某個線程執行這個同步代碼塊的時候,就跟原子性一樣,其他的線程不能搶占CPU,只能等這個同步代碼塊執行完畢。
解決辦法:
1.synchronized ―― 自動鎖
2.lock ―― 手動鎖
synchronized
synchronized(對象){ //可能會發生線程安全問題的代碼 } //這里的對象可以是任意對象,我們可以用 Object obj = new Object()里面的obj放入括號中
使用synchronized的條件:
1.必須有兩個或兩個以上的線程同一時間只有一個線程能夠執行同步代碼塊多個線程想要同步時,必須共用同一把鎖
synchronized(對象)括號里面的對象就是一把鎖
使用synchronized的過程:
1.只有搶到鎖的線程才可以執行同步代碼塊,其余的線程即使搶到了CPU執行權,也只能等待,等待鎖的釋放。
2.代碼執行完畢或者程序拋出異常都會釋放鎖,然后還未執行同步代碼塊的線程爭搶鎖,誰搶到誰就能運行同步代碼塊。
同步代碼塊
因此,修改后的代碼為:
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) { //這里沒有改動,只是在上一個代碼中加了一把鎖 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(); } }
可以看出來結果符合我們的預期,是正確的
現在又有了新的問題,那就是如果我在構造線程的RunnableThread類里面加入方法呢?同步代碼塊里面出現方法時,我們應該怎么“上鎖”呢?
同步方法(this鎖)
同步方法,在public的后面加上synchronized關鍵字
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關鍵字 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鎖
先來看看,如果有兩條路徑,一條路徑是使用同步代碼塊,但是對象是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--)+" 張票"); } } }
結果出錯,說明同步方法的用的對象鎖不能是任意的對象,不同的線程應該用相同的鎖。同步方法是屬于對象,而在這個類里面調用方法的是this對象,也就是this.sellTicket(),因此把this提取出來作為對象鎖中的對象。這樣多個線程都用的是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--)+" 張票"); } } }
修改完成后再運行代碼,發現沒有錯誤
注:
1.一個線程使用同步方法,另一個線程使用同步代碼塊this鎖,可以實現同步
2.一個線程使用同步方法,另一個線程使用同步代碼塊,但是不是this鎖。這種情況不能實現同步。
靜態同步方法
同步方法的鎖對象是this,
靜態同步方法的鎖對象是:這個靜態同步方法所屬的類的字節碼文件
下面代碼挺長的,但其實就修改了上面同步方法的代碼的兩處地方
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()方法里面創建進程和啟動進程的代碼,和上面同步方法里面的代碼相同
結果也和上面的一樣,都不再列出來了
死鎖問題
package test.MyThread.ticketDemo; //兩個不同的鎖對象 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(); } }
程序會卡在這一步,不能進行下一步也不能停止
利用有參構造,構造出來的線程d1應該是先獲得鎖對象LockObject.lock1然后執行打印語句。接著獲取鎖對象LockObject.lock2,然后打印lock2。
但是這里因為線程d2是先獲取的鎖對象LockObject.lock2,并占據這個鎖對象,然后想獲得鎖對象LockObject.lock1,但LockObject.lock1此時被線程d1占據著
兩個線程都在等待對方釋放鎖對象,然后進行下一步,但是兩者都不釋放,導致程序卡死在這里。這就造成了死鎖。
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(); } }
結果正確
總結
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關注服務器之家的更多內容!
原文鏈接:https://blog.csdn.net/qq_44823756/article/details/120925364