有關創建多執行緒 (Multi-threading) 的方式 - 繼承 Thread 類,可以參考我的另一篇文章 Java - Thread 執行緒(一)
除了上述說的方式創建新的執行緒;
在實際應用面,以透過實現 interface Runnable 來做是更好的方式,詳細的說明如下:
1.創建一個實現了 interface Runnable
的類別
2.實現類去實現 Runnable 的抽象方法: run()
3.創建實現類的物件
4.將此物件作為參數傳遞到 Thread 類的建構子(constructor)
中
5.通過 Thread 類的物件調用 start()
[ 註 ] 以下的例子為非thread-safety
,還必須優化;本文最後會介紹兩種優化方法
/**
* 售票窗口類
*/
class Window implements Runnable {
private int ticketAmount = 20;
@Override
public void run() {
while (true) {
if (ticketAmount > 0) {
// 進行賣票操作
System.out.println(Thread.currentThread().getName() + " : 售出票號 - " + ticketAmount);
ticketAmount--;
} else {
break;
}
}
}
}
/**
* 測試類
* @author sunwc
* @create 2023-03-23 下午 03:01
*/
public class ImplementsRunnableTest {
public static void main(String[] args) {
Window window = new Window();
Thread t1 = new Thread(window);
t1.setName("Window 1");
Thread t2 = new Thread(window);
t2.setName("Window 2");
Thread t3 = new Thread(window);
t3.setName("Window 3");
t1.start();
t2.start();
t3.start();
}
}
演示執行緒不安全輸出結果:
Window 1 : 售出票號 - 20
Window 2 : 售出票號 - 20
Window 3 : 售出票號 - 20
Window 2 : 售出票號 - 18
Window 2 : 售出票號 - 16
Window 1 : 售出票號 - 19
Window 2 : 售出票號 - 15
Window 3 : 售出票號 - 17
Window 2 : 售出票號 - 13
Window 2 : 售出票號 - 11
Window 2 : 售出票號 - 10
Window 2 : 售出票號 - 9
Window 2 : 售出票號 - 8
Window 2 : 售出票號 - 7
Window 2 : 售出票號 - 6
Window 2 : 售出票號 - 5
Window 2 : 售出票號 - 4
Window 2 : 售出票號 - 3
Window 2 : 售出票號 - 2
Window 2 : 售出票號 - 1
Window 1 : 售出票號 - 14
Window 3 : 售出票號 - 12
開發中會優先
選擇 實現 interface Runnable 方式
原因:
沒有
類別單一繼承 的侷限性共享資料
的情況其實原生的 Thread 類別 也是 interface Runnable 的實現類,
所以不管是子類繼承 Thread 類 或 實現類實現 interface Runnable ,相同點是:
需要 Override run(),將執行緒要執行的邏輯寫在 run() 中
執行緒安全問題
,問題點解釋:當一個執行緒A在操作ticket的時候,其它執行緒不能參與進來。直到執行緒A操作完ticket時,其它執行緒才可以開始操作ticket。這種情況即使執行A被阻塞了,其它執行緒也一定要等待
現實例子:
當某A在使用唯一一間廁所時,某B即使肚子痛要使用,也要等某A使用完才可以進入。因為某A鎖門了,某B只好等了
同步
機制,來解決執行緒安全的問題synchronized(同步鎖) {
// 需要被同步的程式
}
說明:
將共享資料
的程式用synchronized(同步鎖){}
包起來,當一個thread先進入這層,其它thread要先在這層外面等
共享資料:指的是Multi-threading
中共同操作的一個變數,以例子來說 ticketAmount 就是共享的資料
什麼是同步鎖?
任何一個類別的物件都可以充當鎖,但是Multi-threading 中的每個 thread 進入這層時都必須使用同一把鎖
侷限性:在同步程式區塊內,只能有一個執行緒參與,其它執行緒等待。區塊內相當於是一個單執行緒的過程,效率會比較低
/**
* 售票窗口類
*/
class Window implements Runnable {
private int ticketAmount = 20;
@Override
public void run() {
while(true) {
// 同步程式區塊 - Window.class(當前類別)充當唯一一把鎖
// 因為類別只會加載一次,當作唯一鎖相對安全
// 用this的話要看這個類別在主執行緒(main方法)中new了幾個物件
// 若只有一個就可以用this當唯一鎖
synchronized (Window.class) { // 或 synchronized (this)
if (ticketAmount > 0) {
// 執行緒阻塞,提高執行緒不安全機率
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 進行賣票操作
System.out.println(Thread.currentThread().getName() + " : 售出票號 - " + ticketAmount);
ticketAmount--;
} else {
break;
}
}
}
}
}
解決執行緒不安全輸出結果:
Window 1 : 售出票號 - 20
Window 1 : 售出票號 - 19
Window 1 : 售出票號 - 18
Window 1 : 售出票號 - 17
Window 1 : 售出票號 - 16
Window 1 : 售出票號 - 15
Window 1 : 售出票號 - 14
Window 3 : 售出票號 - 13
Window 2 : 售出票號 - 12
Window 2 : 售出票號 - 11
Window 2 : 售出票號 - 10
Window 2 : 售出票號 - 9
Window 2 : 售出票號 - 8
Window 2 : 售出票號 - 7
Window 2 : 售出票號 - 6
Window 3 : 售出票號 - 5
Window 3 : 售出票號 - 4
Window 1 : 售出票號 - 3
Window 1 : 售出票號 - 2
Window 1 : 售出票號 - 1
若使用到共享資料的程式完整的寫在一個方法中,我們不妨將此方法宣告成同步的
同步方法仍然涉及到同步鎖,只是不需要我們顯示的宣告
同步鎖 :
非靜態的同步方法:this (當前物件本身)
靜態的同步方法:當前類別本身
/**
* 售票窗口類
*/
class Window implements Runnable {
private static int ticketAmount = 20;
@Override
public void run() {
while (ticketAmount > 0) {
// 售票
sellTicket();
}
}
// private synchronized void sellTicket() {// 默認使用this當作鎖
private static synchronized void sellTicket() { // 默認使用Window.class當鎖
if (ticketAmount > 0) {
// 執行緒阻塞,提高執行緒不安全機率
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 進行賣票操作
System.out.println(Thread.currentThread().getName() + " : 售出票號 - " + ticketAmount);
ticketAmount--;
}
}
}
解決執行緒不安全輸出結果:
Window 1 : 售出票號 - 20
Window 1 : 售出票號 - 19
Window 1 : 售出票號 - 18
Window 3 : 售出票號 - 17
Window 3 : 售出票號 - 16
Window 3 : 售出票號 - 15
Window 3 : 售出票號 - 14
Window 3 : 售出票號 - 13
Window 3 : 售出票號 - 12
Window 3 : 售出票號 - 11
Window 3 : 售出票號 - 10
Window 3 : 售出票號 - 9
Window 3 : 售出票號 - 8
Window 3 : 售出票號 - 7
Window 2 : 售出票號 - 6
Window 2 : 售出票號 - 5
Window 2 : 售出票號 - 4
Window 2 : 售出票號 - 3
Window 2 : 售出票號 - 2
Window 2 : 售出票號 - 1
本文幫助了解如何
1.透過實現類 implements interface Runnable 達成 Multi-threading
2.透過將共享資源宣告成「同步的」以解決執行緒不安全問題
延伸學習:有關透過鎖 Lock 解決執行緒不安全問題,可以參考我的另一篇文章 Java - Lock - 解決執行緒不安全