Android併發編程 開篇

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


前言

從本篇博文開始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種操做來完成,虛擬機實現時必須保證每一種操做都是原子的、不可再分的。app

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

8種操做的實現規則:ide

  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字段的值,無須同步就能被其餘線程正確訪問

  3. 時序性問題
    線程在引用變量時不能直接從主內存引用,若是工做內存中內有該變量,則會從主內存拷貝一個副本到工做內 存中,即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多線程與鎖


此致,敬禮

相關文章
相關標籤/搜索