1. 程式人生 > 實用技巧 >Java 多執行緒入門

Java 多執行緒入門

基本概念

  • 程式 :是指為完成特定任務 ,用某種程式語言編寫的一組指令的集合 ,一段靜態程式碼 。
  • 程序 :是指程式的一次執行過程 ,或正在執行的一個程式 。出生 -> 存在 -> 消亡 》生命週期
  • 執行緒 :程式內部的執行路徑 。

例如 :360 木馬查殺 、磁碟清理 、修復 。可以同時進行 支援多 執行緒 。

每個執行緒用於獨立的 虛擬機器棧 和 程式計數器 (pc)

一個 Java應用程式 java.exe 至少有三個執行緒 ,主執行緒 gc 垃圾回收執行緒,異常處理執行緒

並行與併發

  • 並行 :多個 CPU 同時執行多個任務 。例如 :多個人同時做不同的事。
  • 併發 :一個 CPU(採用時間片) 同時執行多個任務 。例如 :秒殺 ,多個人做同一件事 。

多執行緒優點等

1、提高應用程式的相應 。對圖形化介面更有意義 ,可增強使用者體驗 。

2、提高計算機系統 CPU 的利用率

3、改善程式結構 ,將既長又複雜的程序分為多個執行緒 ,獨立執行 ,利於理解和修改 。

何時需要多執行緒

  • 程式需要同時執行兩個或多個任務
  • 程式需要實現一些需要等待的任務時,如使用者輸入 、檔案讀寫 操作 、網路操作 、搜尋等 。
  • 需要一些後臺執行的程式時

建立多執行緒方式一

多執行緒的建立 ,方式一 :繼承於Thread 類

1、建立一個繼承於 Thread類 的子類

MyThread extends Thread

2、重寫 Thread 類的 run() 方法 , 將此執行緒執行的操作宣告在 run() 方法中 。

@Override
public void run() {

3、建立 Thread類的子類的物件 例項

MyThread thread = new MyThread();

4、通過此物件呼叫 start() 方法

​ 1、啟動當前執行緒 ,2、呼叫當前執行緒的 run() 方法

thread.start();

執行緒的常用方法

方法名稱 作用
void start() 啟動執行緒 ,並執行物件的 run() 方法
run() 執行緒在被排程時執行的操作
String getName() 返回執行緒的名稱
void setName(String name) 設定執行緒名稱
static Thread currentThread() 返回當前執行緒 。
static void yield() 執行緒讓步
暫停當前正在執行的執行緒 ,把執行機會讓給優先順序更高(或相同)的執行緒
join() 當某個程式執行流中呼叫其他執行緒的 join() 方式時,呼叫執行緒將被阻塞,直到 join() 方法加入的join執行緒執行完為止 。
線上程a中呼叫 執行緒b的join(),此時執行緒b進入阻塞狀態 ,直到 執行緒b完全執行完以後 ,執行緒a才結束阻塞狀態
static void sleep(long millis) 等待時間 (指定時間 :毫秒)令當前活動執行緒在指定時間段內放棄對 CPU 的控制 ,時間到後重新排隊 。
boolean isAlive() 返回 boolean ,判斷執行緒是否存活
stop() 強制執行緒生命週期結束 (不推薦使用 )

執行緒優先順序的設定

  • 執行緒的優先順序等級

    • MAX_PRIORITY = 10

    • MIN_PRIORITY = 1

    • NORM_PRIORITY = 5

  • 涉及的方法

    • getPriority() :返回執行緒優先值
    • setPriority(int newPriority) :改變執行緒的優先順序
  • 注意事項 :

    • 執行緒建立時 ,繼承父執行緒的優先順序 。
    • 低優先順序只是獲得排程的概率低 ,並非一定 高優先順序之後才被呼叫 。

建立多執行緒方式二

建立多執行緒的方式二 :實現 Runnable 介面

  • 1、建立一個實現了 Runnable 介面的類
  • 2、實現類去 重寫 Runnable 中的抽象方法 :run()
  • 3、建立實現類的物件 (例項化)
  • 4、將此物件引數傳遞到 Thread 類的構造器中 ,建立 Thread 類的物件
  • 5、通過 Thread 類的物件呼叫 start()

兩種方式的比較 :

1、實現Runnable 沒有類的單繼承的侷限性

2、實現 Runnable 更適合來處理多個執行緒有共享資料的情況 。

public class Thread implements Runnable {		// Thread類  實際上也是 實現 Runnable 介面 

執行緒的生命週期

  • JDK 中 用 Thread.State 類定義了執行緒的幾種狀態 。

完整的生命週期 經歷五種狀態

1、新建狀態(NEW):執行緒被建立後 ,就進入了新建狀態 。 比如 : Thread t1 = new Thread();

2、就緒狀態(Runnable):也被稱為 “可執行狀態” 。執行緒被建立後 ,其他執行緒呼叫 start() 方法來啟動執行緒 。

​ 例如 : t1.start() 處於就緒狀態的執行緒 ,隨時可能被CPU排程執行 。

3、執行狀態(Running):執行緒獲取CPU許可權進行執行任務 。但需要注意 :執行緒只能從就緒狀態進入到執行狀態 。

4 、阻塞狀態(Blocked):阻塞狀態是因為執行緒因為某種原因放棄CPU使用權 ,暫時停止執行 。直到執行緒進入就緒狀態,才有機會轉到執行狀態 。阻塞的情況分為三種 :

  • 1、等待阻塞 :通過呼叫執行緒的 wait() 方法 ,讓執行緒等待某工作的完成 。
  • 2、同步阻塞 :執行緒在獲取 synchronized 同步鎖失敗(因為鎖被其他執行緒所佔用) ,它會進入到同步阻塞狀態 。
  • 3、其他阻塞 :通過呼叫執行緒的 sleep()join() 或發出了 I/O請求時 ,執行緒會進入到阻塞狀態 。當 sleep() 狀態超時、join() 等待執行緒終止或者超時、或者 I/O 處理完畢時 ,執行緒重新轉入就緒狀態 。

5、死亡狀態(Dead):執行緒執行完了或者因異常退出了 run() 方法 ,該執行緒結束生命週期 。

理解執行緒的安全問題

執行緒的同步

方式一 :同步程式碼塊

synchronized (同步監視器) {
    // 需要被同步的程式碼
}

說明

​ 1、操作共享資料的程式碼 ,即視為需要被同步的程式碼

​ 2、共享資料 :多個執行緒共同操作的變數 。例如 > ticket(車票) 就是共享資料

​ 3、同步監視器 ,俗稱 鎖 ,任何一個類的物件都可以充當鎖 。

​ 要求 :多個執行緒必須共有同一把鎖 。

補充 :

​ 1、實現 Runnable ,同步監視器 可以使用 this 充當鎖 ,代表當前呼叫run方法的物件 。

​ 2、繼承 Thread ,同步監視器 可以使用 當前類 充當 鎖 ,類 也是物件 當前類.class

方式二 :同步方法

public synchronized void show() {
    // 需要被同步的程式碼
}

同步方法總結 :

​ 1、同步方法依然涉及到同步監視器 ,但是不需要我們顯式的宣告

​ 2、非靜態的同步方法 ,同步監視器為 this

​ 靜態的同步方法 ,同步監視器 為 當前類本身

實現 Runnable ,同步方法不需要使用 static 關鍵字

繼承 Thread ,同步方法 需要使用 static 關鍵字

死鎖的問題

Lock鎖方式解決執行緒安全問題

JDK 5.0新增

  • Lock 鎖 是介面 ,是控制多個執行緒對共享資源進行訪問的工具 。

  • ReentrantLock 類實現了 Lock ,可以顯式加鎖、釋放鎖 。【常用】與 synchronized相同的併發性 。

  • 1、例項化 ReentrantLock

Lock lock = new ReentrantLock();
  • 2、呼叫鎖的反法 lock()
try{
    lock.lock()
	// 需要被同步的程式碼
}
  • 3、呼叫解鎖方法 :unlock()
finally {
    lock.unlock();
}

面試題 :

1、synchronized 與 lock 的異同 ?

​ · synchronized 自動釋放鎖 ,程式碼塊鎖 和方法鎖

​ · lock 手動釋放鎖 ,只有程式碼塊鎖 ,

​ · 使用順序:

​ Lock --》同步程式碼塊 --》 同步方法 。

同步機制練習

class Account{
    private double balance;
    public Account(double balance) {
        this.balance = balance;
    }
    public synchronized void deposit(double amt) {
        if (amt > 0) {
            balance += amt;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "存錢成功:賬戶餘額:" + balance);
        }
    }
}

class MyThreadRunnable implements Runnable{
    private Account account;
    public MyThreadRunnable(Account account) {
        this.account = account;
    }
    @Override
    public void run() {
             for (int i = 0; i < 3; i++) {
                 account.deposit(1000);
        }
    }
}
public class ThreadRunnableTest {
    public static void main(String[] args) {
        Account account = new Account(0);
        MyThreadRunnable runnable = new MyThreadRunnable(account);
        Thread thread01 = new Thread(runnable);
        Thread thread02 = new Thread(runnable);
        thread01.setName("甲");
        thread02.setName("乙");
        thread01.start();
        thread02.start();
    }
}

執行緒的通訊

涉及執行緒通訊的 方法 ,只能夠在 同步程式碼塊 或者 同步方法中 使用 。

方法名稱 作用
wait() 一旦執行此方法,當前執行緒就進入阻塞狀態 ,並釋放同步監視器
notify() 一旦執行此方法 ,就會喚醒被 wait 的一個執行緒 。如果有多個執行緒被 wait ,就喚醒優先順序高的那個執行緒
notifyAll() 一旦執行此方法 ,就會喚醒所有被 wait 的執行緒

上述方法必須使用在 同步程式碼塊 或 同步方法中 。

上訴方法的呼叫者 必須是同步程式碼程式碼塊或同步方法中的同步監視器,否則會出現異常 IllegalMonitorStateException

都定義在 Object 類 中 。

面試題 :sleep() 和 wait() 的異同 ?

1、宣告位置不同 。Thread類中 宣告 sleep() ,Object類中宣告 wait()

2、呼叫要求不同 。sleep() 可以在任何需要出現的地方呼叫 。wait() 只能在同步程式碼塊或者同步方法 中使用 。

3、是否釋放同步鎖 。如果都在 同步程式碼塊或同步方法中執行 ,sleep 不會釋放鎖 ,wait 會釋放鎖

生產者消費者 例題

Resource.java 【工廠】

public class Resource {
    int c = 0;
    public synchronized void t1() {
        if (c < 20) {
            c++;
            System.out.println("開始生產第" + c + "個商品");
        } else {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notify();
    }

    public synchronized void put() {
        if (c > 0) {
            System.out.println("開始消費第" + c + "個商品");
            c--;
        } else {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notify();
    }
}

producerExample.java 【生產者】

public class producerExample implements Runnable{
    private Resource resource;

    public producerExample(Resource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        while (true) {
            resource.t1();
        }
    }
}

consumerExample.java 【消費者】

public class consumerExample implements Runnable{
    private Resource resource;
    public consumerExample(Resource resource) {
        this.resource = resource;
    }
    @Override
    public void run() {
        while (true) {
            resource.put();
        }
    }
}

Cashier.java 【超市 ,收銀員】

public class Cashier {
    private int number;
    private String merchandise;

    public void setMerchandise(String merchandise) {
        this.merchandise = merchandise;
    }

    public String getMerchandise() {
        return merchandise;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }
}

測試類

public class Test01 {
    public static void main(String[] args) {
        Resource resource = new Resource();
        producerExample producerExample = new producerExample(resource);
        consumerExample consumerExample = new consumerExample(resource);
        new Thread(producerExample).start();
        new Thread(consumerExample).start();
    }
}

建立多執行緒方式三 :實現 Callable

JDK 5.0 新增

  • 1、建立一個實現 Callable 的實現類
class MyCallableTest implements Callable {
  • 2、Callable 類的 call() 方法 ,將此執行緒需要執行的操作宣告在 call() 中 。
@Override
public Object call() throws Exception {
  • 3、建立 Callable 介面實現類的物件
MyCallableTest myCallableTest = new MyCallableTest();
  • 4、建立 FutureTask 物件 ,將 Callable 介面的實現類物件作為引數傳遞到 FutureTask 構造器中
FutureTask task = new FutureTask(myCallableTest);
  • 5、建立 Thread物件 ,將 FutureTask 的物件作為引數傳遞到 Thread 類的構造器中 ,並呼叫 start() 。
new Thread(task).start();
  • 6、如果需要獲取返回值 ,使用 FutureTask 物件 呼叫 get() 方法 。【可選 】
Object o = task.get();

如何理解 Callable 與 Runnable 的區別 ?

1、call() 可以有返回值

2、call() 可以丟擲異常 ,可以被捕獲異常 ,獲取異常資訊 。

3、Callable 支援泛型 。

使用執行緒池的好處

使用執行緒池

  • 背景 :經常建立和銷燬 ,使用量特別大的資源 。比如併發情況下的執行緒 ,對效能影響很大 。

  • 思路 :提前建立號多個執行緒 ,放入執行緒池中 ,使用時直接獲取 ,用完放回池中 。可以避免頻繁地建立和銷燬 ,實現重複利用 。

  • 好處 :

    • 提高響應速度 (減少建立執行緒的時間)
    • 降低資源消耗 (重複利用執行緒 )
    • 便於執行緒管理
      • CorePoolSize :核心池的大小
      • MaximumPoolSize :最大執行緒數
      • KeepAliveTim :執行緒沒有任務時 最多保持多長時間後會終止 。
  • JDK 5.0 起提供了 執行緒池相關的API :ExecutorServiceExecutors

  • ExecutorService :執行緒池介面 ,常見子類 ThreadPoolExecutor

    •   <T> Future<T> submit(Callable<T> task);// 執行任務/命令	有返回值 。 適用於 實現Callable介面
      
    •   void execute(Runnable command);	// 執行任務 ,無返回值 適用於 實現Runnable介面
      
    •   void shutdown();		// 關閉連線池
      
  • Executors :工具類 、執行緒池的工廠類 。用於建立並返回不同型別的執行緒池 。

    •   Executors.newFixedThreadPool(int nThreads)	//建立一個可重用固定執行緒數的執行緒池 
      
    •   Executors.newCachedThreadPool()	// 建立一個可根據需要建立新執行緒的執行緒池
      
    •   Executors.newSingleThreadExecutor()		// 建立一個只有一個執行緒的執行緒池
      
    •   Executors.newScheduledThreadPool(int corePoolSize)	// 建立一個執行緒池 ,它可安排在給定延遲後執行命令或者定期地執行 
      

使用 :

  • 1、提供指定執行緒數量的執行緒池
ExecutorService service = Executors.newFixedThreadPool(10);
  • 2、執行指定的執行緒的操作 ,需要提供實現 Runnable介面 或Callable 介面的實現類的物件
service.execute(myCallableTest); // 適合 實現Runnable介面
service.submit(myCallableTest);	// 適合 實現Callable介面
  • 關閉連線池
service.shutdown();

寫一個執行緒安全的懶漢式 【使用同步機制 修改為執行緒安全 。】餓漢式

class Bank{
    private Bank(){}
    private static Bank instance = null;
    private static Bank getInstance() {
        if (instance == null) {
            synchronized (Bank.class) {
                if (instance == null) {
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

小結釋放鎖的操作

小結不釋放鎖的操作

入門完結,相信你對多執行緒有了一個概念