Java多線程

1. 基本概念

1. 程序

​ 爲完成特定任務,用某種語言編寫的一組指令的集合。即指一段靜態的代碼

2. 進程

​ 正在運行的一個程序,是一個動態的過程

3. 線程

​ 是一個程序內部的一條執行路徑

4. 並行

​ 多個CPU同時執行多個任務

5. 併發

​ 一個CPU同時執行多個任務

2. 線程的創建和使用

1. 方式一:繼承 Thread 類

JDK中這樣描述:

There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread. This subclass should override the run method of class Thread. An instance of the subclass can then be allocated and started.

整理一下可分爲四步:

  1. 創建一個繼承於 Thread 類的子類
  2. 重寫 Thread 類的 run() 方法
  3. 創建 Thread 類的子類對象
  4. 通過此對象調用 start() 方法

舉個栗子:

// 1. 創建一個繼承於 Thread 類的子類
class MyThread extends Thread {

    // 2. 重寫 Thread 類的 run() 方法
    @Override
    public void run() {
        System.out.println("我是一個多線程");
    }
}
public class ThreadTest {

    public static void main(String[] args) {
        // 3. 創建 Thread 類的子類對象
        MyThread myThread = new MyThread();
        // 4. 通過此對象調用 start() 方法
        //    ①啓動當前線程
        //    ②調用當前線程的 run()方法
        myThread.start();
        
        
        // 或者使用匿名內部類的方式
        new Thread() {
            @Override
            public void run() {
                System.out.println("我也是一個多線程");
            }
        }.start();
    }
}

需要注意的點:

  1. 當執行 myThread.run()並沒有啓動線程,是直接調用了 myThread 類的 run() 方法
  2. 當需要再次啓動一個線程時,需要重新創建一個對象

2. 方式二:實現 Runnable 接口

JDK中是這樣描述的:

The other way to create a thread is to declare a class that implements the Runnable interface. That class then implements the run method. An instance of the class can then be allocated, passed as an argument when creating Thread, and started

整理一下可分爲五步:

  1. 創建實現了 Runnable 接口的類
  2. 實現類實現 Runnable 中的抽象方法:run()
  3. 創建實現類的對象
  4. 將此對象作爲參數傳遞到 Thread 類的構造器中,創建 Thread 類的對象
  5. 通過 Thread 類的對象調用 start() 方法

舉個栗子:

// 1. 創建實現了 Runnable 接口的類
class MyThread implements Runnable{

    // 2. 實現類實現 Runnable 中的抽象方法:run()
    @Override
    public void run() {
        System.out.println("我是一個多線程");
    }
}
public class ThreadTest {

    public static void main(String[] args) {

        //3. 創建實現類的對象
        MyThread myThread = new MyThread();

        //4. 將此對象作爲參數傳遞到 Thread 類的構造器中,創建 Thread 類的對象
        Thread thread = new Thread(myThread);

        // 5, 通過 Thread 類的對象調用 start() 方法
        thread.start();
    }
}

3. 創建線程的兩種方式的比較

  1. 優先選擇:實現 Runnable 接口的方式,原因如下:
    1. 實現的方式沒有類的單繼承的侷限性
    2. 實現的方式更適合處理多個線程有共享數據的情況
  2. 聯繫:Thread 類也實現了 Runnable 接口
  3. 相同點:都需要將線程要執行的邏輯重寫在 Run() 方法中

4. Thread 類中的常用方法

  1. start():

    Causes this thread to begin execution; the Java Virtual Machine calls the run method of this thread.

    啓動當前線程;調用當前線程的 run() 方法

  2. run():

    If this thread was constructed using a separate Runnable run object, then that Runnable object’s run method is called; otherwise, this method does nothing and returns.

    創建的線程要執行的操作聲明在此方法中

  3. yield():

    A hint to the scheduler that the current thread is willing to yield its current use of a processor.

    讓出當前線程的使用權

  4. join():

    Waits for this thread to die.

    在線程 a 中調用線程 b 的 join(),此時線程 a 進入阻塞狀態,直到 b 完全執行後,線程 a 才結束阻塞狀態(在哪個線程裏調用,哪個線程被阻塞)

  5. sleep(long millis):

    Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers.

    使當前線程休眠指定 millis 毫秒

  6. getName():獲取當前線程的名字

  7. setName(String name):設置當前線程的名字

5. 線程的調度

  1. Java 中線程的調度
  • 同優先級線程先到先服務,平均分配CPU執行時間
  • 高優先級的線程搶佔CPU
  1. 線程的優先級

    • MIN_PRIORITY:1
    • NORM_PRIORITY:5 -----> 默認優先級
    • MAX_PRIORITY:10
  2. 設置和獲取線程的優先級

    • getPriority():獲取線程的優先級
    • setPriority(int newPriority):設置線程的優先級

    說明:高優先級的線程要搶佔低優先級線程的CPU執行權,並不是意味着高優先級線程執行完後才執行低優先級線程,只是高優先級獲取CPU執行權的概率更高

3. 線程的生命週期

  • 新建:線程剛被創建,但是還未執行 start() 方法
  • 就緒:處於新建狀態的線程被start() 後,將進入線程隊列等待 CPU 分配時間片
  • 運行:當就緒狀態的線程被調度並獲得 CPU 資源時,進入運行狀態
  • 阻塞:在特殊情況下,線程被迫讓出CPU並臨時中止自己的執行,進入阻塞狀態
  • 死亡:線程完成全部工作或出現異常退出

4. 線程的同步

1. 問題的提出

  1. 舉個小栗子:

    三個窗口同時賣票,總票數爲 10 張,使用實現 Runnable 接口的方式

    
    class MyThread implements Runnable {
        private int ticket = 10;
        @Override
        public void run() {
            while (true) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "出售一張票,票號爲:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
    
    public class ThreadTest {
    
        public static void main(String[] args) {
            MyThread myThread = new MyThread();
    
            Thread thread1 = new Thread(myThread);
            Thread thread2 = new Thread(myThread);
    
            thread1.setName("窗口一");
            thread2.setName("窗口二");
    
            thread1.start();
            thread2.start();
        }
    }
    
    

    其中一次輸出結果爲:

    窗口二出售一張票,票號爲:10
    窗口一出售一張票,票號爲:10
    窗口一出售一張票,票號爲:8
    窗口二出售一張票,票號爲:7
    窗口二出售一張票,票號爲:6
    窗口一出售一張票,票號爲:5
    窗口二出售一張票,票號爲:4
    窗口一出售一張票,票號爲:4
    窗口一出售一張票,票號爲:2
    窗口二出售一張票,票號爲:1
    窗口一出售一張票,票號爲:0
    
  2. 輸出分析

    1. 賣票過程中,出現了重票問題,如多個窗口(線程)出售票號爲 10
    2. 賣票過程中,出現了錯票問題,如窗口(線程)一最後出售票號爲 0
  3. 問題分析

    1. 當線程 a 執行 run() 方法,遇到 sleep 進入堵塞狀態,線程 b 得到 CPU 的時間片進入 run() 方法,遇到 sleep 進入堵塞狀態,線程 a 率先從堵塞狀態回到就緒狀態,CPU 分配時間片給線程 a ,線程 a 進入運行狀態,當線程 a 執行 System.out.println 語句後還未執行 ticket-- 語句時,線程 b 也從堵塞狀態回到就緒狀態,CPU 分配時間片給線程 b,線程 b 進入運行狀態,也執行 System.out.println 語句,此時 ticket 的值併爲被更改,所以窗口一和二售出的票號都爲 10
    2. 當 ticket 爲 1 時,線程 a 進入 if 語句,執行 sleep 進入堵塞狀態,此時 ticket 的值依然爲 1,線程 b 得到CPU分配的時間片也進入 if 語句,所以窗口一售出票號爲 0
  4. 問題歸納

    多線程操縱共享數據數據時,可能造成共享數據的不確定性,即出現了多線程的安全問題

2. 解決辦法

  1. 解決思路

    當一個線程 a 操作共享數據時,其他線程不能參與進來。直到線程 a 操作完共享數據,其他線程才能開始操作共享數據。即使線程 a 進入阻塞狀態,只要未操作完共享數據,其他線程依然不能開始操作共享數據

    在 Java 中,通過同步機制,解決線程的安全問題

    1. 解決方法

      1. 同步代碼塊
      synchronized(同步監視器){
          // 需要被同步的代碼
          ...
      }
      

      說明:

      1. 操作共享數據的代碼,即未需要被同步的代碼
      2. 共享數據:多個線程共同操作的變量
      3. 同步監視器:俗稱:鎖。任何一個類的對象,都可以充當鎖(包括類對象)。要求:多個線程必須共用一把鎖
      4. 在實現 Runnable 接口創建多線程的方式中,可以考慮使用 this 充當同步監視器;在繼承 Thread 類創建多線程的方式中,考慮使用當前類(類名.class)充當同步監視器
      class MyThread implements Runnable {
          private int ticket = 10;
          @Override
          public void run() {
              while (true) {
                  synchronized (MyThread.class) {
                      if (ticket > 0) {
                          try {
                              Thread.sleep(50);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                          System.out.println(Thread.currentThread().getName() + "出售一張票,票號爲:" + ticket);
                          ticket--;
                      } else {
                          break;
                      }
                  }
              }
          }
      }
      
      public class ThreadTest {
          public static void main(String[] args) {
              MyThread myThread = new MyThread();
      
              Thread thread1 = new Thread(myThread);
              Thread thread2 = new Thread(myThread);
      
              thread1.setName("窗口一");
              thread2.setName("窗口二");
      
              thread1.start();
              thread2.start();
          }
      }
      
      1. 同步方法

      如果操作共享數據的代碼完整聲明在一個方法中,可以考慮將此方法聲明爲同步的,爲了鎖都爲同一個對象,可以同時將方法聲明爲靜態的

      說明:

      1. 同步方法仍然涉及同步監視器,只是不需要顯示聲明
      2. 非靜態方法的同步方法,同步監視器是:this;靜態方法的同步方法,同步監視器是:當前類本身
      class MyThread implements Runnable {
          private int ticket = 10;
      
          @Override
          public void run() {
              while (true) {
                  show();
              }
          }
      
          // 相當於用 synchronized(this) 把整個方法體包裹起來
          private synchronized void show() {
              if (ticket > 0) {
                  try {
                      Thread.sleep(50);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName() + "出售一張票,票號爲:" + ticket);
                  ticket--;
              }
          }
      }
      
      public class ThreadTest {
          public static void main(String[] args) {
              MyThread myThread = new MyThread();
      
              Thread thread1 = new Thread(myThread);
              Thread thread2 = new Thread(myThread);
      
              thread1.setName("窗口一");
              thread2.setName("窗口二");
      
              thread1.start();
              thread2.start();
          }
      }
      
    2. lock 鎖

      與 synchronized 的區別?

      1. synchronized 機制在執行完相應的同步代碼之後,自動的釋放同步監視器
      2. lock 需要手動啓動同步(lock()),結束同步也需要手動實現(unlock())
      import java.util.concurrent.locks.ReentrantLock;
      
      class MyThread implements Runnable {
          private int ticket = 10;
      
          //1. 實例化ReentrantLock
          private ReentrantLock lock = new ReentrantLock();
      
          @Override
          public void run() {
              while (true) {
                  try {
                      // 2. 調用鎖定方法 lock()
                      lock.lock();
                      if (ticket > 0) {
                          try {
                              Thread.sleep(50);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                          System.out.println(Thread.currentThread().getName() + "出售一張票,票號爲:" + ticket);
                          ticket--;
                      } else {
                          break;
                      }
                  } finally {
                      // 3. 調用解鎖方法 unlock()
                      lock.unlock();
                  }
              }
          }
      }
      
      public class ThreadTest {
          public static void main(String[] args) {
              MyThread myThread = new MyThread();
      
              Thread thread1 = new Thread(myThread);
              Thread thread2 = new Thread(myThread);
      
              thread1.setName("窗口一");
              thread2.setName("窗口二");
      
              thread1.start();
              thread2.start();
          }
      }
      

3. 優缺點分析

  1. 好處:

    解決了線程安全問題

  2. 缺點:

    操作同步代碼時,只有一個線程參與,效率低

5. 線程的通信

1. 常用方法:

  1. wait():

    Causes the current thread to wait until it is awakened, typically by being notified or interrupted.

    當前線程進入阻塞狀態,並釋放同步監視器

  2. notify():

    Wakes up a single thread that is waiting on this object’s monitor.

    喚醒被 wait 的線程,如果有多個線程,優先喚醒優先級高的線程

  3. notifyAll():

    Wakes up all threads that are waiting on this object’s monitor.

    喚醒所有被 wait 的線程

說明:

  1. 三個方法必須使用在同步代碼塊或同步方法中
  2. 三個方法的調用者必須是同步代碼塊或同步方法中的同步監視器
  3. 三個方法都是定義在 java.lang.Object 類中

2. 實例

兩個線程輪流打印 1 - 100

class MyThread implements Runnable {
    private int number = 0;
    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (number < 100) {
                    // 喚醒被 wait()阻塞的一個線程
                    notify();
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;
                    try {
                        // 使得調用wait()方法的線程進入阻塞狀態
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();

        Thread thread1 = new Thread(myThread);
        Thread thread2 = new Thread(myThread);

        thread1.setName("線程1");
        thread2.setName("線程2");

        thread1.start();
        thread2.start();
    }
}

3. sleep() 和 wait() 方法的異同

相同點:一旦執行方法,都可以使得當前線程進入阻塞狀態

不同點:

  1. 兩個方法聲明的位置不同:Thread 類中聲明 sleep(),Object 類中聲明 wait()
  2. 調用的要求不同:sleep() 可以在任何場景下調用,wait() 只能在同步代碼塊或同步方法中調用
  3. 關於釋放同步監視器:如果兩個方法都是在同步代碼塊或同步方法中調用,sleep() 不會釋放鎖,wait() 會釋放鎖

6. JDK1.5 新增創建多線程的方法

1. 實現 Callable 接口

JDK 中是這樣描述的:

The Callable interface is similar to Runnable, in that both are designed for classes whose instances are potentially executed by another thread.

具體說來步驟爲:

  1. 創建一個實現 Callable 接口的實現類
  2. 實現類實現 call() 方法,將此線程需要執行的操作聲明在其中
  3. 創建實現 Callable 接口的實現類對象
  4. 將此 Callable 接口實現類對象作爲參數傳遞到 FutureTask 構造器中,創建 FutureTask 的對象
  5. 將 FutureTask 的對象作爲參數傳遞到 Thread 類的構造器中,創建 Thread 對象,並調用 start() 啓動線程
  6. FutureTask 對象調用 get() 方法獲取 Callable 實現類中 Call() 的返回值

舉個栗子:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

//1. 創建一個實現 Callable 接口的實現類
class numThread implements Callable<Integer> {
    //2. 實現類實現 Call() 方法,將此線程需要執行的操作聲明在其中
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

public class ThreadTest {
    public static void main(String[] args) throws Exception {
        //3. 創建實現 Callable 接口的實現類對象
        numThread numThread = new numThread();
        //4. 將此 Callable 接口實現類對象作爲參數傳遞到 FutureTask 構造器中,創建 FutureTask 的對象
        FutureTask<Integer> futureTask = new FutureTask<>(numThread);
        //5. 將 FutureTask 的對象作爲參數傳遞到 Thread 類的構造器中,創建 Thread 對象,並調用 start() 啓動線程
        new Thread(futureTask).start();
        //6. FutureTask 對象調用 get() 方法獲取 Callable 實現類中 Call() 的返回值
        Integer o = futureTask.get();
        System.out.println(o);
    }
}

Callable 接口與 Runnable 接口的比較:

  1. call() 可以有返回值
  2. call() 可以拋出異常
  3. Callable 支持泛型

2. 線程池

import java.util.concurrent.*;

class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println("我是Runnable接口");
    }
}

class MyThread2 implements Callable {

    @Override
    public Object call() throws Exception {
        System.out.println("我是Callable接口");
        return null;
    }
}

public class ThreadTest {

    public static void main(String[] args) {
        // 1. 提供指定線程數的線程池
        ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);

        // 2. 執行指定線程的操作
        // 適用於Runnable
        executorService.execute(new MyThread());
        // 適用於Callable
        executorService.submit(new MyThread2());

        // 3. 關閉連接池
        executorService.shutdown();
    }
}

使用線程池的好處:

  1. 提高響應速度(減少了創建新線程的時間)
  2. 降低資源消耗(重複利用線程池中的線程)
  3. 便於線程管理
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章