1. 程式人生 > >Android併發程式設計 開篇

Android併發程式設計 開篇

該文章是一個系列文章,是本人在Android開發的漫漫長途上的一點感想和記錄,我會盡量按照先易後難的順序進行編寫該系列。該系列引用了《Android開發藝術探索》以及《深入理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相關知識,另外也借鑑了其他的優質部落格,在此向各位大神表示感謝,膜拜!!!

前言

從本篇博文開始Android併發程式設計系列。由於筆者水平有限,如果博文之中有任何錯誤或者紕漏之處,還請不吝賜教。

Java執行緒

在Android SDK中並沒有提供新穎的執行緒實現方案,使用的依舊是JDK中的執行緒。在Java中開啟新執行緒有3中常見的方式

  1. 繼承自Thread類,重寫run()方法
public class ThreadA extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(getName());
    }
}
//測試的主執行緒
public class Main {
    public static void main(String[] args){
        ThreadA threadA = new ThreadA();
        threadA.setName("threadA");
        threadA.start();
        System.out.println("主執行緒"+Thread.currentThread().getName());
    }
}
  1. 實現Runnable介面,實現run()方法
public class ThreadB implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

//測試的主執行緒
public class Main {
    public static void main(String[] args){
        ThreadB threadB = new ThreadB();
        //注意這裡啟動的方式跟方式1不一樣
        Thread thread = new Thread(threadB);
        thread.setName("threadB");
        thread.start();
        System.out.println("主執行緒"+Thread.currentThread().getName());
    }
}
  1. 實現Callable介面,實現call()方法
public class ThreadC implements Callable<String> {
    @Override
    public String call() throws Exception {
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return Thread.currentThread().getName();
    }
}

public class Main {
    public static void main(String[] args){
        ThreadC threadC = new ThreadC();
        //FutureTask 後續會講到,先知道有怎麼個實現方式
        FutureTask<String> feature = new FutureTask<>(threadC);
        //注意啟動方式有點不一樣;
        Thread thread1 = new Thread(feature);
        thread1.setName("threadC");
        thread1.start();
        //注意細細體會這個,只有主執行緒get了,主執行緒才會繼續往下面執行
        try {
            System.out.println(feature.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        System.out.println("主執行緒"+Thread.currentThread().getName());
    }
}

JMM(Java 記憶體模型)

上面簡單的介紹了3種開啟執行緒的方式,接下來我們來看一下Java的記憶體模型,因為後續文章講到的許多知識都需要這個作為基礎。

主記憶體與工作記憶體

JMM規定JVM有主記憶體(Main Memory)和工作記憶體(Working Memory),主記憶體其實就是我們平常所說的Java堆記憶體,存放所有類例項變數等,這部分記憶體是多個執行緒共享的;工作記憶體裡存放的則是執行緒從主記憶體拷貝過來的變數以及訪問方法得到的臨時變數,這部分記憶體為執行緒私有,其他的執行緒不能訪問。

注:上面所說的拷貝並不是拷貝整個物件例項到工作記憶體,虛擬機器可能拷貝物件引用或者物件欄位,而不是整個物件。

主記憶體與工作記憶體的關係如下圖所示

主記憶體與工作記憶體間的互動操作

主記憶體與工作記憶體之間具體的互動協議,被定義了以下8種操作來完成,虛擬機器實現時必須保證每一種操作都是原子的、不可再分的。

  1. lock,鎖定,所用於主記憶體變數,它把一個變數標識為一條執行緒獨佔的狀態。
  2. unlock,解鎖,解鎖後的變數才能被其他執行緒鎖定。
  3. read,讀取,所用於主記憶體變數,它把一個主記憶體變數的值,讀取到工作記憶體中。
  4. load,載入,所用於工作記憶體變數,它把read讀取的值,放到工作記憶體的變數副本中。
  5. use,使用,作用於工作記憶體變數,它把工作記憶體變數的值傳遞給執行引擎,當JVM遇到一個變數讀取指令就會執行這個操作。
  6. assign,賦值,作用於工作記憶體變數,它把一個從執行引擎接收到的值賦值給工作記憶體變數。
  7. store,儲存,作用域工作記憶體變數,它把工作記憶體變數值傳送到主記憶體中。
  8. write,寫入,作用於主記憶體變數,它把store從工作記憶體中得到的變數值寫入到主記憶體變數中。

8種操作的實現規則:

  1. 不允許read和load、store和write操作之一單獨出現,即不允許載入或同步工作到一半。
  2. 不允許一個執行緒丟棄它最近的assign操作,即變數在工作記憶體中改變了之後,必須吧改變化同步回主記憶體。
  3. 不允許一個執行緒無原因地(無assign操作)把資料從工作記憶體同步到主記憶體中。
  4. 一個新的變數只能在主記憶體中誕生。
  5. 一個變數在同一時刻只允許一條執行緒對其進行lock操作,但lock操作可以被同一條執行緒重複執行多次,,多次lock之後必須要執行相同次數的unlock操作,變數才會解鎖。
  6. 如果對一個物件進行lock操作,那會清空工作記憶體變數中的值,在執行引擎使用這個變數前,需要重新執行load或assign操作初始化變數的值。
  7. 如果一個變數事先沒有被lock,就不允許對它進行unlock操作,也不允許去unlock一個被其他執行緒鎖住的變數。 對一個變數執行unlock操作之前,必須將此變數同步回主記憶體中(執行store、write)。

併發程式設計中的根本問題以及JMM提供的解決方案

整個併發程式設計所遇到的問題可以說是以下三個問題的變種。

  1. 原子性問題 由Java記憶體模型提供的8個原子性操作所支援,Long和Double的讀寫大部分商業虛擬機器上已實現為原子性操作,更大範圍的原子性操作,Java記憶體模型還提供了lock和unlock操作來支援,在位元組碼層次提供了monitorenter和monitorexit來隱式的使用這兩個操作,反映到java程式碼中就是同步程式碼塊了 synchronize。
  2. 可見性問題 由上圖主記憶體與工作記憶體的關係圖可知,執行緒不與主記憶體進行直接互動,而是把主記憶體的例項變數拷貝一份到執行緒的工作記憶體中進行操作,然後再同步給主記憶體。之所以這樣做,是因為工作記憶體大都由快取記憶體、暫存器這類比主記憶體存取速度更快的記憶體擔當,以便彌補CPU速度與主記憶體存取速度不在一個數量級的差距。

    注:當執行緒操作某個物件時,執行順序如下: 1 從主存複製變數到當前工作記憶體(read -> load) 2 執行程式碼改變共享變數的值(use -> assign) 3 用工作記憶體的資料重新整理主存相關內容(store -> write) 所以單個執行緒與執行緒的工作記憶體之間就有了相互的隔離效果,專業術語稱之為“可見性問題”

可見性是指當一個執行緒修改了共享變數的值,其他執行緒能夠立即得知這個修改,可見性由volatile支援,除了volatile以外,synchronize和final關鍵字,synchronize的可見性是由”對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中“這條規則保證的,而final關鍵字是指當final修飾的欄位在建構函式中一旦初始化完成,並且構造器沒有把this的引用傳遞出去,那在其他執行緒中就能看見final欄位的值,無須同步就能被其他執行緒正確訪問

  1. 時序性問題 執行緒在引用變數時不能直接從主記憶體引用,如果工作記憶體中內有該變數,則會從主記憶體拷貝一個副本到工作內 存中,即read -> load ,完成後執行緒會引用該副本。當同一個執行緒再度引用該欄位時,有可能重新從主記憶體獲取變數副本(read -> load -> use),也有可能直接引用原來的副本(use),也就是說read、load、use 順序可以有JVM實現系統決定。這個時候執行緒與執行緒之間操作的先後順序,就會決定你程式對主記憶體最後的修改是不是正確的,專業術語稱之為“時序性問題”。 Java提供了volatile和synchronize兩個關鍵字來保證執行緒之間操作的有序性,synchronize是由“一個變數在同一時刻只允許一條線成對其進行lock操作”。

HP(happens-before)

在JMM中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關 系。這裡提到的兩個操作既可以是在一個執行緒之內,也可以是在不同執行緒之間。 與程式設計師密切相關的happens-before規則如下。

  • 程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中的任意後續操作。
  • 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
  • volatile變數規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
  • 傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。
  • start()規則:如果執行緒A執行操作ThreadB.start()(啟動執行緒B),那麼A執行緒的ThreadB.start()操作happens-before於執行緒B中的任意操作。
  • join()規則:如果執行緒A執行操作ThreadB.join()併成功返回,那麼執行緒B中的任意操作happens-before於執行緒A從ThreadB.join()操作成功返回。

    注意 兩個操作之間具有happens-before關係,並不意味著前一個操作必須要在後一個操作之前執行!happens-before僅僅要求前一個操作(執行的結果)對後一個操作可見,且前一個操作按順序排在第二個操作之前(the first is visible to and ordered before the second)。

JMM的happens-before規則不但簡單易懂,而且也向程式設計師提供了足夠強的記憶體可見性保證

本篇總結

本篇文章簡單分析了下執行緒的啟動方式以及JMM模型,為後面的文章鋪墊一下。

下篇預告

Java多執行緒與鎖

此致,敬禮