Android基礎知識:多線程基礎總結

一、前言

Android中因爲主線程不能進行耗時操做,因此耗時操做都要放到子線程中去作,因此多線程開發在實際中幾乎沒法避免。這篇文章就來總結一下與多線程有關的基礎知識。java

二、線程狀態

一個線程有如下幾種狀態:
1. New: 新建立狀態。線程被建立還沒被調用start方法。在線程運行前還有些基礎工做要作。
2. Runnable: 可運行狀態。調用過start方法。一個可運行的線程可能正在運行也可能沒在運行。
3. Blocked: 阻塞狀態。表示線程被鎖阻塞,暫時不活動。
4. Waiting: 等待狀態。線程暫時不活動,直到線程調度器從新激活它。
5. Time waiting: 超時等待狀態。和等待狀態不用的是它是能夠在指定的時間自行返回。
6. Terminated: 終止狀態。表示當前線程已經執行完畢。android

各個狀態之間的轉換關係以下圖。 編程

線程狀態

三、線程的建立與終止

Java中建立線程的方法有三種:數組

3.1 繼承Thread類,複寫run方法。

class MyThread extends Thread {
        @Override
        public void run() {
            Log.d(TAG, "MyThread線程名:" + Thread.currentThread().getName());
            Log.d(TAG, "MyThread is running");
        }
    }

    private void createThread() {
        MyThread myThread = new MyThread();
        myThread.start();
    }
複製代碼

3.2 實現Runnable接口,實現run方法

class MyRunnable implements Runnable {
        @Override
        public void run() {
            Log.d(TAG, "MyRunnable線程名:" + Thread.currentThread().getName());
            Log.d(TAG, "MyRunnable is running");
        }
    }
    
    private void createRunnable() {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
複製代碼

3.3 實現Callable接口,實現call方法。

class MyCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            Log.d(TAG, "MyCallable線程名:" + Thread.currentThread().getName());
            Log.d(TAG, "MyCallable is running");
            return "callable return";
        }
    }
    
    private void createCallable() throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        //獲取返回值
        String result = futureTask.get();
        Log.d(TAG, result);
    }
複製代碼

ThreadRunnable平時見得不少了,而關於Callable相對比較少,它與Runnnable接口相似,可是它有兩點與Runnnable接口不一樣:緩存

  1. Runnnable中的run方法沒法拋出異常,而Callablecall方法能夠。
  2. Runnnable中的run方法沒有返回值,而Callablecall方法能夠。Callable能夠拿到一個Future對象,表示異步任務的返回結果,調用get方法能夠獲取結果,若是此時異步任務還沒完成會阻塞當前進程直到返回結果。

下面舉個Callable使用的例子,模擬一個買菜作菜的過程。先用Runnable實現,再用Callable實現,作一下對比。安全

private void runnableCook() throws InterruptedException {
        //買食材線程
        Thread foodThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //買食材耗時
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d(TAG, "五花肉已買");
                food = new String("五花肉");
            }
        });
        Log.d(TAG, "我要開始作菜了");
        Log.d(TAG, "我準備作紅燒肉");
        Log.d(TAG, "我沒有食材");
        //開啓購買食材線程
        Log.d(TAG, "叫隔壁老王幫我出門去買食材");
        foodThread.start();
        //模擬清洗廚具準備的耗時操做
        Log.d(TAG, "清洗廚具準備調料,準備作菜");
        Thread.sleep(3000);
        //要等到食材買回來才能開始作菜,因此將食材線程join
        foodThread.join();
        Log.d(TAG, "開始作菜了");
        cook(food);
    }
    
    private void cook(String food) {
        if (TextUtils.isEmpty(food)) {
            //沒調用foodThread.join就會走這裏
            Log.d(TAG, "沒有食材無法作菜");
            return;
        }
        Log.d(TAG, "菜作好了,開吃了");
    }
複製代碼

代碼很簡單,就是定義一個買菜的線程作買菜的耗時任務,而後主線程打印作菜日誌,由於Runnable沒有返回值,在作完作菜準備以後須要調用foodThread.join()等待食材買回,再進行cook工做,若是不等到foodThread工做結束就調用cook方法,就會發現沒有食材作菜。bash

未調用foodThread.join方法
調用foodThread.join方法

接下來使用Callable來完成這個過程。多線程

private void callableCook() throws InterruptedException, ExecutionException {
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(5000);
                Log.d(TAG, "五花肉已買");
                return "五花肉";
            }
        };
        FutureTask<String> futureTask = new FutureTask<String>(callable);
        Thread foodThread = new Thread(futureTask);
        Log.d(TAG, "我要開始作菜了");
        Log.d(TAG, "我準備作紅燒肉");
        Log.d(TAG, "我沒有食材");
        Log.d(TAG, "叫隔壁小王出門去買食材");
        foodThread.start();
        Log.d(TAG, "清洗廚具準備調料,準備作菜");
        Thread.sleep(3000);
        if (!futureTask.isDone()) {
            Log.d(TAG, "食材還沒到啊,小王好慢啊");
        }
        food = futureTask.get();
        Log.d(TAG, "開始作菜了");
        cook(food);
    }
    
       private void cook(String food) {
        if (TextUtils.isEmpty(food)) {
            //沒調用foodThread.join就會走這裏
            Log.d(TAG, "沒有食材無法作菜");
            return;
        }
        Log.d(TAG, "菜作好了,開吃了");
    }
複製代碼

Callable接口的使用方法和Runnable相似。併發

第一步先實現Callable接口併爲其指定一個泛型,這個泛型類型就是call方法要返回的類型,實現call方法在其中作耗時任務,並將任務結果經過return返回。app

第二步建立一個FutrueTask,一樣設置返回類型泛型,並將剛實現的Callable對象傳入,這個FutrueTask後面就用來得到異步任務的結果。

最後一步和Runnable同樣,新建一個Thread對象,將FutrueTask傳入,而後開啓線程便可。

Runnable不一樣的是FutrueTask提供一個isDone方法用來判斷異步任務是否完成,而且提供futureTask.get方法來獲取異步任務的返回結果,若是此時任務還沒結束,則會阻塞當前線程直到任務完成。這樣就不會出現食材還沒買回來就開始作菜了。

callable實現

3.4 終止線程

一個線程的run方法運行完成或者方法中剛出現異常以後這個線程就會終止。除此以外Thread類提供了stopinterrupt方法終止線程,其中stop已過期被棄用,而調用interrupt方法,線程會將本身的終止標識爲true,線程會一直檢測這個標識位以判斷是否被終止。

安全中止線程的方法:採用布爾值變量
private volatile boolean flag = true;

  private void createFlagThread() {
        flag = true;
        count = 0;
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (flag) {
                    Log.d(TAG, "flag count:" + count++);
                }
            }
        });
        thread.start();
    }
    
    private void changeFlag() {
        flag = false;
    }
複製代碼

定義布爾值flag調用createFlagThread方法建立線程,run方法中經過flag來判斷是否結束循環從而結束線程,當須要中止線程時,調用changeFlag方法將布爾值設置爲false便可。

3.5 volatile關鍵字

在3.4的例子中,用來中止循環的布爾值變量上加了一個volatile關鍵字,volatile被稱爲輕量級的synchronized,但volatile只能修飾變量不能修飾方法。在遇到變量須要被多個線程訪問時能夠用volatile關鍵字修飾它。例如在經典的雙重檢查的單例模式下就會用到volatile修飾。

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  
複製代碼

那麼volatile真的能替代synchronized嗎?答案確定是不能的。關於volatile的做用先要從併發編程的三個特性和Java的內存模型開始講起。

3.5.1 原子性、可見性、有序性
  • 原子性: 原子性即知足不可拆分的操做,一個操做要麼執行完成,要麼沒有執行。
// 例如Java中的賦值操做
    int i = 1;//是原子行操做
    int j = i;//不是原子性操做,由於這個操做要先讀取i的值,再將i的值賦給j
複製代碼
  • 可見性: 可見性是指多線程之間的可見性,一個線程修改了某個狀態對另外一個線程是可見的即另外一個線程立刻就能看得見修改。
  • 有序性: 有序性是指Java內存模型中是容許編譯器和處理器對指令進行重排序的,重排序不會影響單線程執行的正確性,可是在多線程狀況下就會形象多線程併發的正確行。
3.5.2 Java內存模型

Java運行時數據區域以下圖。

從圖中能夠看出主要分爲如下幾個部分:

1. 程序計數器

程序計數器是一塊較小的內存空間。可看作當前線程所執行的字節碼的行號指示器,線程私有。

2. Java虛擬機棧

線程私有。生命週期與線程相同。Java虛擬機棧描述的是Java方法執行的內存模型。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

3. 本地方法棧

與虛擬機棧做用類似,區別是虛擬機棧爲虛擬機執行Java方法也就是字節碼服務,而本地方法棧爲虛擬機使用到的Native方法服務。

4. Java堆

全部線程共享的一塊內存區域。在虛擬機啓動時建立,存放對象實例。是垃圾收集器管理的主要區域。

5. 方法區

線程共享的一塊內存區域。用於存儲虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼的數據。

6. 運行時常量池

是方法區的一部分。用於存放編譯期生成的各類字面量和符號引用。具備動態性。

7. 直接內存

並非虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域,可是這部份內存被頻繁使用。

從以上能夠看出Java堆內存是被全部線程共享的內存區域,因而就存在可見性的問題。Java內存模型定義了線程和主存之間的抽象關係:線程之間共享變量存儲在主存中,每一個線程有個私有的本地內存,本地內存中存儲了共享變量的一個副本(本地內存是Java內存模型中一個抽象的概念,不真實存在)。抽象示意圖以下。

如上圖,線程A要與線程B通訊要通過兩個步驟:一是線程A把線程A本地內存中更新過的共享變量刷新到主內存中去;二是線程B要到主線程中讀取線程A更新過去的共享變量。

回到以前講的volatile關鍵字,volatile關鍵字修飾的變量只能保證併發三個特性中的兩個:可見性和有序性。並不能保證原子性。因此在以前不管是單例仍是停止線程的代碼中都將多個線程訪問的變量使用了volatile修飾,保證變量在一個線程修改了以後對全部線程可見且指令有序。又由於沒法保證原子性,因此即便聲明變量使用了volatile修飾,可是多個線程併發執行例如i++這種非原子性操做是仍是會出現問題,這時就須要使用synchronized等方法來解決線程的同步問題。

四、線程同步

多線程訪問同一個資源必然會產生線程同步問題,不解決線程同步問題就會形成資源數據的錯誤。關於線程同步問題仍是經過那個經典的多窗口賣票問題來解釋理解。

4.1 多窗口賣票

m個賣票窗口同時賣n張票,採用多線程實現。這裏先看一下不作同步的錯誤示例。這裏畫了個簡單的界面,兩個輸入框輸入總票數和窗口數,按鈕點擊開始賣票,最後的結果由TextView展現。

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".TicketActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <EditText
            android:id="@+id/et_ticket_count"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="輸入總票數"
            android:inputType="number"
            android:text="100" />
        <EditText
            android:id="@+id/et_window_count"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="輸入賣票窗口數"
            android:inputType="number"
            android:text="3" />
        <Button
            android:id="@+id/btn_begin"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="開賣" />

        <TextView
            android:id="@+id/tv_result"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="@android:color/black"
            android:textSize="16sp" />
    </LinearLayout>
</ScrollView>
複製代碼

賣票方法代碼:

//結果字符串
    private StringBuilder result = new StringBuilder();
    //總票數
    private  int  allTicketCount = 0;
    //賣票窗口數
    private int windowCount = 0;
    
    private void begin() {
        allTicketCount = Integer.parseInt(mEtTicketCount.getText().toString().trim());
        windowCount = Integer.parseInt(mEtWindowCount.getText().toString().trim());
        //每次調用清空以前結果
        result.delete(0, result.length());
        result.append("總票數:" + allTicketCount + ",共有" + windowCount + "個窗口賣票\n");
        mTvResult.setText(result.toString());
        //循環建立多個窗口線程並開啓
        for (int count = 1; count <= windowCount; count++) {
            Thread window = new Thread(new TicketErrorRunnable(), "售票窗口" + count);
            window.start();
        }
    }
    
    /**
     * 無同步
     */
    class TicketErrorRunnable implements Runnable {

        @Override
        public void run() {
            while (allTicketCount > 0) {
                    // 總票數大於0就將票數減一
                    allTicketCount--;
                    // 輸出添加賣出一張票和剩餘票數
                    result.append(Thread.currentThread().getName() + ":賣出一張票,還剩" + allTicketCount + "張票。\n");
                    //通知刷新UI
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mTvResult.setText(result.toString());
                        }
                    });
            
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
            //循環結束說明票賣完了,刷新UI
            result.append(Thread.currentThread().getName() + ":票賣完啦。\n");
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mTvResult.setText(result.toString());
                }
            });

        }
    }
複製代碼

運行結果:

未作同步
點擊按鈕執行 begin方法,根據運行結果能夠看到不作線程同步,會出現這種兩個窗口賣出同一張票的狀況致使數據的錯誤。

4.2 同步代碼塊

將須要同步的代碼放到同步代碼塊中能夠保證線程同步,具體來看代碼。

/**
     * 同步代碼塊
     */
    class TicketSyncRunnable implements Runnable {
        @Override
        public void run() {
            while (allTicketCount > 0) {
                // ----------同步代碼塊----------------
                synchronized (TicketActivity.class) {
                    allTicketCount--;
                    result.append(Thread.currentThread().getName() + ":賣出一張票,還剩" + allTicketCount + "張票。\n");
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mTvResult.setText(result.toString());
                        }
                    });
                }
               // ----------同步代碼塊----------------
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
            result.append(Thread.currentThread().getName() + ":票賣完啦。\n");
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mTvResult.setText(result.toString());
                }
            });
        }
    }
複製代碼

這裏其它的都沒改變,只修改了Runnable中的內容,將對總票數的計算放到同步代碼塊中進行同步,保證同時只有一個線程能對總票數allTicketCount進行操做。

運行結果:

同步代碼塊

4.3 同步方法

將須要同步的代碼抽出一個方法,再方法上加上synchronized關鍵字成爲同步方法,也能夠保證線程同步。

/**
     * 同步方法
     */
    class TicketSyncMethodRunnable implements Runnable {
        @Override
        public void run() {
            ticket();
            result.append(Thread.currentThread().getName() + ":票賣完啦。\n");
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mTvResult.setText(result.toString());
                }
            });
        }
        // 同步方法
        private synchronized void ticket() {
            while (allTicketCount > 0) {
                allTicketCount--;
                result.append(Thread.currentThread().getName() + ":賣出一張票,還剩" + allTicketCount + "張票。\n");
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mTvResult.setText(result.toString());
                    }
                });
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
複製代碼

將對總票數的操做抽出到同步方法中,查看運行結果:

同步方法

4.4 重入鎖ReentrantLock

本身給代碼加鎖固然也能夠保證進程同步。

private ReentrantLock reentrantLock = new ReentrantLock();
    
    /**
     * 重入鎖
     */
    class TicketLockRunnable implements Runnable {
        @Override
        public void run() {
            while (allTicketCount > 0) {
                //上鎖
                reentrantLock.lock();
                try {
                    allTicketCount--;
                    result.append(Thread.currentThread().getName() + ":賣出一張票,還剩" + allTicketCount + "張票。\n");
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mTvResult.setText(result.toString());
                        }
                    });
                } finally {
                    //開鎖
                    reentrantLock.unlock();
                }
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            result.append(Thread.currentThread().getName() + ":票賣完啦。\n");
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mTvResult.setText(result.toString());
                }
            });

        }
    }
複製代碼

運行結果:

加鎖

五、Android中實現多線程的幾種方式

Android中固然可使用上面提到的三種建立線程的方法實現多線程,可是我這裏要說的是Android裏爲咱們提供的幾種封裝過的多線程執行異步任務的使用方法,畢竟咱們手動new Thread().start()不只不便於管理,並且還會有意想不到的收穫(內存泄漏)。

5.1 AsyncTask

AsyncTaskAndroid提供的執行異步任務的類。
AsyncTask的使用:首先定義一個類繼承AsyncTask

class MyAsyncTask extends AsyncTask<String, Integer, String> {
        int count = 0;
        @Override
        protected void onPreExecute() {
            //doInBackground執行以前
            Log.d(TAG, Thread.currentThread().getName() + " 異步任務準備開始 " + System.currentTimeMillis());
        }
        @Override
        protected void onProgressUpdate(Integer... values) {
            //doInBackground執行中
            Log.d(TAG, Thread.currentThread().getName() + " 任務進行中:" + values[0] + "% ");

        }
        @Override
        protected void onPostExecute(String result) {
            //doInBackground執行以後
            Log.d(TAG, Thread.currentThread().getName() + " " + result + " " + System.currentTimeMillis());
        }
        @Override
        protected String doInBackground(String... strings) {
            Log.d(TAG, Thread.currentThread().getName() + " 異步任務執行中 " + System.currentTimeMillis());
            while (count < 100) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count += 10;
                publishProgress(count);
            }
            Log.d(TAG, Thread.currentThread().getName() + " 異步任務完成!" + System.currentTimeMillis());
            return Thread.currentThread().getName() + ":任務完成";
        }
    }
複製代碼

AsyncTask三個泛型參數ParamsProgressResult分別對應傳入的參數類型、中間進度的類型、返回結果的類型。AsyncTask主要有四個回調方法複寫。

  • doInBackground:必須複寫,異步執行後臺線程,要完成的耗時任務在這裏處理,運行在子線程。
  • onPreExecute:執行doInBackground前被調用,進行耗時操做前的初始化或者準備工做,運行在主線程。
  • onProgressUpdate:doInBackground執行完成後調用,返回處理結果,更新UI等,運行在主線程。
  • onProgressUpdate:在doInBackground方法中調用publishProgress方法後調用更新耗時任務進度,運行在主線程。

調用AsyncTask

MyAsyncTask myAsyncTask = new MyAsyncTask();
   myAsyncTask.execute();
複製代碼

運行結果日誌:

關於想進一步瞭解AsyncTask的源碼運行原理能夠看Android進階知識:AsyncTask相關

5.2 HandlerThread

HandlerThread是將HandlerThread的封裝,本質上仍是一個Thread
HandlerThread的使用以下:

// 建立一個HandlerThread
        HandlerThread handlerThread = new HandlerThread("myHandlerThread");
        //調用start方法開啓線程
        handlerThread.start();
        //建立一個Handler傳入handlerThread的Looper
        handler = new Handler(handlerThread.getLooper()) {
            //複寫handleMessage方法處理消息
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case 1:
                        Log.d(TAG, Thread.currentThread().getName() + " receive one message");
                        break;
                }
            }
        };
        
        //在調用處使用handler發送消息
        Message message = Message.obtain();
        message.what = 1;
        handler.sendMessage(message);
        
        // 使用完退出HandlerThread
        handlerThread.quit();
複製代碼

運行結果日誌:

關於想進一步瞭解 HandlerThread的源碼運行原理能夠看 Android進階知識:HandlerThread相關

5.3 IntentService

IntentService是將ServiceHandlerThread封裝,與普通Service不一樣的是,普通Service運行在主線程,IntentService建立一個工做子線程執行任務,而且IntentService在執行完任務後自動關閉服務,而普通Service須要手動調用stopService方法。 IntentService使用以下:

//繼承實現IntentService
public class MyIntentService  extends IntentService {

    private static final String TAG = "MyIntentService";

    public MyIntentService() {
        super("MyIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        //複寫onHandleIntent方法根據intent傳遞參數實現處理任務
        String type = intent.getStringExtra("type");
        switch (type){
            case "type1":
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d(TAG,Thread.currentThread().getName()+" type1 doing");
                break;
        }
    }
}

//由於是服務因此要在AndroidManifest中註冊
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.thread.threaddemo">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
          ......
        <service android:name=".MyIntentService"/>
    </application>
</manifest>

//啓動服務
Intent intent = new Intent(this, MyIntentService.class);
intent.putExtra("type", "type1");
startService(intent);
複製代碼

運行日誌結果:

關於想進一步瞭解 IntentService的源碼運行原理能夠看 Android進階知識:IntentService相關

5.4 Handler

對於Android開發者來講,Handler再熟悉不過了,HandlerAndroid提供的一種多線程間通訊方式。Android關於多線程確定避不開Handler。這裏對Handler的使用就不舉例了,想要了解使用及原理的能夠看我寫的這篇Android進階知識:Handler相關

六、線程池

線程池是用來管理線程的工具,在開發中若是每次進行異步任務都手動建立一個線程,不只浪費資源並且不容易管理控制,線程池的使用就能很好的解決這些問題。

6.1 阻塞隊列

在瞭解線程池以前先來了解一下阻塞隊列,阻塞隊列的理解有助於咱們對線程池的工做原理的學習。阻塞隊列是併發編程中「生產者-消費者」模式裏用來存放元素的容器,生產者生產元素放入阻塞隊列,消費者從阻塞隊列中取出元素,當隊列中沒有元素或者隊列中元素已滿時會發生阻塞,直到隊列中再次加入元素或者去除元素爲止。阻塞隊列實現了元素添加取出時的鎖操做,因此使用時無需單獨考慮線程同步問題。

Java中提供了七種阻塞隊列:

  • ArrayBlockingQueue:由數組結構組成的有界阻塞隊列。
  • LinkedBlockingQueue:由鏈表結構組成的有界阻塞隊列。
  • PriorityBlockingQueue:支持優先級排序的無界阻塞隊列。
  • DelayQueue:使用優先級隊列實現的無界阻塞隊列。
  • SynchronousQueue:不存儲元素的阻塞隊列。
  • LinkedTransferQueue:由鏈表結構足層的無界阻塞隊列。
  • LinkedBlockingDeque:由鏈表結構組成的雙向阻塞隊列。

下面經過使用阻塞隊列實現一個「生產者-消費者」模式的例子。

package com.thread.threaddemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import java.util.concurrent.ArrayBlockingQueue;

public class BlockingQueueActivity extends AppCompatActivity implements View.OnClickListener {

    private static final String TAG = "BlockingQueueActivity";
    private Button mBtnMain;
    private Button mBtnProduce;
    private Button mBtnConsumer;
    // 阻塞隊列
    private ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(5);
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_blocking_queue);
        initView();
    }
    private void initView() {
        mBtnMain = (Button) findViewById(R.id.btn_main);
        mBtnProduce = (Button) findViewById(R.id.btn_produce);
        mBtnConsumer = (Button) findViewById(R.id.btn_consumer);
        mBtnMain.setOnClickListener(this);
        mBtnProduce.setOnClickListener(this);
        mBtnConsumer.setOnClickListener(this);
    }
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_main:
                mainStart();
                break;
            case R.id.btn_produce:
                addData();
                break;
            case R.id.btn_consumer:
                removeData();
                break;
        }
    }
    /*
     * 手動從阻塞隊列取出一個元素
     */
    private void removeData() {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    queue.take();
                    printData();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "remove");
        thread.start();
    }
    /*
     * 手動從阻塞隊列添加一個元素
     */
    private void addData() {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    queue.put("data");
                    printData();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "add");
        thread.start();
    }
     /*
      * 打印日誌
      */
    private void printData() {
        Log.d(TAG, Thread.currentThread().getName() + ":" + queue.toString());
    }
     /*
      * 開啓生產者消費者線程
      */
    private void mainStart() {
        ProducerThread producerThread = new ProducerThread("Producer");
        producerThread.start();
        ConsumerThread consumerThread = new ConsumerThread("Consumer");
        consumerThread.start();
    }
     /*
      * 消費者線程
      */
    class ConsumerThread extends Thread {
        public ConsumerThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(2000);
                    queue.take();
                    printData();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
     /*
      * 生產者線程
      */
    class ProducerThread extends Thread {
        public ProducerThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(1000);
                    queue.put("data");
                    printData();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
複製代碼

運行打印日誌:

從這段代碼能夠看到首先建立阻塞隊列queue容量爲5,點擊按鈕調用mainStart方法,開啓生產者線程和消費者線程,生產者線程每隔1秒生產一個元素加入隊列,消費者每隔2秒消費一個元素取出隊列。能夠看到因爲生產比消費快,因此容器逐漸被加滿,最後保證隊列中充滿五個元素,再想添加只能等到消費者線程消費了元素才行。固然也能夠經過手動調用addDataremoveData方法來向隊列裏添加和取出元素,一樣阻塞隊列滿了或者空了就會發生阻塞,直到隊列中空出位置或者加入新元素爲止。

6.2 線程池

6.2.1 構造函數

Java中線程池的核心實現類是ThreadPoolExecutor,不管是Runnable仍是Callable均可以交給線程池來統一管理,要使用線程池就要先建立一個線程池對象。

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 20, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100), threadFactory, new ThreadPoolExecutor.AbortPolicy());
複製代碼

這樣就建立了一個線程池對象,接着就能夠調用executesubmit提交任務,線程池會分配線程處理任務。不過ThreadPoolExecutor的構造方法中這幾個參數到底表明了什麼,咱們來看一下它的構造方法。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
複製代碼

構造函數中就是對這幾個成員參數的一個初始化賦值,來看這幾個參數表明的意思。

  • corePoolSize: 核心線程數。默認狀況下線程池是空的,在有任務提交時,當前運行線程少於corePoolSize數,就會建立新線程來處理任務。
  • maximumPoolSize: 線程池容許建立的最大線程數。若是任務隊列已滿而且線程數小於maximumPoolSize時,線程池仍舊會建立新的線程來處理任務。
  • keepAliveTime: 非核心線程空閒的超時時間。超過這個時間則會被回收。
  • TimeUnit:keepAliveTime的時間單位。
  • workQueue: 任務隊列。是一個阻塞隊列,若是當前線程數大核心線程數,則將任務添加到此任務隊列中。
  • ThreadFactory: 線程工廠。
  • RejectedExecutionHandler: 飽和策略。當任務隊列和線程池都滿了時所採起的策略。
6.2.2 執行流程

線程執行流程源碼從ThreadPoolExecutorexecute方法開始來看。

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        //判斷工做線程數是否小於核心線程數
        if (workerCountOf(c) < corePoolSize) {
            //小於就調用addWorker方法建立核心線程
            //這裏第二個參數true表示是核心線程
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //不然調用workQueue.offer方法將任務放入任務隊列
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //放入任務隊列失敗則再調用addWorker方法建立非核心線程執行任務
        //這裏第二參數傳入false表示非核心線程
        else if (!addWorker(command, false))
           //若是addWorker返回false
           //說明線程數超過最大線程數maximumPoolSize
           //調用reject方法執行飽和策略
            reject(command);
    }
複製代碼

由上述代碼和註釋能夠看出具體執行流程以下:

  1. 判斷線程池中核心線程數量是否已到達設置的最大核心線程數corePoolSize,若沒有到達核心線程數就建立核心線程處理任務。
  2. 若已到達核心線程數,再判斷任務隊列是否已滿,若未滿就將任務加入到任務隊列中。
  3. 若任務隊列已滿,再判斷是否已達到線程池最大線程數maxmumPoolSize,若未達到最大線程數,則建立非核心線程處理任務。
  4. 若到達最大線程數,就執行飽和策略。

這裏再繼續看一下reject方法:

final void reject(Runnable command) {
        handler.rejectedExecution(command, this);
    }
複製代碼

調用了飽和策略中的rejectedExecution方法。這裏飽和策略有4種,分別是:

  • AbordPolicy: 默認是這種策略,表示沒法處理新任務,並拋出RejectedExecutionException異常。
  • CallerRunsPolicy: 用調用者所在的線程來處理任務。
  • DiscardPolicy: 不能執行的任務,並將該任務刪除。
  • DiscardOldestPolicy: 丟棄隊列最近的任務,並執行當前的任務。

這裏看下默認的AbordPolicy的實現。

public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() { }

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }
複製代碼

能夠看到實現很簡單就是實現了RejectedExecutionHandler接口,複寫rejectedExecution方法,拋出RejectedExecutionException異常。其它的飽和策略的實現也是相似。

線程池執行流程

6.2.3 經常使用線程池

Java中提供了幾種默認經常使用線程池。

  • FixedThreadPool: 可重用固定線程數的線程池。
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }
複製代碼

從建立方法中能夠看出FixedThreadPool線程池的核心線程數和最大線程數是相同的,因此它是沒有非核心線程的。而且它的線程空閒超時時長設置爲0,因此一旦線程任務結束空閒下來就會被回收,所以不會有空閒線程存在。

  • CachedThreadPool: 可緩存線程池。
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
複製代碼

CachedThreadPool它的核心線程數爲0,不會建立核心線程,而是直接將任務加入任務隊列,而它的最大線程數爲Integer.MAX_VALUE,也就是說能夠無限建立非核心線程的來處理任務。而且它的線程空閒超時時間爲60秒,空閒線程能夠緩存60秒後纔會被回收。

  • SingleThreadExecutor: 單線程工做的線程池。
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
複製代碼

SingleThreadExecutor的核心線程數爲1,最大線程數爲1,因此SingleThreadExecutor線程池中只有一個核心線程在工做,空閒超時時間爲0,任務結束當即回收,在惟一的核心線程在工做時,提交的任務會放入LinkedBlockingQueue任務隊列中等待一個一個執行。

  • ScheduledThreadPool: 實現定時和週期性任務的線程池。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }
複製代碼

ScheduledThreadPool的核心線程數是固定傳入的,而最大線程數是Integer.MAX_VALUE,任務隊列爲DelayedWorkQueue,自己是無界隊列,因此線程池中不會建立非核心線程,當工做線程數到達核心線程數時,線程池只會一直往任務隊列中添加任務,空閒線程的超時時間爲10秒。

七、總結

以上就是Android中多線程相關的基礎知識。關於多線程在使用的時候須要多加註意,不只要注意保證線程同步在Android中還要注意內存泄漏的發生。須要頻繁建立子線程操做最好使用線程池進行管理。

參考資料:

Android進階之光
深刻理解Java虛擬機

相關文章
相關標籤/搜索