Java多執行緒(四)
目錄
四、執行緒的同步
4.1 執行緒同步介紹
-
場景案例
例如電影院有三臺自動售票機售賣100張電影票,不考慮實際情況,票號從100號依次減少,售票過程包括出票和自減兩個步驟,先自減再出票,當其中一臺機器A自減結束未完成出票的操作,另一臺機器B執行了自減操作,這樣導致兩臺機器均售出了相同的票(重票、漏票),再者,剩餘最後一張票時,三臺機器同時執行自減操作,這樣三臺售賣機可能賣出0號票、-1號票的情況(錯票)。所以,當一臺售票機執行售票操作(自減和出票),不允許其他機器同時執行售票操作(即執行緒同步),三臺售票機相當於三條執行緒,而100張電影票則是共享資源,且實際情況下,出票和自減均是一瞬間的事情,所以從感官上是三臺機器同時操作。
-
原因分析
當多條語句在操作執行緒共享資料時,一個執行緒對多條語句只執行其中一部分,還沒有執行完成,另一個執行緒參與進來,導致共享資料錯誤,即發生執行緒安全問題。
-
解決方案
對於多條操作共享資料的語句,只允許其中一個執行緒都執行完,在執行過程中,不允許其他的執行緒參與執行,哪怕正在執行中的執行緒處於堵塞狀態,也不允許其他執行緒去執行。
-
執行緒同步
當執行緒共享了相同的資源,在某些時刻同時讀寫該資源時,可能會產生衝突,從而導致執行緒的安全問題,因此一次只能允許其中一條執行緒來訪問共享資源,即執行緒同步。
4.2 執行緒同步實現
4.2.1 場景介紹
-
需求描述
準備100張票作為共享資源,三臺自動售票機進行售票,即啟動三個執行緒去消費100張電影票。
-
繼承Thread方式
class Window1 extends Thread { private static Integer ticket = 100; @Override public void run() { while (true) { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket); ticket--; } else { break; } } } }
Window1 w1 = new Window1();
Window1 w2 = new Window1();
Window1 w3 = new Window1();
w1.setName("視窗1");
w2.setName("視窗2");
w3.setName("視窗3");
w1.start();
w2.start();
w3.start();
-
實現Runnable介面
class Window2 implements Runnable {
private Integer ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
ticket--;
} else {
break;
}
}
}
}
Window2 w = new Window2();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("視窗1");
t2.setName("視窗2");
t3.setName("視窗3");
t1.start();
t2.start();
t3.start();
-
錯誤現象
4.2.2 Synchronized使用
-
同步程式碼塊
// 格式
synchronized(同步監視器){
// 需要被同步的業務程式碼程式
}
1.操作共享資料的程式程式碼,即是需要被同步的程式碼,不能包含程式碼多了,也不能包含程式碼少了;
2.共享資料,即多個執行緒可以共同操作的變數,例如100張電影票;
3.同步監視器,俗稱鎖,任何一個物件都可以充當鎖,潛在的要求就是多個執行緒物件必須共用一把鎖,所以推薦繼承Thread類的的子類,使用子類的物件(類名.class),實現Runnable介面的子類,使用當前子類物件本身(this)。
-
繼承Thread方式
private static Integer ticket = 100;
while (true) {
synchronized (Window3.class) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
ticket--;
} else {
break;
}
}
}
}
-
實現Runnable介面
private Integer ticket = 100;
@Override
public void run() {
while (true) {
synchronized (this) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
ticket--;
} else {
break;
}
}
}
}
-
同步方法
// synchronized還可以放在方法宣告中,表示整個方法為同步方法。
private static synchronized void show();
private synchronized void show();
// 在方法的修飾詞中加上synchronized關鍵字
1.靜態方法,同步方法的同步監視器是類名.class;
2.非靜態方法,同步方法的同步監視器是this
-
繼承Thread方式
private static synchronized void show() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
ticket--;
}
}
-
實現Runnable介面
private synchronized void show() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
ticket--;
}
}
注意,這裡一個使用的是靜態的同步方法,一個使用的非靜態的同步方法。
總結
優點:同步的方式,解決了執行緒的安全問題 缺點:操作同步程式碼,只能一個執行緒參與,其他執行緒等待,相當於是單執行緒操作,效率低
同步鎖機制: 在《Thinking in Java》中,是這麼說的:對於併發工作,你需要某種方式來防 止兩個任務訪問相同的資源(其實就是共享資源競爭)。 防止這種衝突的方法 就是當資源被一個任務使用時,在其上加鎖。第一個訪問某項資源的任務必須 鎖定這項資源,使其他任務在其被解鎖之前,就無法訪問它了,而在其被解鎖 之時,另一個任務就可以鎖定並使用它了。