一. 執行緒管理之Thread基礎

jasonhww發表於2019-01-06

不忘初心 砥礪前行, Tomorrow Is Another Day !

相關文章

本文概要:

  1. 程式與執行緒
  2. 執行緒的生命週期
  3. 執行緒易混淆的函式
  4. 鎖機制
  5. 執行緒同步的四種方式
  6. 對 volatile 關鍵字的理解

一. 程式和執行緒

1.1 基本概念

  • 程式

概念:程式是程式的實體,是受作業系統管理的基本執行單元.

  • 執行緒

概念:作業系統中最小排程單元,一個程式可以擁有多個執行緒.

1.2 執行緒的生命週期

  • new : 新建狀態,當new Thread例項化時.
  • Runnable : 可執行狀態,當呼叫start方法時.
  • Running : 執行狀態,執行緒被cpu執行,呼叫了run方法時.
  • Blocked 阻塞狀態,當呼叫join()、sleep()、wait()時.分三種阻塞情況
    • wait : 等待狀態,當呼叫wait方法時,此時需要呼叫它的notify方法去喚醒它,才會重回可執行狀態.
    • timeWait : 超時等待狀態,當呼叫t.join(long)、Thread.sleep(long),obj.wait(long)時,超過指定時間,都會自動返回可執行狀態.
    • lock: 同步狀態,獲取物件的同步鎖,若該同步鎖被別的執行緒佔用時.
  • Dead : 銷燬狀態,執行緒執行完畢或者發生異常時.

1.3 執行緒易混淆的函式

  • Thread.sleep()/sleep(long millis),當前執行緒進阻塞狀態,不會釋放鎖
  • t.join()/join(long millis),在當前執行緒裡呼叫其它執行緒的join方法,當前執行緒進阻,不放鎖.
    • (相當於其他執行緒插隊進來,需要等待其他執行緒執行完畢才可往下執行),
  • obj.wait()/wait(long timeout),當前執行緒進阻,放鎖.需要依靠notify()/notifyAll()喚醒或者等待時間到自動喚醒
  • obj.notify()/obj.nofiyAll喚醒在此物件監視器上阻塞的任意某個執行緒/所有執行緒.
  • Thread.yield(),當前執行緒不進阻,不放鎖.而是重置為可執行狀態.
  • interrupt(),中斷執行緒.

二. 執行緒的建立

執行緒的建立有3種方式.

2.1 繼承Thread,重寫run方法.

這種方式的本質也是實現Runnable介面.當我們呼叫start方法時,並不會立即執行執行緒裡面程式碼,而只是將執行緒狀態變為可執行狀態,具體的執行時機由作業系統決定.

public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("直接繼承Thread,重寫run方法");
    }

    public static void main(String[] args) {
        new MyThread().start();
    }
}
複製程式碼

2.2 實現Runnable介面,重寫run方法.

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("實現Runnable介面,重寫run方法");
    }

    public static void main(String[] args) {
        new Thread(new MyRunnable()).start();
    }
}
複製程式碼

2.3 實現Callable介面,重寫Call方法.

相比Runnable的三大功能.

  • 可以丟擲異常
  • 提供返回值
  • 通過Future非同步任務統計,可以對目標執行緒Call方法監視執行情況,獲取執行完畢時的返回值結果.

關於ExecutorService與Future相關知識,將線上程池一篇文中詳細講解.這裡只需要知道Callable一般是和ExecutorService配合來使用的.

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("子執行緒正在幹活");
        Thread.sleep(3000);
        return "實現Callable介面,重寫Call方法";
    }

    public static void main(String[] args) throws Exception {
        MyCallable myCallable = new MyCallable();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> future = executorService.submit(myCallable);
        executorService.shutdown();

        Thread.sleep(1000);//模擬正在幹活
        System.out.println("主執行緒正在幹活");
        //阻塞當前執行緒,等待返回結果.
        System.out.println("等待返回結果:" + future.get());
        System.out.println("主執行緒所有的活都幹完了");

    }
}

//呼叫輸出
子執行緒正在幹活
主執行緒正在幹活
等待返回結果:實現Callable介面,重寫Call方法
主執行緒所有的活都幹完了
複製程式碼

三. 執行緒的同步

執行緒同步的目的就是為了防止當多個執行緒對同一個資料物件進行儲存時,造成資料不一致的問題.

同步問題示例

public class SyncThread extends Thread {
    private static final String TAG = "SyncThread";
    private Pay pay;

    public SyncThread(String name, Pay pay) {
        super(name);
        this.pay = pay;

    }
    
    @Override
    public void run() {
        while (isRunning) {
            pay.count();
        }
    }
}

Pay.java
    /**
     * 未同步時
     */
    public void count() {
        if (count > 0) {
            System.out.println(Thread.currentThread().getName() + ":>" + count--);
        } else {
            isRunning = false;
        }
    }
複製程式碼
//未使用同步時
D: 執行緒3:>912
D: 執行緒1:>911
D: 執行緒2:>911 //此時已經出現資料不一樣
D: 執行緒1:>909
D: 執行緒3:>910
D: 執行緒2:>908
D: 執行緒1:>907
D: 執行緒3:>906
D: 執行緒2:>905
D: 執行緒1:>904
D: 執行緒2:>903
D: 執行緒3:>903
D: 執行緒1:>902
D: 執行緒2:>901
D: 執行緒3:>900

//使用同步時
D: 執行緒1:>1000
D: 執行緒2:>999
D: 執行緒2:>998
D: 執行緒1:>997
D: 執行緒2:>996
D: 執行緒1:>995
D: 執行緒2:>994
D: 執行緒1:>993
D: 執行緒2:>992
D: 執行緒1:>991
D: 執行緒2:>990
D: 執行緒1:>989
D: 執行緒2:>988
D: 執行緒1:>987
D: 執行緒2:>986
D: 執行緒1:>985
D: 執行緒2:>984
D: 執行緒1:>983
複製程式碼

3.1 鎖機制

為了解決非同步的問題,JAVA提供了鎖的機制,synchronized 關鍵字.可以很方便的實現執行緒的同步.

先理解如何進行手動加鎖,這樣更容易理解自動加鎖的機制.

3.1.1 認識重入鎖與條件物件
  1. 重入鎖:對資源進行手動枷鎖.
  • ReentrantLock() : 建立一個ReentrantLock例項
  • lock() : 獲得鎖
  • unlock() : 釋放鎖
  1. 條件物件: 使執行緒滿足某一條件後才能執行,不滿足則進阻,放鎖.用於管理已獲得鎖但是暫時沒作用的執行緒.
  • lock.newCondition : 獲得一個條件物件.
  • condition.await() :進阻,放鎖.相當於obj.wait方法.
  • condition.signal/signalAll : 喚醒在此條件上阻塞的任意某個執行緒/所有執行緒.相當於obj.notify/notifyAll

虛擬碼

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition;
lock.lock();
        try {
            if(count == 0){
                //進阻,放鎖
                condition.await();
            }
            //喚醒因此條件,而阻塞的所有執行緒
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
           lock.unlock(); 
        }

複製程式碼
3.1.2 synchronized 關鍵字

資源互斥,執行緒資料同步,提供了自動加鎖.同步本質見java記憶體模型.

  • 物件鎖: 每一個物件有一個內部鎖,並且只有一個內部條件.
    • 對應synchronized關鍵字,如果是多個執行緒訪問同個物件的sychronized塊,才是同步的,但是訪問不同物件的話就是不同步的。
  • 類鎖: 是一個全域性鎖
    • 對應static sychronized關鍵字,無論是多執行緒訪問單個物件還是多個物件的sychronized塊,都是同步的

在實際開發中大多數情況使用同步方法與同步程式碼塊,實現同步,除非一些需要高度控制鎖的則使用重入鎖和條件物件.

3.2 實現同步的方式

執行緒同步的四種方式.

1. 同步方法

private synchronized void countSyncMethod() {
        if (count > 0) {
            Log.d(TAG, Thread.currentThread().getName() + ":>" + count--);
        } else {
            isRunning = false;
        }
    }
複製程式碼

2. 同步程式碼塊

private void countSyncCode() {
        synchronized (this) {
            if (count > 0) {
                Log.d(TAG, Thread.currentThread().getName() + ":>" + count--);
            } else {
                isRunning = false;
            }
        }
    }
複製程式碼

3. 使用重入鎖


private void countSyncLock() {
        mLock.lock();
        try {
            if (count > 0) {
                Log.d(TAG, Thread.currentThread().getName() + ":>" + count--);
            } else {
                isRunning = false;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }inally{
           mLock.unlock(); 
        }

    }
複製程式碼

4. 使用特殊域變數(volatile)

3.3 volatile 關鍵字

先了解java記憶體模型與“三性”知識.

3.3.1 Java記憶體模型

java記憶體模型定義了執行緒和主記憶體之間的抽象關係.

  • 所有執行緒的共享變數在主記憶體中.
  • 每個執行緒都有一個本地記憶體.
一. 執行緒管理之Thread基礎
java記憶體模型基礎圖

執行緒在對變數進行存與取時,一般先改變工作記憶體的變數值,再在某個時機重新整理到主存中去.這樣就會導致多執行緒併發時另一個執行緒從主存中獲取到的不一定是最新的值.

3.3.2 Java併發程式設計的原子性、可見性、有序性
  • 原子性:對於基本資料型別只是簡單的讀取和賦值(將數字賦值給某個變數),僅有一個操作就是原子性操作,操作是不可中斷的.
  • 可見性:一個執行緒的修改對另外一個執行緒是立即可見的.
    • 即volatile修飾的變數,如果在一個執行緒修改值,則會立即更新到主存中去,那麼另一個執行緒會獲取到最新的值.
  • 有序性:編譯時和執行時會對指令進行重新排序,會影響多執行緒併發執行的正確性.

這樣當一個共享變數被volatile修飾時.

  1. 保證可見性,即一個執行緒對變數值進行了修改,另一個執行緒可以立即獲取到最新修改後的值.
  2. 保證有序性,即禁止指令重排序.
  3. 不保證原子性.
    • 所以不適用於修飾如自增自減等一些依賴於自身或者其他變數值的變數時.
private int x;
private int y;

//依賴自身
x++;
//依賴其他變數
if(x > y){
    x = y;
}
複製程式碼

四. 執行緒的中斷

  • 關鍵字: interrupted
    中斷目標執行緒,並不是指立即停止執行緒,而僅僅只是將執行緒的標識為true,一般由目標執行緒自己去檢測並決定是否終止執行緒。

示例程式碼

@Override
    public  void run() {
        //中斷目標執行緒,並不是指立即停止執行緒,而僅僅只是將執行緒的標識為true,一般由目標執行緒自己去檢測並決定是否終止執行緒.
        for (int j = 0; j <100000000 ; j++) {
            if (Thread.currentThread().isInterrupted()){
                System.out.println("Interrupted!已經中斷停止輸出.開始收尾工作1");
                return;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                //處於阻塞狀態的執行緒,也會立馬被中斷,所以就會丟擲此異常.
                System.out.println("Interrupted!已經中斷停止輸出.開始收尾工作2");
                return;
            }
            System.out.println("還沒中斷繼續輸出j:" + j);
        }
    }

複製程式碼

關於Thread基礎相關就介紹到這裡了.接著下一篇介紹多執行緒程式設計中的執行緒池.Demo原始碼在最後一篇一起給出.

由於本人技術有限,如有錯誤的地方,麻煩大家給我提出來,本人不勝感激,大家一起學習進步.

參考連結:

相關文章