Java學習多執行緒第二天
- 2019 年 10 月 3 日
- 筆記
內容介紹
- 執行緒安全
- 執行緒同步
- 死鎖
- Lock鎖
- 等待喚醒機制
1 多執行緒
1.1 執行緒安全
如果有多個執行緒在同時運行,而這些執行緒可能會同時運行這段程式碼。程式每次運行結果和單執行緒運行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。
我們通過一個案例,演示執行緒的安全問題:
電影院要賣票,我們模擬電影院的賣票過程。假設要播放的電影是 “功夫熊貓3”,本次電影的座位共100個(本場電影只能賣100張票)。
我們來模擬電影院的售票窗口,實現多個窗口同時賣 “魔童哪吒”這場電影票(多個窗口一起賣這100張票)
需要窗口,採用執行緒對象來模擬;需要票,Runnable介面子類來模擬
- 測試類
public class ThreadDemo { public static void main(String[] args) { //創建票對象 Ticket ticket = new Ticket(); //創建3個窗口 Thread t1 = new Thread(ticket, "窗口1"); Thread t2 = new Thread(ticket, "窗口2"); Thread t3 = new Thread(ticket, "窗口3"); t1.start(); t2.start(); t3.start(); } }
-
模擬票
public class Ticket implements Runnable { //共100票 int ticket = 100; @Override public void run() { //模擬賣票 while(true){ if (ticket > 0) { //模擬選坐的操作 try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--); } } } }
運行結果發現:上面程式出現了問題
- 票出現了重複的票
- 錯誤的票 0、-1
其實,執行緒安全問題都是由全局變數及靜態變數引起的。若每個執行緒中對全局變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全局變數是執行緒安全的;若有多個執行緒同時執行寫操作,一般都需要考慮執行緒同步,否則的話就可能影響執行緒安全。
1.2 執行緒同步(執行緒安全處理Synchronized)
java中提供了執行緒同步機制,它能夠解決上述的執行緒安全問題。
執行緒同步的方式有兩種:
- 方式1:同步程式碼塊
- 方式2:同步方法
1.2.1 同步程式碼塊
同步程式碼塊: 在程式碼塊聲明上 加上synchronized
synchronized (鎖對象) { 可能會產生執行緒安全問題的程式碼 }
同步程式碼塊中的鎖對象可以是任意的對象;但多個執行緒時,要使用同一個鎖對象才能夠保證執行緒安全。
使用同步程式碼塊,對電影院賣票案例中Ticket類進行如下程式碼修改:
public class Ticket implements Runnable { //共100票 int ticket = 100; //定義鎖對象 Object lock = new Object(); @Override public void run() { //模擬賣票 while(true){ //同步程式碼塊 synchronized (lock){ if (ticket > 0) { //模擬電影選坐的操作 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--); } } } } }
當使用了同步程式碼塊後,上述的執行緒的安全問題,解決了。
1.2.3 同步方法
- 同步方法:在方法聲明上加上synchronized
public synchronized void method(){ 可能會產生執行緒安全問題的程式碼 }
同步方法中的鎖對象是 this
使用同步方法,對電影院賣票案例中Ticket類進行如下程式碼修改:
public class Ticket implements Runnable { //共100票 int ticket = 100; //定義鎖對象 Object lock = new Object(); @Override public void run() { //模擬賣票 while(true){ //同步方法 method(); } } //同步方法,鎖對象this public synchronized void method(){ if (ticket > 0) { //模擬選坐的操作 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--); } } }
- 靜態同步方法: 在方法聲明上加上static synchronized
public static synchronized void method(){ 可能會產生執行緒安全問題的程式碼 }
靜態同步方法中的鎖對象是 類名.class
1.3 死鎖
同步鎖使用的弊端:當執行緒任務中出現了多個同步(多個鎖)時,如果同步中嵌套了其他的同步。這時容易引發一種現象:程式出現無限等待,這種現象我們稱為死鎖。這種情況能避免就避免掉。
synchronzied(A鎖){ synchronized(B鎖){ } }
我們進行下死鎖情況的程式碼演示:
- 定義鎖對象類
public class MyLock { public static final Object lockA = new Object(); public static final Object lockB = new Object(); }
- 執行緒任務類
public class ThreadTask implements Runnable { int x = new Random().nextInt(1);//0,1 //指定執行緒要執行的任務程式碼 @Override public void run() { while(true){ if (x%2 ==0) { //情況一 synchronized (MyLock.lockA) { System.out.println("if-LockA"); synchronized (MyLock.lockB) { System.out.println("if-LockB"); System.out.println("if大口吃肉"); } } } else { //情況二 synchronized (MyLock.lockB) { System.out.println("else-LockB"); synchronized (MyLock.lockA) { System.out.println("else-LockA"); System.out.println("else大口吃肉"); } } } x++; } } }
- 測試類
public class ThreadDemo { public static void main(String[] args) { //創建執行緒任務類對象 ThreadTask task = new ThreadTask(); //創建兩個執行緒 Thread t1 = new Thread(task); Thread t2 = new Thread(task); //啟動執行緒 t1.start(); t2.start(); } }
1.4 Lock介面
查閱API,查閱Lock介面描述,Lock
實現提供了比使用 synchronized
方法和語句可獲得的更廣泛的鎖定操作。
Lock提供了一個更加面對對象的鎖,在該鎖中提供了更多的操作鎖的功能。
我們使用Lock介面,以及其中的lock()方法和unlock()方法替代同步,對電影院賣票案例中Ticket類進行如下程式碼修改:
public class Ticket implements Runnable { //共100票 int ticket = 100; //創建Lock鎖對象 Lock ck = new ReentrantLock(); @Override public void run() { //模擬賣票 while(true){ //synchronized (lock){ ck.lock(); if (ticket > 0) { //模擬選坐的操作 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--); } ck.unlock(); //} } } }
1.5 等待喚醒機制
在開始講解等待喚醒機制之前,有必要搞清一個概念——執行緒之間的通訊:多個執行緒在處理同一個資源,但是處理的動作(執行緒的任務)卻不相同。通過一定的手段使各個執行緒能有效的利用資源。而這種手段即—— 等待喚醒機制。
等待喚醒機制所涉及到的方法:
- wait() :等待,將正在執行的執行緒釋放其執行資格 和 執行權,並存儲到執行緒池中。
- notify():喚醒,喚醒執行緒池中被wait()的執行緒,一次喚醒一個,而且是任意的。
- notifyAll(): 喚醒全部:可以將執行緒池中的所有wait() 執行緒都喚醒。
其實,所謂喚醒的意思就是讓 執行緒池中的執行緒具備執行資格。必須注意的是,這些方法都是在 同步中才有效。同時這些方法在使用時必須標明所屬鎖,這樣才可以明確出這些方法操作的到底是哪個鎖上的執行緒。
仔細查看JavaAPI之後,發現這些方法 並不定義在 Thread中,也沒定義在Runnable介面中,卻被定義在了Object類中,為什麼這些操作執行緒的方法定義在Object類中?
因為這些方法在使用時,必須要標明所屬的鎖,而鎖又可以是任意對象。能被任意對象調用的方法一定定義在Object類中。
接下里,我們先從一個簡單的示例入手:
如上圖說示,輸入執行緒向Resource中輸入name ,sex , 輸出執行緒從資源中輸出,先要完成的任務是:
- 1.當input發現Resource中沒有數據時,開始輸入,輸入完成後,叫output來輸出。如果發現有數據,就wait();
- 2.當output發現Resource中沒有數據時,就wait() ;當發現有數據時,就輸出,然後,叫醒input來輸入數據。
下面程式碼,模擬等待喚醒機制的實現:
- 模擬資源類
public class Resource { private String name; private String sex; private boolean flag = false; public synchronized void set(String name, String sex) { if (flag) try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } // 設置成員變數 this.name = name; this.sex = sex; // 設置之後,Resource中有值,將標記該為 true , flag = true; // 喚醒output this.notify(); } public synchronized void out() { if (!flag) try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } // 輸出執行緒將數據輸出 System.out.println("姓名: " + name + ",性別: " + sex); // 改變標記,以便輸入執行緒輸入數據 flag = false; // 喚醒input,進行數據輸入 this.notify(); } }
- 輸入執行緒任務類
public class Input implements Runnable { private Resource r; public Input(Resource r) { this.r = r; } @Override public void run() { int count = 0; while (true) { if (count == 0) { r.set("小明", "男生"); } else { r.set("小花", "女生"); } // 在兩個數據之間進行切換 count = (count + 1) % 2; } } }
- 輸出執行緒任務類
public class Output implements Runnable { private Resource r; public Output(Resource r) { this.r = r; } @Override public void run() { while (true) { r.out(); } } }
- 測試類
public class ResourceDemo { public static void main(String[] args) { // 資源對象 Resource r = new Resource(); // 任務對象 Input in = new Input(r); Output out = new Output(r); // 執行緒對象 Thread t1 = new Thread(in); Thread t2 = new Thread(out); // 開啟執行緒 t1.start(); t2.start(); } }
- 多執行緒有幾種實現方案,分別是哪幾種?
a, 繼承Thread類
b, 實現Runnable介面
c, 通過執行緒池,實現Callable介面
- 同步有幾種方式,分別是什麼?
a,同步程式碼塊
b,同步方法
靜態同步方法
- 啟動一個執行緒是run()還是start()?它們的區別?
啟動一個執行緒是start()
區別:
start: 啟動執行緒,並調用執行緒中的run()方法
run : 執行該執行緒對象要執行的任務
- sleep()和wait()方法的區別
sleep: 不釋放鎖對象, 釋放CPU使用權
在休眠的時間內,不能喚醒
wait(): 釋放鎖對象, 釋放CPU使用權
在等待的時間內,能喚醒
- 為什麼wait(),notify(),notifyAll()等方法都定義在Object類中
鎖對象可以是任意類型的對象