主頁 > 後端開發 > Java 多執行緒

Java 多執行緒

2021-09-03 17:28:13 後端開發

目錄

1 執行緒與行程

2 執行緒調度

3 執行緒的兩種實作方式

4 Thread 的各種方法

5 執行緒安全

6 執行緒死鎖

7 執行緒間通信/互動

8 執行緒的六種狀態

9 執行緒的第三種實作方式

10 執行緒池

1 執行緒與行程

行程:一個記憶體中運行的應用程式,每個行程都有一個獨立的記憶體空間,

執行緒:行程中的一個執行路徑,同一行程中的執行緒共享一個記憶體空間,執行緒之間可以自由切換,并發執行,

  • 一個行程最少擁有一個執行緒,

  • 執行緒實際上是在行程的基礎上的進一步劃分,一個行程啟動后,里面的若干執行路徑又可以劃分成若干個路線,

  • 每個執行緒都擁有自己的堆疊空間(也就是說,由同一個執行緒呼叫的方法,都會在該執行緒內部執行),執行緒之間共用一份堆記憶體,

2 執行緒調度

  1. 分時調度

    所有執行緒輪流獲得 CPU 的使用權,平均分配每個執行緒占用 CPU 的時間,

  2. 搶占式調度

    優先級高的執行緒先使用 CPU,如果執行緒的優先級相同,則誰拿到使用權是隨機的(執行緒隨機性),

Java 中使用的執行緒調度方式為搶占式調度,CPU 采用搶占式調度模式在多個執行緒間進行著高速切換,某個時刻CPU 的一個核只能執行一個執行緒,由于切換速度飛快,看上去就像在同時進行多個任務一樣, 所以多執行緒并不能提高程式的運行速度,但通過提高 CPU 的使用率,從而提高了程式的運行效率,

3 執行緒的兩種實作方式

  1. 繼承Thread

通過繼承Thread類定義一個執行緒類,并通過重寫run()方法,在run()方法中完成執行緒需要執行的任務,在一個現有執行緒中創建一個執行緒類并呼叫其start()方法開啟執行緒(注意不是直接呼叫run()方法,直接呼叫run()并不會啟動新執行緒,而只是在當前執行緒中呼叫了一個方法而已)

// MyThread.java
// 定義一個執行緒類
public class MyThread extends Thread {
    // run() 中的程式就是執行緒要執行的任務
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("秦時明月漢時關" + i);
        }
    }
}

// Test.java
public class Test {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("萬里長征人未還" + i);
        }
    }
}

運行結果:

秦時明月漢時關0
萬里長征人未還0
秦時明月漢時關1
萬里長征人未還1
秦時明月漢時關2
萬里長征人未還2
秦時明月漢時關3
萬里長征人未還3
秦時明月漢時關4
萬里長征人未還4
秦時明月漢時關5
萬里長征人未還5
秦時明月漢時關6
萬里長征人未還6
秦時明月漢時關7
秦時明月漢時關8
萬里長征人未還7
秦時明月漢時關9
萬里長征人未還8
萬里長征人未還9
  1. 實作Runnable介面

通過實作Runnable介面定義一個任務類,再將該任務類物件交給Thread物件執行,從而完成執行緒的創建,

// MyRunnable.java
// 在 MyRunnable 類中重寫 run() 寫入需要執行的任務
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("但使龍城飛將在" + i);
        }
    }
}

// Test.java
public class Test {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t = new Thread(r);
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("不教胡馬度陰山" + i);
        }
    }
}

運行結果:

但使龍城飛將在0
不教胡馬度陰山0
但使龍城飛將在1
不教胡馬度陰山1
但使龍城飛將在2
不教胡馬度陰山2
但使龍城飛將在3
不教胡馬度陰山3
但使龍城飛將在4
不教胡馬度陰山4
但使龍城飛將在5
不教胡馬度陰山5
但使龍城飛將在6
不教胡馬度陰山6
但使龍城飛將在7
不教胡馬度陰山7
但使龍城飛將在8
不教胡馬度陰山8
但使龍城飛將在9
不教胡馬度陰山9

實作Runnable的方式跟繼承Thread的方式相比具有如下優勢:

  • 通過創建任務再給執行緒分配的方式實作多執行緒,更適合多個執行緒都執行相同任務的情況;
  • 可以避免單繼承所帶來的麻煩(Runnable是介面,Thread是類);
  • 任務與執行緒是分離的,降低了耦合性,提高了程式的健壯性
  • 執行緒池技術中,接收Runnable型別的任務,而不接收Thread型別的執行緒,
  1. 內部類

當然,如果只需要執行一次某個執行緒的話,通過以內部類或者匿名內部類的方式繼承Thread可以很簡單的實作,內部類的另一個好處是可以很方便的訪問外部的區域變數,

// 新執行緒
new Thread() {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("秦時明月漢時關" + i);
        }
    }
}.start();
// 當前執行緒
for (int i = 0; i < 10; i++) {
    System.out.println("萬里長征人未還" + i);
}

4 Thread 的各種方法

  1. 執行緒名稱
class MyRunable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());  // 獲取當前執行緒名稱
    }
}

// Test.java
public class Test {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()); // 主執行緒名稱為 "main"
        new Thread(new MyRunable(), "壹").start();  // 設定執行緒名稱為 "貳"
        Thread t = new Thread(new MyRunable());
        System.out.println(t.getName()); // 默認名稱為 "Thread-0"
       	t.setName("貳");  // 設定執行緒名稱為 "貳"
        t.start();
        new Thread(new MyRunable()).start(); // 默認名稱為 "Thread-1"
        new Thread(new MyRunable()).start(); // 默認名稱為 "Thread-2"
    }
}

運行結果:

main
Thread-0
壹
Thread-2
Thread-1
貳
  1. 執行緒休眠
public class Test {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            System.out.println(i);
            Thread.sleep(1000); // 執行緒休眠 1s
        }
    }
}

運行結果:從 0 開始每隔 1s 輸出加 1

0
1
2
3
4

還有一個多載的sleep()兩參方法:

// 毫秒 + 納秒
// 納秒允許范圍 [0, 999999]
public static void sleep(long millis, int nanos) {}
  1. 執行緒中斷

通過呼叫執行緒的interrupt(),通知執行緒關閉,由執行緒中的任務代碼決定是否結束自己的生命,

// MyRunable.java
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
            try {
                Thread.sleep(1000); // 執行緒在 sleep 期間檢查是否有中斷標記,若有則拋出例外
            } catch (InterruptedException e) {
                System.out.println("發現中斷標記,執行緒自殺");
                return; // 任務終止,執行緒從此結束
            }
        }
    }
}
// Test.java
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new MyRunnable());
        t.start();
        // 主執行緒每隔一秒輸出一次數字,共輸出 5 次,因此主執行緒回圈先結束
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
            Thread.sleep(1000);
        }
        t.interrupt(); // 結束回圈之后通知執行緒 t 中斷
    }
}

運行結果:

Thread-0: 0
main: 0
main: 1
Thread-0: 1
Thread-0: 2
main: 2
Thread-0: 3
main: 3
Thread-0: 4
main: 4
Thread-0: 5
發現中斷標記,執行緒自殺
  1. 守護執行緒
    • 執行緒分為用戶執行緒守護執行緒
    • 用戶執行緒:當一個行程不包含任何存活的用戶執行緒時,行程結束;
    • 守護執行緒:用于守護用戶執行緒,通常被用來做日志、性能統計等作業,當一個行程中最后一個用戶執行緒結束時,所有守護執行緒自動終止,行程結束,
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            public void run(){
                for (int i = 0; i < 10; i++){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.printf("%s 運行了:%ss\n",Thread.currentThread().getName(), i);
                }
            }
        };
        t.setDaemon(true); // 設定為守護執行緒
        t.start();
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
            Thread.sleep(1000);
        }
    }
}

運行結果:

main: 0
Thread-0 運行了:0s
main: 1
Thread-0 運行了:1s
main: 2
Thread-0 運行了:2s
main: 3
Thread-0 運行了:3s
main: 4
Thread-0 運行了:4s

5 執行緒安全

  1. 執行緒同步問題
// Ticket.java
public class Ticket implements Runnable{
    private int count = 5; // 剩余票數
    @Override
    public void run() {
        while (count > 0) {
            System.out.println("出票中...");
            try {
                Thread.sleep(800);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count--;
            System.out.println("出票成功,余票為:" + count);
        }
    }
}

// Test.java
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }
}

運行結果:

出票中...
出票中...
出票中...
出票成功,余票為:4
出票中...
出票成功,余票為:3
出票中...
出票成功,余票為:2
出票中...
出票成功,余票為:-1
出票成功,余票為:0
出票成功,余票為:1

上面例子中出現了最后余票為 -1 的情況,原因是在最后階段余票為 1 時,同時有不止一個執行緒進入了出票程式中,造成了不止一次的出票操作,因此上面代碼的操作流程是執行緒不安全的,

  1. 執行緒同步

    • synchronized同步代碼塊

    synchronized同步代碼塊使用一個物件 o 作為鎖,表示當前執行緒獨占該物件,這個物件又叫做同步物件,任何物件都可以作為同步物件,

    Object o = new Object();
    synchronized (lock){
    	//只有占有了 lock 后才可以執行此塊的代碼
    }
    

    如上面代碼所示為使用 synchronized 語法的格式,只有執行緒占用了鎖,它才能執行大括號部分代碼塊中的程式,執行結束又會釋放對鎖的占用,鎖被占用期間,其它試圖占用它的執行緒就會等待而不會進入代碼塊中執行,直到鎖被釋放,等待的執行緒就會爭搶該鎖,搶到占用的執行緒又進入代碼塊中執行程式,其它執行緒繼續等待,

    修改上面的出票任務為同步代碼塊的方式如下:

    // Ticke.java
    public class Ticket implements Runnable{
        private int count = 5; // 剩余票數
        final Object o = new Object();
        @Override
        public void run() {
            while (true) {
                synchronized (o) {
                    if (count > 0) {
                        System.out.println(Thread.currentThread().getName() + "出票中...");
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                        System.out.println("出票成功,余票為:" + count);
                    } else {
                        break;
                    }
                    // 讓當前執行緒執行完一次出票操作后,休眠一段時間,給其它執行緒更大的機會來搶占鎖
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    // Test.java
    public class Test {
        public static void main(String[] args) throws InterruptedException {
            Runnable run = new Ticket();
            new Thread(run).start();
            new Thread(run).start();
            new Thread(run).start();
        }
    }
    

    運行結果:

    Thread-0出票中...
    出票成功,余票為:4
    Thread-2出票中...
    出票成功,余票為:3
    Thread-2出票中...
    出票成功,余票為:2
    Thread-2出票中...
    出票成功,余票為:1
    Thread-1出票中...
    出票成功,余票為:0
    

    可以看到出票正常,個執行緒間沒有交叉出票的情況,當然,整個程式的執行時間也變長了,和單執行緒運行時間差不多,

    注意:必須使用同一個物件作為鎖,才能達到同步的效果,如果各個執行緒占用不同的物件作為鎖,實際上是沒有作用的,它們還是在一起執行同一段代碼塊,沒有實作等待的效果,

    • synchronized同步方法

    這種方式將需要同步的代碼塊提取為一個方法,并使用synchronized修飾,這種方式的鎖(同步物件)被隱式的指定為方法所在物件,即this物件,如果同步方法被static修飾,那么此時的同步物件為方法所在類的位元組碼檔案物件,即類名.class

    修改上面的出票任務為同步方法的方式如下:

    // Ticket.java
    public class Ticket implements Runnable{
        private int count = 5; // 剩余票數
        @Override
        public void run() {
            while (true) {
                if (!sale()) break;
                // 讓當前執行緒執行完一次出票操作后,休眠一段時間,給其它執行緒更大的機會來搶占鎖
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        private synchronized boolean sale() {
            if (count > 0) {
                System.out.println(Thread.currentThread().getName() + "出票中...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println("出票成功,余票為:" + count);
                return true;
            }
            return false;
        }
    }
    // Test.java
    public class Test {
        public static void main(String[] args) throws InterruptedException {
            Runnable run = new Ticket();
            new Thread(run).start();
            new Thread(run).start();
            new Thread(run).start();
        }
    }
    
    

    運行結果:

    Thread-0出票中...
    出票成功,余票為:4
    Thread-2出票中...
    出票成功,余票為:3
    Thread-1出票中...
    出票成功,余票為:2
    Thread-2出票中...
    出票成功,余票為:1
     Thread-0出票中...
    出票成功,余票為:0
    
   

   
   不論是同步代碼塊或者時同步方法,只要使用了同一個鎖,當鎖被某個執行緒占用時,任何使用該鎖的代碼塊或者方法都會被同步化,即其它執行緒不能執行其中的任何代碼塊或者方法,
   
   - `Lock`顯式鎖
   
   顯式鎖的方式通過直接定義一個 Java 中的`Lock`物件來實作同步效果,通過呼叫該物件的`lock()`方法上鎖,與`synchronized`語法自動釋放鎖不同,`Lock`物件必須通過顯式地呼叫`unlock()`方法來釋放鎖,所以,把`unlock()`放在`finally`中可以確保釋放的執行,
   
   修改上面的出票任務為顯式鎖的方式如下:
   
   ```java
   // Ticket.java
   import java.util.concurrent.locks.Lock;
   import java.util.concurrent.locks.ReentrantLock;
   
   public class Ticket implements Runnable{
       private int count = 5; // 剩余票數
       private Lock lk = new ReentrantLock();
       @Override
       public void run() {
           while (true) {
               lk.lock();
               try {
                   if (count <= 0) break;
                   System.out.println(Thread.currentThread().getName() + "出票中...");
                   Thread.sleep(500);
                   count--;
                   System.out.println("出票成功,余票為:" + count);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               } finally {
                   lk.unlock();
               }
               // 讓當前執行緒執行完一次出票操作后,休眠一段時間,給其它執行緒更大的機會來搶占鎖
               try {
                   Thread.sleep(100);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }
   }
   // Test.java
   public class Test {
       public static void main(String[] args) throws InterruptedException {
           Runnable run = new Ticket();
           new Thread(run).start();
           new Thread(run).start();
           new Thread(run).start();
       }
   }

運行結果:

Thread-1出票中...
出票成功,余票為:4
Thread-0出票中...
出票成功,余票為:3
Thread-2出票中...
出票成功,余票為:2
Thread-1出票中...
出票成功,余票為:1
Thread-0出票中...
出票成功,余票為:0

Java 中的Lock是一個介面,所以需要使用它的實作類ReentrantLock創建一個Lock物件,同樣的,為了達到同步的效果,即執行緒安全的目的,必須要讓各個執行緒使用同一個Lock物件才行,

  • 公平鎖

上述的三種執行緒同步的實作中,使用的都不是公平鎖,前面說過 Java 中采用搶占式調度模式切換執行緒,就是說,當某個鎖被釋放時,并不一定是最先前來請求占用的執行緒占用到該鎖,而是隨機的,所以是“不公平”的,

可以在顯式鎖的方式中實作公平鎖,即讓執行緒排隊獲取鎖的占用權,只需要在創建ReentrantLock物件時,傳入為boolean型別引數傳入true就行,

Lock lk = new ReentrantLock(true);

6 執行緒死鎖

執行緒死鎖的現象:兩個執行緒占有了對方需要的鎖,導致了兩邊都在等待對方釋放鎖,使得程式永遠處于僵持狀態,或者多個執行緒之間占用了后面執行緒需要的鎖,形成了死鎖環,下面通過一個應聘者和招聘者之間的例子展示兩個執行緒之間的死鎖現象,

// Candidate.java
public class Candidate {
    public synchronized void sayTo(Recruiter recruiter) {
        System.out.println("應聘者:你給我作業,我就有作業經驗了!");
        // 停一段時間,給另一個執行緒有足夠的時間占用 recruiter 鎖
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 等待應聘者回應
        recruiter.response();
    }

    public synchronized void response() {
        System.out.println("好我有作業經驗了!");
    }
}
// Recruiter.java
public class Recruiter {
    public synchronized void sayTo(Candidate candidate) {
        System.out.println("面試官:你有作業經驗,我就給你作業!");
        // 停一段時間,另一個執行緒有足夠的時間占用 candidate 鎖
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 等待應聘者回應
        candidate.response();
    }

    public synchronized void response() {
        System.out.println("好我給你作業!");
    }

}
// DeadLock.java 對話僵局
public class DeadLock {
    public static void main(String[] args) {
        Candidate candidate = new Candidate();
        Recruiter recruiter = new Recruiter();
        new Thread(new Runnable() {
            @Override
            public void run() {
                candidate.sayTo(recruiter);
            }
        }).start();
        recruiter.sayTo(candidate);
    }
}

程式會進入死鎖,物件recruiter在等待物件candidate釋放作為鎖的自己,反過來candidate也在等待recruiter釋放作為鎖的自己,程式卡住了,永遠不會結束,局面十分焦灼,,,

死鎖現象

在程式中避免死鎖現象的最直接有效的辦法就是,在會產生鎖的代碼塊或方法中不要呼叫另外一個會產生鎖的代碼塊或方法,

7 執行緒間通信/互動

同步執行緒之間通過呼叫鎖物件的wait()/notify()/notifyAll()方法相互通知,某個執行緒通過呼叫鎖物件.wait()讓自己進入等待狀態,之后該執行緒不再暫停運行,直到另外某個執行緒呼叫鎖物件.notify(),前面暫停的執行緒才可能重新回到運行狀態,只是可能,因為notify()只會讓處于等待狀態的執行緒中的一個重新運行,或者當某個執行緒運行中呼叫鎖物件.notifyAll(),這會讓所有處于等待狀態中的執行緒都恢復運行,下面是一個炒菜與上菜的簡單例子:

// Food.java 食物
public class Food {
    private String name;
    private String taste;
	// 做菜
    public synchronized void setNameAndTaste(String name, String taste) {
        System.out.println("開始做菜...");
        this.name = name;
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.taste = taste;
        System.out.println("菜做好了,");
    }
	// 上菜
    public synchronized void get() {
        System.out.println("上菜: ");
        System.out.println(this.name + "-" + this.taste);
    }
}
// Cook.java 廚師
public class Cook implements Runnable{
    private final Food f;
    public Cook(Food f) {
        this.f = f;
    }
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            if (i % 2 == 0) {
                f.setNameAndTaste("西紅柿炒番茄", "麻辣味");
            } else {
                f.setNameAndTaste("馬鈴薯燒土豆", "芥末味");
            }
        }
    }
}
// Waiter.java 服務員
public class Waiter implements Runnable{
    private final Food f;
    public Waiter(Food f) {
        this.f = f;
    }
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            f.get();
        }
    }
}
// 上菜服務
public class Serving {
    public static void main(String[] args) {
        Food f = new Food();
        new Thread(new Cook(f)).start();
        new Thread(new Waiter(f)).start();
    }
}

運行結果:

開始做菜...
菜做好了,
上菜:
西紅柿炒番茄-麻辣味
開始做菜...
菜做好了,
上菜:
馬鈴薯燒土豆-芥末味
上菜:
馬鈴薯燒土豆-芥末味
上菜:
馬鈴薯燒土豆-芥末味
上菜:
馬鈴薯燒土豆-芥末味
開始做菜...
菜做好了,
開始做菜...
菜做好了,
開始做菜...
菜做好了,

上面程式中,即使Food中的方法已經同步化了,使得做菜setNameAndTaste()和上菜get()的程序你不會相互干擾,但上菜服務的流程仍然是不合理的,waiter再上完一道菜后,沒等cook 呼叫setNameAndTaste()做下一道菜,就開始繼續呼叫get()上菜了,同樣可能的是,cook做完一道菜還沒等waiter呼叫get()上菜,就繼續呼叫setNameAndTaste()換做下一道菜了,這個問題稱為生產者與消費者問題,這里cook作為生產者,waiter作為消費者,

合理的流程應該是生產者在產生資料時,消費者應該暫停執行使用資料的操作,反過來,消費者在使用資料時,生產者應該暫停更新資料的才做,上面例子的問題可以通過執行緒間通信的方式解決,需要做修改的是Food類中的setNameAndTaste()get()

public class Food {
    private String name;
    private String taste;

    public synchronized void setNameAndTaste(String name, String taste) {
        System.out.println("開始做菜...");
        this.name = name;
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.taste = taste;
        System.out.println("菜做好了,");
        this.notifyAll(); // 通知其它執行緒恢復運行
        try {
            this.wait(210); // 進入等待狀態 210ms
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized void get() {
        System.out.println("上菜: ");
        System.out.println(this.name + "-" + this.taste);
        this.notifyAll(); // 通知其它執行緒恢復運行
        try {
            this.wait(210); // 進入等待狀態 210ms
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

這樣的Serving.java運行結果為:

開始做菜...
菜做好了,
上菜: 
西紅柿炒番茄-麻辣味
開始做菜...
菜做好了,
上菜: 
馬鈴薯燒土豆-芥末味
開始做菜...
菜做好了,
上菜: 
西紅柿炒番茄-麻辣味
開始做菜...
菜做好了,
上菜: 
馬鈴薯燒土豆-芥末味
開始做菜...
菜做好了,
上菜: 
西紅柿炒番茄-麻辣味

8 執行緒的六種狀態

執行緒的六種狀態以列舉形式存在于Thread類中,

public enum State {
    NEW, // 已創建未運行的狀態
    RUNNABLE, // 運行狀態
    BLOCKED, // 阻塞狀態,等待占用鎖物件從而進入到同步代碼塊或者同步方法中
    WAITING, // 等待狀態,
    TIMED_WAITING, // 定時等待狀態,執行緒暫停運行一段時間
    TERMINATED; // 執行緒生命結束
}

這幾種狀態再執行緒的生命周期中的轉換關系如下圖:

執行緒狀態

9 執行緒的第三種實作方式

Java 中除了上面使用ThreadRunnable的兩種方式,還可以使用Callable介面實作執行緒,與前兩種方式不同,Callable可以實作有回傳值的執行緒,而前兩種沒有回傳值,

使用Callable實作執行緒的步驟如下:

  1. 實作Callable介面,實作其中的call()方法;
  2. 創建FutureTask物件,并傳入一個前面實作Callable介面的類的物件;
  3. 通過Thread創建執行緒,傳入第二步中的FutureTask物件作為執行緒的執行任務,啟動執行緒,

獲取執行緒回傳值的方式是呼叫FutureTask物件的get()方法,此方法會阻塞主行程執行,一直等到FutureTask物件所在執行緒結束回傳,具體實作如下例:

// MyCallable.java
import java.util.concurrent.Callable;
// 1. 實作 Callable 介面
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
        return 100;
    }
}
// Test.java
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Test {
    public static void main(String[] args)
        	throws ExecutionException, InterruptedException {
        // 2. 創建 FutureTask 物件,并傳入一個實作 Callable 介面的類的物件;
        Callable<Integer> call = new MyCallable();
        FutureTask<Integer> task = new FutureTask<>(call);
        // 3. 通過 Thread 創建執行緒,傳入 task,啟動執行緒,
        new Thread(task).start();
        Integer num = task.get(); // 主執行緒會等待執行 task 的執行緒執行結束并接收一個回傳值,然后再繼續往下執行
        System.out.println("task 回傳結果:" + num);
        for (int i = 0; i < 10; i++) {
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}

運行結果:

Thread-0: 0
Thread-0: 1
Thread-0: 2
Thread-0: 3
Thread-0: 4
task 回傳結果:100
main: 0
main: 1
main: 2
main: 3
main: 4

我們也可以在主執行緒中不用一直等待task所在執行緒結束,而是在主執行緒執行程序中某個時機呼叫task.isDone()來查看task所在執行緒是否結束,檢查到已經結束時,在取得其回傳值,具體來說,修改上面Test.java如下:

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> call = new MyCallable();
        FutureTask<Integer> task = new FutureTask<>(call);
        System.out.println("task 回傳結果:" + num);
        for (int i = 0; i < 10; i++) {
            if (task.isDone()) {
                int num = task.get();
                System.out.println("task 回傳結果:" + num);
                break;
            }
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}

運行結果:

Thread-0: 0
main: 0
Thread-0: 1
main: 1
main: 2
Thread-0: 2
Thread-0: 3
main: 3
Thread-0: 4
main: 4
main: 5
task 回傳結果:100

還可以通過呼叫task.cancel(true)來主動取消task所在執行緒的執行,如果取消成功,該方法會回傳true,這表示該執行緒在自然結束之前被取消了;如果取消失敗,則回傳false,這就表示該執行緒在執行取消操作之前已經結束了,

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> call = new MyCallable();
        FutureTask<Integer> task = new FutureTask<>(call);
        new Thread(task).start();
        for (int i = 0; i < 3; i++) {
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
        if (task.cancel(ture)) {
            System.out.println("取消成功!");
        }
    }
}

運行結果:

main: 0
Thread-0: 0
main: 1
Thread-0: 1
main: 2
Thread-0: 2
取消成功!

10 執行緒池

如果為了頻繁地執行很多任務,且這些任務的執行時間很短,我們為此頻繁地創建了很多執行緒,因為創建執行緒和銷毀執行緒需要時間,所以大量的時間都消耗在了執行緒的創建和銷毀上,這樣就導致了系統的效率大大降低, 執行緒池就是為了解決這個問題的,執行緒池是一個容納多個執行緒的容器,其中的執行緒可以反復使用,省去了頻繁創建執行緒物件的操作,從而可以省下很多的時間和資源,

執行緒池的好處:

  • 降低資源消耗
  • 提高回應速度
  • 提高執行緒的可管理性

Java 中提供了四種執行緒池,分別是快取執行緒池、定長執行緒池、單執行緒執行緒池和周期性任務定長執行緒池,

  • 快取執行緒池

    快取執行緒池的執行緒陣列長度無限制,可以變化,其執行流程是:

    1. 判斷執行緒池是否存在空閑執行緒
    2. 存在則使用
    3. 不存在,則創建執行緒 并放入執行緒池, 然后使用
// 使用快取線城市
ExecutorService service = Executors.newCachedThreadPool();
// 指揮執行緒池執行新的任務
service.execute(() -> System.out.println(Thread.currentThread().getName() + " haha"));
service.execute(() -> System.out.println(Thread.currentThread().getName() + " keke"));
try {
    Thread.sleep(100);
} catch (InterruptedException e) {
    e.printStackTrace();
}
service.execute(() -> System.out.println(Thread.currentThread().getName() + " didi"));

運行結果(可以看到第三個任務在前面的第二個執行緒中執行,因為前面的執行緒已經空閑下來了):

pool-1-thread-2 keke
pool-1-thread-1 haha
pool-1-thread-2 didi
  • 定長執行緒池

    定長執行緒池的執行緒陣列的長度是指定不變的,其執行流程如下:

    1. 判斷執行緒池是否存在空閑執行緒
    2. 存在則使用
    3. 不存在空閑執行緒,且執行緒池未滿的情況下,則創建執行緒并放入執行緒池,,然后使用
    4. 不存在空閑執行緒,且執行緒池已滿的情況下,則等待執行緒池存在空閑執行緒
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(() -> {
    System.out.println(Thread.currentThread().getName() + " hahaha");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
service.execute(() -> {
    System.out.println(Thread.currentThread().getName() + " hahaha");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
service.execute(() -> System.out.println(Thread.currentThread().getName() + "hahaha"));

運行結果(可以看到執行緒池不會第三個任務創建新的執行緒,而是會等待前兩個執行緒空閑下來再執行第三個任務):

pool-1-thread-1 hahaha
pool-1-thread-2 xixixi
pool-1-thread-1 yuyuyu
  • 單執行緒執行緒池

    單執行緒執行緒池相當于將定長執行緒池的執行緒陣列長度指定為 1,所以執行流程和前面一樣,只是始終只有一個執行緒可供使用,它可以用來操作需要排隊執行的任務,

ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(() -> {
    System.out.println(Thread.currentThread().getName() + " hahaha");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
service.execute(() -> {
    System.out.println(Thread.currentThread().getName() + " xixixi");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
service.execute(() -> {
    System.out.println(Thread.currentThread().getName() + " bobobo");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

運行結果(可以看到從頭到尾只有一個執行緒執行任務):

pool-1-thread-1 hahaha
pool-1-thread-1 xixixi
pool-1-thread-1 bobobo
  • 周期性任務定長執行緒池

    周期性任務定長執行緒池的執行流程和定長執行緒池差不多,但它可以在同一個執行緒中周期性地執行同一個任務,

ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
// 指定 3 個時間單位后執行任務,時間單位為秒
service.schedule(() -> System.out.println(Thread.currentThread().getName() + "hehe"), 3, TimeUnit.SECONDS);
// 指定 2 個時間單位后執行任務,每個 1 個時間單位執行一次同樣的任務,時間單位為秒
service.scheduleAtFixedRate(() -> System.out.println(Thread.currentThread().getName() + "wuwu"), 2, 1, TimeUnit.SECONDS);

運行結果(pool-1-thread-1 在 2s 時啟動,并每隔 1 秒列印 wuwu,pool-1-thread-2 在 3s 時啟動并列印一次 hehe 后結束):

pool-1-thread-1 wuwu
pool-1-thread-1 wuwu
pool-1-thread-2 hehe
pool-1-thread-1 wuwu
pool-1-thread-1 wuwu
pool-1-thread-1 wuwu
... ...

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/297000.html

標籤:Java

上一篇:Go 中的 byte、rune 與 string

下一篇:kubebuilder實戰之八:知識點小記

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more