Thread

Prerequisites:

名詞介紹

程式(Program)透過javac編譯後,產生了.class檔,.class檔又稱為位元碼。

javac 類別.java

把.class位元碼載入到記憶體,程式執行的過程,稱之為程序(Process)。

java 類別

可以執行多次java 類別指令,產生很多程序。

img

操作系統(OS)可以管理多個程序(Process),可以讓多個程序(Process)輪流使用CPU資源。

Jvm視為虛擬的操作系統,管理多個程序中的多個執行緒。

每一個執行緒(Thread)都是一個流程。

每個程序(Process)都有一個主要執行緒(Main Thread),也稱作主要流程,是程式的入口。

想「同時執行」很多流程,每一個流程都是一個執行緒,下圖中,main主要執行緒啟動了2個執行緒,其中一個執行緒,又再啟動2個執行緒,大家來「同時」執行。

img

Concurrent併發

一個CPU執行多個執行緒,Jvm有一個排程管理系統,會十分快速的將一個流程(執行緒)切換到另一個流程(執行緒),快速的切換讓使用者以為是同時執行這些流程(執行緒),實際上是十分快速的輪流執行(執行緒)。

img

Parallel

多個CPU執行多個執行緒,平行執行任務。

img

但執行緒若有100個,不可能有100個CPU彼此對映,大部分的時候都是Concurrent + Parallel互相搭配,1核心就是1個CPU,8核心就是8個CPU,可能8個CPU分配處理100個執行緒,每一個CPU快速切換在各個不同執行序之間。

Runnable 介面

要實作Runnable,並且覆寫run方法,類別才能變成執行緒。

1
2
3
public interface Runnable {
    void run();
}

Thread原始碼

Thread實作Runnable介面,並且覆寫run()方法。

實作Runnable介面的類別,才可以放進Thread(Runnable task)建構子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Thread implements Runnable {
    // 無參數建構子
    public Thread() {
        this(null, null, 0, null, 0, null);
    }

    // 參數是有實作Runnable的類別
    public Thread(Runnable task) {
        this(null, null, 0, task, 0, null);
    }

    // 覆寫run方法
    public void run() {
        Runnable task = holder.task;
        if (task != null) {
            Object bindings = scopedValueBindings();
            runWith(bindings, task);
        }
    }

    // 啟動執行緒
    public void start() {
        synchronized (this) {
            // zero status corresponds to state "NEW".
            if (holder.threadStatus != 0)
                throw new IllegalThreadStateException();
            // 呼叫Native C++的執行緒方法
            start0();
        }
    }

    // Native C++的執行緒方法,start0會呼叫run()方法
    private native void start0();
}

Thread.start()

啟動執行緒。

Thread.run()

不是啟動執行緒,run()是給native c++的start0()呼叫。

使用run()方法,就等同於main呼叫方法,並不是啟動執行緒。

Thread.sleep()

讓執行緒暫時休息。

參數是毫秒,1秒是1000毫秒,0.5秒是500毫秒,0.1秒是100毫秒,0.01秒是10毫秒,0.001秒是1毫秒,請自行換算。

使用時會有Exception,要強制處理Exception。

1
2
3
4
5
  try {
    Thread.sleep(1000);  // 休息1秒
  } catch (InterruptedException e) {
    // 要處理異常
  }

等待

傳統寫程式流程,在main()方法中呼叫方法,方法執行完,再回main方法。

main()要等待方法執行完,才能執行呼叫方法以下的程式碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test2 {
  public static void main(String[] args) {
    // 呼叫func1()
    func1();

    System.out.println("回到main方法");
    System.out.println("執行func1()之後的程式碼");
  }

  public static void func1 () {
    System.out.println("執行func1()");
  }
}
執行func1()
回到main方法
執行func1()之後的程式碼

執行緒與主程式同時執行

執行緒與main執行緒是同時執行,main不等待。

下圖中,程序呼叫主要執行緒(Main Thread),main啟動執行緒1,但main執行結束,執行緒1仍在執行,直到程序中所有執行緒都結束了,程序才結束。

img

建立Thread

建立Thread有二個方式,一個是繼承thread類別,一個是實作Runnable介面。

繼承thread類別

Java不支援多繼承,繼承Thread後就沒辦法繼承其它類別,建議使用Runnable,比較不會有限制。

類別繼承Thread,並覆寫run()方法。

1
2
3
4
5
6
public class 類別名 extends Thread {
  @Override
  public void run() {
    // 要執行的流程
  }
}

啟動執行緒

1
new 類別名().start();

程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Thread1 extends Thread {
  @Override
  public void run() {
    // 每1秒印1句話,執行10秒
    for (int i = 0; i < 10; i++) {
      System.out.println("Thread1 i = " + i);
      try {
        // 休息1秒
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }

    System.out.println("Thread1 執行緒結束");
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
  public static void main(String[] args) throws InterruptedException {
    // 建立執行緒
    Thread1 t1 = new Thread1();
    // 啟動執行緒
    t1.start();

    // 每1秒印1句話,執行5秒
    for (int i = 0; i < 5; i++) {
      System.out.println("main i = " + i);
      // 休息1秒
      Thread.sleep(1000);
    }

    System.out.println("main 主要執行緒結束");
  }
}
main i = 0
Thread1 i = 0
Thread1 i = 1
main i = 1
Thread1 i = 2
main i = 2
Thread1 i = 3
main i = 3
Thread1 i = 4
main i = 4
main 主要執行緒結束
Thread1 i = 5
Thread1 i = 6
Thread1 i = 7
Thread1 i = 8
Thread1 i = 9
Thread1 執行緒結束

Process finished with exit code 0

由執行結果可以發現main與Thread1同時執行,而且main比Thread1提早結束。

Thread1執行結束,程序才結束。

實作Runnable介面

類別實作Runnable介面,並覆寫run()方法。

1
2
3
4
5
6
public class 類別名 implements Runnable {
  @Override
  public void run() {
    // 要執行的流程
  }  
}

先前Thread原始碼有提到,Thread類別才有start()方法,要透過start()方法啟動執行緒。

所以要把實作Runnable物件放入Thread有參數的建構子。

1
2
3
4
5
6
public class Thread implements Runnable {
    // 參數是有實作Runnable的類別
    public Thread(Runnable task) {
        this(null, null, 0, task, 0, null);
    }
}

把Runable物件放進Thread的有參數的建構子,然後使用Thread類別start()方法啟動執行緒。

1
new Thread(new Runnable物件()).start();

程式碼,把上一個程式碼修改成實作Runnable。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class R1 implements Runnable {
  @Override
  public void run() {
    for (int i = 0; i < 10; i++) {
      System.out.println("R1 i = " + i);
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    System.out.println("R1 執行緒結束");
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
  public static void main(String[] args) throws InterruptedException {
    // 建立Runnable物件
    R1 r1 = new R1();
    // 把Runnable物件放入Thread的建構子
    Thread t1 = new Thread(r1);
    // 啟動執行緒
    t1.start();
    for (int i = 0; i < 5; i++) {
      System.out.println("main i = " + i);
      Thread.sleep(1000);
    }
    System.out.println("main 主要執行緒結束");
  }
}

匿名類別

Runnable匿名類別

要把實作Runnable匿名物件放入Thread有參數的建構子。

1
2
3
4
new Thread(new Runnable() {
  @Override
  public void run() { ... }
}).start();

thread匿名類別

使用無參數的建構子。

1
2
3
4
5
6
new Thread() {
  @Override
  public void run() {
    // 要執行的流程
  }
}.start();

結束執行緒

return

使用return,就會結束執行緒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Thread1 implements Runnable {
  private int count = 0;
  @Override
  public void run() {
    for (int i = 0; i < 10; i++) {
      for (int j = 0; j < 3; j++) {
        System.out.println("Thread1 count  = " + (++count));
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        // 若count等於5,就結束執行緒
        if( count == 5) return;
      }
    }
    System.out.println("Thread1 執行緒結束");
  }
}
Thread1 count  = 1
main i = 0
main i = 1
Thread1 count  = 2
main i = 2
Thread1 count  = 3
main i = 3
Thread1 count  = 4
main i = 4
Thread1 count  = 5
main 主要執行緒結束

由執行結果發現,執行到count = 5,Thread1就結束了。

break

離開迴圈也是結束執行緒的方法,如果只有1層迴圈,可以離開執行緒,2層迴圈使用break,是只跳出目前的迴圈,並非離開函式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Thread1 implements Runnable {
  private int count = 0;
  @Override
  public void run() {
    for (int i = 0; i < 10; i++) {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      // 若count等於5,就結束執行緒
      if( count == 5) break;
    }
    System.out.println("Thread1 執行緒結束");
  }
}

使用flag

提供set方法,設定私有屬性flag。

由其它類別控制何時結束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class R2 implements Runnable {
  // 控制結束的flag
  private boolean flag = true;

  // 由其它類別來設定flag
  public void setFlag(boolean flag) {
    this.flag = flag;
  }

  @Override
  public void run() {
    while (flag) {
      System.out.println(Thread.currentThread().getName() + "執行中");
      // 每秒印1次
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test2 {
  public static void main(String[] args) throws InterruptedException {
    R2 r2 = new R2();
    // 啟動執行緒
    new Thread(r2).start();

    for (int i = 0; i < 10; i++) {
      // i為5就把執行緒關掉
      if(i == 5) {
        r2.setFlag(false);
      }
      // 每秒印1次
      System.out.println("i = " + i);
      Thread.sleep(1000);
    }
  }
}
Thread-0執行中
i = 0
Thread-0執行中
i = 1
Thread-0執行中
i = 2
Thread-0執行中
i = 3
Thread-0執行中
i = 4
Thread-0執行中
i = 5
i = 6
i = 7
i = 8
i = 9

Thread類別常用方法

setName()

設定緒行緒的名字。

1
2
3
4
5
6
7
8
9
10
public class Test {
  public static void main(String[] args) throws InterruptedException {
    // 建立執行緒物件
    Thread1 t1 = new Thread1();
    //設定執行緒的名字
    t1.setName("執行緒1");
    // 啟動執行緒
    t1.start();
  }
}

currentThread().getName()

取得目前執行緒的名字。

1
2
3
4
5
6
7
8
9
class R1 implements Runnable {
  @Override
  public void run() {
    for (int i = 0; i < 10; i++) {
      // 取得目前執行緒的名字。
      System.out.println(Thread.currentThread().getName() + " i  = " + i);
    }
  }
}

在main中取得執行緒的名字。

1
2
3
4
5
public class Test {
  public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName());
  }
}
main

執行緒共享

所謂的共享是不同的執行緒(Thread),存取相同的記憶體位址。

Runnable共享

只要建立一個Runnable物件,多個執行緒可以共享同一個Runnable物件的變數。

同一個物件建立的執行緒,多個執行緒可使用相同的成員屬性。

img

R1是實作Runnable的物件。

1
2
3
4
5
6
7
8
9
10
11
12
13
class R1 implements Runnable {
  // 執行緒使用相同屬性money
  private int money = 10000;
  @Override
  public void run() {
    // 無窮迴圈
    while (true) {
      if (money <= 0) break;
      money = money - 1000;
      System.out.println(Thread.currentThread().getName() + " 提領 1000 元,剩下 " + money + " 元");
    }
  }
}

同時啟動2個執行緒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test {
  public static void main(String[] args) {
    // 建立Runnable物件
    R1 r1 = new R1();

    // 使用r1 Runnable物件為參數
    Thread t1 = new Thread(r1);
    // 設定名字
    t1.setName("執行緒1");

    // 使用r1 Runnable物件為參數
    Thread t2 = new Thread(r1);
    // 設定名字
    t2.setName("執行緒2");

    // 啟動執行緒
    t1.start();
    t2.start();
  }
}
執行緒2 提領 1000 元,剩下 8000 元
執行緒2 提領 1000 元,剩下 7000 元
執行緒2 提領 1000 元,剩下 6000 元
執行緒2 提領 1000 元,剩下 5000 元
執行緒2 提領 1000 元,剩下 4000 元
執行緒1 提領 1000 元,剩下 9000 元
執行緒1 提領 1000 元,剩下 2000 元
執行緒1 提領 1000 元,剩下 1000 元
執行緒1 提領 1000 元,剩下 0 元
執行緒2 提領 1000 元,剩下 3000 元

因為沒有使用synchronized,所以執行結果不如預期,但二個執行緒都使用相同money屬性。

繼承Thread共享資源

繼承Thread沒辦法共用相同物件屬性,因為每一次都是用new Thread(),每一次new都是不同物件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  // 建立執行緒
  Thread1 t1 = new Thread1();
  // 啟動執行緒
  t1.start();

  // 建立執行緒
  Thread1 t2 = new Thread1();
  // 啟動執行緒
  t2.start();
  
  // 建立執行緒
  Thread1 t3 = new Thread1();
  // 啟動執行緒
  t3.start();        

只能使用以下二種方法,存取相同的記憶體位址。

  1. Static靜態變數
  2. 提供一個public方法或建構子,傳入相同物件,才能存取相同的記憶體位址。

使用Static變數

1
2
3
4
5
6
7
8
9
10
11
class TestThread extends Thread {
  private static int money = 10000;
  @Override
  public void run() {
    while (true) {
      if (money <= 0) break;
      money = money - 1000;
      System.out.println(Thread.currentThread().getName() + " 提領 1000 元,剩下 " + money + " 元");
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
  public static void main(String[] args) {
    TestThread t1 = new TestThread();
    t1.setName("執行緒1");

    TestThread t2 = new TestThread();
    t2.setName("執行緒2");

    //啟動執行緒
    t1.start();
    t2.start();
  }
}
執行緒2 提領 1000 元,剩下 8000 元
執行緒2 提領 1000 元,剩下 7000 元
執行緒2 提領 1000 元,剩下 6000 元
執行緒2 提領 1000 元,剩下 5000 元
執行緒2 提領 1000 元,剩下 4000 元
執行緒2 提領 1000 元,剩下 3000 元
執行緒2 提領 1000 元,剩下 2000 元
執行緒2 提領 1000 元,剩下 1000 元
執行緒2 提領 1000 元,剩下 0 元
執行緒1 提領 1000 元,剩下 9000 元

使用物件

建立提款卡類別,裡面有10000元。

1
2
3
4
5
6
7
8
9
10
11
class BankCard {
  private int money = 10000;

  public int getMoney() {
    return money;
  }

  public void setMoney(int money) {
    this.money = money;
  }
}

執行緒提供參數為提款卡的建構子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TestThread extends Thread {
  private BankCard card;

  public TestThread(BankCard card) {
    this.card = card;
  }

  @Override
  public void run() {
    while (true) {
      if (card.getMoney() <= 0) break;
      card.setMoney(card.getMoney() - 1000);
      System.out.println(Thread.currentThread().getName() + " 提領 1000 元,剩下 " + card.getMoney() + " 元");
    }
  }
}

啟動執行緒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
  public static void main(String[] args) {
    BankCard bankCard = new BankCard();
    // 放入相同的bankCard物件
    TestThread t1 = new TestThread(bankCard);
    t1.setName("執行緒1");

    // 放入相同的bankCard物件
    TestThread t2 = new TestThread(bankCard);
    t2.setName("執行緒2");

    // 啟動
    t1.start();
    t2.start();
  }
}
執行緒2 提領 1000 元,剩下 8000 元
執行緒2 提領 1000 元,剩下 7000 元
執行緒1 提領 1000 元,剩下 9000 元
執行緒1 提領 1000 元,剩下 5000 元
執行緒2 提領 1000 元,剩下 6000 元
執行緒1 提領 1000 元,剩下 4000 元
執行緒2 提領 1000 元,剩下 3000 元
執行緒1 提領 1000 元,剩下 2000 元
執行緒2 提領 1000 元,剩下 1000 元
執行緒1 提領 1000 元,剩下 0 元

因為沒有使用synchronized,所以執行結果不如預期,但二個執行緒都使用相同bankCard物件。

呼叫run()

先前已經提過,呼叫run(),等同於呼叫方法,不是啟動執行緒。

呼叫方法,會有阻斷Block,也就是前面的方法執行完,才能執行接下來的方法,按照順序執行。

1
2
3
4
5
6
7
8
class TestThread2 extends Thread {
  @Override
  public void run() {
    for (int i = 0; i < 5; i++) {
      System.out.println(Thread.currentThread().getName() + " i = " + i);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
  public static void main(String[] args) {
    TestThread2 t1 = new TestThread2();
    t1.setName("執行緒1");

    TestThread2 t2 = new TestThread2();
    t2.setName("執行緒2");

    t1.run();
    t2.run();
  }
}
main i = 0
main i = 1
main i = 2
main i = 3
main i = 4
main i = 0
main i = 1
main i = 2
main i = 3
main i = 4

由執行結果可以發現,雖然有設定執行緒的名字,但印出來的執行緒名字都是main,代表run()都是在main執行緒中執行。

而且印出的i是0至4,共印出2次,也就是呼叫2次run()。

results matching ""

    No results matching ""