Android小知識-Java多執行緒的基礎知識瞭解下

顧林海發表於2019-02-26

本平臺的文章更新會有延遲,大家可以關注微信公眾號-顧林海,包括年底前會更新kotlin由淺入深系列教程,目前計劃在微信公眾號進行首發,如果大家想獲取最新教程,請關注微信公眾號,謝謝!

十月份離職,在家修養一個多月,這一個多月做了很多事,自己的微信公眾號開通了,部落格也換了一種風格,在簡書和掘金分享一些Android方面的小知識,這一個多月看了些書,有技術相關的,也有非技術相關的,突然間覺得的這種生活也挺不錯的,這五年買了很多書,加起來最起碼有四五箱的書,以前上班忙,只有晚上回來看個一兩個小時,現在閒了,想全天看就全天看,讀書是一輩子的事,喜歡讀書,這樣無論在什麼時候都會有自己的思考和見地,不會一味地迎合或沉淪,從而失去立場,失去自己。當然在進行自我提升的同時也在看看有沒有一些工作機會,目前剛回到上海,希望自己能找到一家與之奮鬥的公司,一起成長下去。

閒話就扯到這裡,下面進入正文。


在講到多執行緒有必要了解下什麼是程式,在百度百科上是這麼定義程式的:程式是作業系統結構的基礎;是一次程式的執行;是一個程式及其資料在處理上順序執行時所發生的活動;是程式在一個資料集合上執行的過程,它是系統進行資源分配和排程的一個獨立單位。

百度百科對程式的定義比較抽象,舉個例子,我們在電腦開啟一個程式exe,那這個exe就可以理解成一個程式,程式是受作業系統管理的基本執行單元。那執行緒又是什麼,執行緒是在程式中獨立執行的子任務,比如開啟騰訊視訊(程式),你一邊在看視訊,一邊在下載視訊,同時在看視訊時資料的傳輸等等,這些同時執行的任務都是執行緒,利用多執行緒可以同一時間內執行更多不同種類的任務。

public class Client {

    public static void main(String[] args){
        //輸出main
        System.out.println(Thread.currentThread().getName());
    }

}
複製程式碼

通過currentThread方法獲取當前的執行緒名,上面這個程式在main入口函式中列印當前執行緒的名稱,發現預設就有一個叫做main執行緒在執行main()方法中的程式碼。

在Java中實現多執行緒程式設計的方式有兩種,一種是繼承Thread類,另一種是實現Runnable介面,下面這個程式就使用第一種方式繼承Thread類:

public class Task extends Thread {

    @Override
    public void run() {
        super.run();
        System.out.println("執行相關任務");
    }
}
複製程式碼

Task類繼承Thread,run方法中列印一句“執行相關任務”。

public class Client {

    public static void main(String[] args){
       Thread thread=new Task();
       thread.start();
       System.out.println("任務執行完畢!");
    }

}
複製程式碼

在main函式中先建立Task例項並執行Task執行緒,接著列印“任務執行完畢!”,執行下看看什麼結果。

任務執行完畢!
執行相關任務
複製程式碼

發現先列印“任務執行完畢!”,後列印“執行相關任務”,也就是在使用多執行緒時,程式碼的執行結果與程式碼執行順序或呼叫順序是無關的。執行緒是一個子任務,CPU以不確定的方式,或者說是以隨機的時間來呼叫執行緒中的run方法。

如果我們繼承了Thread類,就不能繼承其它類了,Java不支援多繼承,那怎麼辦呢?幸好Java提供了Runnable介面,接下來看第二種方式實現Runnable介面來建立執行緒。

public class Task implements Runnable {
    @Override
    public void run() {
        System.out.println("執行相關任務");
    }
}
複製程式碼

很簡單,Task類實現了Runnable介面並實現run方法,怎麼使用這個Task,和上面的Client一樣,程式碼如下:

public class Client {

    public static void main(String[] args){
       Runnable runnable=new Task();
       Thread thread=new Thread(runnable);
       thread.start();
       System.out.println("任務執行完畢!");
    }

}
複製程式碼

在編寫多執行緒時容易遇到資料共享問題,多個執行緒可以訪問一個變數,看下面程式:

public class Task implements Runnable {

    private int mTaskCount=0;

    @Override
    public void run() {
        mTaskCount++;
        System.out.println("執行第"+mTaskCount+"任務");
    }
}
複製程式碼

在Task執行緒中對mTaskCount進行遞增,下面是Client程式碼:

public class Client {

    public static void main(String[] args) {
        Runnable runnable = new Task();
        Thread thread_1 = new Thread(runnable);
        Thread thread_2 = new Thread(runnable);
        Thread thread_3 = new Thread(runnable);
        Thread thread_4 = new Thread(runnable);
        thread_1.start();
        thread_2.start();
        thread_3.start();
        thread_4.start();
        System.out.println("任務執行完畢!");
    }

}
複製程式碼

輸出如下:

任務執行完畢!
執行第2任務
執行第2任務
執行第3任務
執行第4任務
複製程式碼

發現兩個執行緒都列印了mTaskCount為2,產生了“非執行緒安全”問題,非執行緒安全主要是指多個執行緒對同一個物件中的同一個例項變數進行操作時會出現值被更改、值不同步的情況,影響程式的執行流程。在某些JVM中,mTaskCount的操作分成3個步驟,第一取得原有mTaskCount值,第二計算mTaskCount+1,第三對mTaskCount進行賦值;在這3個步驟中,如果遇到多個執行緒同時訪問,會出現指令重排序的問題,也就是非執行緒安全問題。那怎麼解決呢?可以在run方法前加上synchronized關鍵字:

public class Task implements Runnable {

    private int mTaskCount=0;

    @Override
   synchronized public void run() {
        mTaskCount++;
        System.out.println("執行第"+mTaskCount+"任務");
    }
}
複製程式碼

這樣輸出時mTaskCount是依次遞增的,在run方法前加上synchronized關鍵字,使多個執行緒在執行run方法時,以排隊的形式進行處理。當一個執行緒試圖呼叫run方法前,先判斷run方法有沒有上鎖,如果上鎖了,說明有其他執行緒在執行run方法,必須等其他執行緒執行完run方法,加鎖的這段程式碼稱為“互斥區”或“臨界區”。一個執行緒想要執行同步方法裡的程式碼時,需要先獲取鎖,如果獲取不到鎖,需要不斷的嘗試拿這把鎖,直到能夠拿到為止。

接著瞭解下Thread常用的幾種方法:

  • isAlive()方法用於判斷當前的執行緒是否處於活動狀態,活動狀態就是執行緒已經啟動且尚未終止,執行緒處於正在執行或準備開始執行的狀態,就認為執行緒是“存活”的。

  • sleep()方法的作用是在指定的毫秒數內讓當前“正在執行的執行緒”休 眠(暫停執行)。

  • getId()方法的作用是獲取執行緒的唯一標識。

執行緒的開啟是如此的簡單,但我們有時需要在滿足一定條件後關閉執行緒,這時如何去做呢?

可以通過interrupt()方法來停止執行緒,但interrupt()方法僅僅是在當前執行緒中打了一個停止的標記,並不是真的停止執行緒。在Java的SDK中,Thread提供了兩種方法用於判斷執行緒的狀態是不是停止,分別是interrupted()方法,用於測試當前執行緒是否已經中斷,還有一個就是isInterrupted()方法,用於測試執行緒是否已經中斷。

先看Thread.interrupted()方法的使用:

public class Task implements Runnable {

    @Override
   synchronized public void run() {
        for (int i=0;i<1000;i++){
            System.out.println("i="+i);
        }
    }
}
複製程式碼

在Task執行緒中通過for迴圈列印0到999。

public class Client {

    public static void main(String[] args) {
        Runnable runnable = new Task();
        Thread thread = new Thread(runnable);
        thread.start();
        try {
            Thread.sleep(10);
            thread.interrupt();
            System.out.println(Thread.interrupted());
            System.out.println(Thread.interrupted());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}
複製程式碼

啟動Task執行緒後,執行暫停10毫秒後呼叫interrupt()方法,最後列印出Thread.interrupted()方法兩次,我們看列印結果:

i=0
i=1
i=2
...
i=248
false
false
i=249
...
i=998
i=999
複製程式碼

通過interrupt()方法並不能停止Task執行緒,而執行Thread.interrupted()方法,輸出兩次都為false,也就是說Thread.interrupted()方法是用於測試當前執行緒是否已經中斷,這個當前執行緒指的是main執行緒,它從未中斷過,所以列印的結果是兩個false,這裡先看如何使main執行緒產生中斷效果,看下面程式碼:

public class Client {

    public static void main(String[] args) {
        Thread.currentThread().interrupt();
        System.out.println(Thread.interrupted());
        System.out.println(Thread.interrupted());
    }

}
複製程式碼

列印:

true
false
複製程式碼

通過Thread.currentThread().interrupt()給當前main執行緒打上停止的標誌,那為什麼第二次輸出Thread.interrupted()方法時是false呢?官方文件對interrupted()方法的解釋如下:測試當前執行緒是否已經中斷。執行緒的中斷狀態由該方法清除。換句話說,如果連續兩次呼叫該方法,則第二次呼叫將返回false。也就說interrupted()方法具有清除狀態的功能。

isInterrupted()方法與interrupted()方法相比,isInterrupted()方法並不具有清除狀態,也就是我們給Task執行緒執行interrupt()方法後,Task執行緒就被打上了中斷狀態,不管執行多少次isInterrupted()方法都會返回true。

既然知道了interrupt()的作用,如果先執行task執行緒的interrupt()方法,這時Task執行緒被打上中斷狀態,然後再在Task的run方法中通過判斷Thread.interrupted()是否為true,如果為true就退出迴圈,程式碼如下:

public class Task implements Runnable {

    @Override
   synchronized public void run() {

        for (int i=0;i<1000;i++){
            if(Thread.interrupted()){
                break;
            }
            System.out.println("i="+i);
        }
    }
}
複製程式碼

Client程式碼如下:

public class Client {

    public static void main(String[] args) {
        Runnable runnable = new Task();
        Thread thread = new Thread(runnable);
        thread.start();
        try {
            Thread.sleep(10);
            thread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}
複製程式碼

列印:

i=0
i=1
i=2
...
i=284
i=285
i=286
i=287
i=288
複製程式碼

這樣不就可以在外部中斷了Task執行緒,這種方式雖然可以停止了Task執行緒,但如果在for語句下列印一句話,程式碼:

public class Task implements Runnable {

    @Override
   synchronized public void run() {

        for (int i=0;i<1000;i++){
            if(Thread.interrupted()){
                break;
            }
            System.out.println("i="+i);
        }
        System.out.println("不應該列印");
    }
}
複製程式碼

列印:

i=0
i=1
i=2
...
i=223
i=224
i=225
不應該列印
複製程式碼

發現for迴圈語句下面的的println還是列印出來了,這時可以在判斷Thread.interrutped()語句中通過丟擲異常來退出,程式碼如下:

public class Task implements Runnable {

    @Override
    synchronized public void run() {
        try {
            for (int i = 0; i < 1000; i++) {
                if (Thread.interrupted()) {
                    throw new InterruptedException();
                }
                System.out.println("i=" + i);
            }
            System.out.println("不應該列印");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}
複製程式碼

列印:

i=0
i=1
i=2
...
i=106
i=107
i=108
java.lang.InterruptedException
    at com.book.demo.demo01.Task.run(Task.java:10)
    at java.lang.Thread.run(Thread.java:745)
複製程式碼

這種方式叫做異常法退出。

當然也可以通過return來退出執行緒:

public class Task implements Runnable {

    @Override
    synchronized public void run() {
        for (int i = 0; i < 1000; i++) {
            if (Thread.interrupted()) {
                return;
            }
            System.out.println("i=" + i);
        }
        System.out.println("不應該列印");

    }
}
複製程式碼

關於多執行緒的相關知識後面還有很多相關文章,等不及的可以在微信公眾號上檢視,謝謝!


838794-506ddad529df4cd4.webp.jpg

相關文章