並發編程從零開始(一)


並發編程從零開始(一)

簡介

java是一個支持多線程的開發語言。多線程可以在包含多個CPU核心的機器上同時處理多個不同的任務,優化資源的使用率,提升程序的效率。在一些對性能要求比較高場合,多線程是java程序調優的重要方面。

Java並發編程主要涉及以下幾個部分:

  • 並發編程三要素:原子性:即一個不可再被分割的顆粒。在Java中原子性指的是一個或多個操作要么全部執行成功要么全部執行失敗。

    有序性:程序執行的順序按照代碼的先后順序執行。(處理器可能會對指令進行重排序)

    可見性:當多個線程訪問同一個變量時,如果其中一個線程對其作了修改,其他線程能立即獲取到最新的值。

  • 線程的五大狀態:

    創建狀態:當用 new 操作符創建一個線程的時候

    就緒狀態:調用 start 方法,處於就緒狀態的線程並不一定馬上就會執行 run 方法,還需要等待CPU的調度

    運行狀態:CPU 開始調度線程,並開始執行 run 方法

    阻塞狀態:線程的執行過程中由於一些原因進入阻塞狀態比如:調用 sleep 方法、嘗試去得到一個鎖等等

    死亡狀態:run 方法執行完 或者 執行過程中遇到了一個異常

  • 悲觀鎖與樂觀鎖:

    悲觀鎖:每次操作都會加鎖,會造成線程阻塞。

    樂觀鎖:每次操作不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止,不會造成線程阻塞。

  • 線程之間的協作:

    線程間的協作有:wait/notify/notifyAll等 。

  • synchronized 關鍵字:

    synchronized是Java中的關鍵字,是一種同步鎖。它修飾的對象有以下幾種:

    1. 修飾一個代碼塊:被修飾的代碼塊稱為同步語句塊,其作用的范圍是大括號{}括起來的代碼,作用的對象是調用這個代碼塊的對象
    2. 修飾一個方法:被修飾的方法稱為同步方法,其作用的范圍是整個方法,作用的對象是調用這個方法的對象
    3. 修飾一個靜態的方法:其作用的范圍是整個靜態方法,作用的對象是這個類的所有對象
    4. 修飾一個類:其作用的范圍是synchronized后面括號括起來的部分,作用主的對象是這個類的所有對象。
  • CAS:

    CAS全稱是Compare And Swap,即比較替換,是實現並發應用到的一種技術。操作包含三個操作數—內存位置(V)、預期原值(A)和新值(B)。 如果內存位置的值與預期原值相匹配,那么處理器會自動將該位置值更新為新值 。否則,處理器不做任何操作。CAS存在三大問題:ABA問題,循環時間長開銷大,以及只能保證一個共享變量的原子操作。

  • 線程池:

    如果我們使用線程的時候就去創建一個線程,雖然簡單,但是存在很大的問題。如果並發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因為頻繁創建線程和銷毀線程需要時間。線程池通過復用可以大大減少線程頻繁創建與銷毀帶來的性能上的損耗。


第一部分:多線程&並發設計原理

1. 多線程

1.1 Thread 和 Runnable

1.1.1 java中的線程

創建執行線程的兩種方法:

  • 擴展Thread 類。繼承Thread類實現多線程,覆蓋run()方法。

    public class ThreadCreatingByThread extends Thread{
        @Override
        public void run() {
            while(true){
                System.out.println(Thread.currentThread().getName()+" is running");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    在Main中調用測試:

    public class Main {
        public static void main(String[] args) {
            //調用線程創建類創建線程
    
            //通過繼承Thread類,重寫run()方法的方式創建線程
            ThreadCreatingByThread thread = new ThreadCreatingByThread();
            thread.start();
        }
    }
    
  • 實現Runnable 接口。實現run()方法。

    public class ThreadCreatingByRunnable implements Runnable{
        @Override
        public void run() {
            while(true){
                System.out.println(Thread.currentThread().getName()+" is running");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    在Main中調用測試:

    public class Main {
        public static void main(String[] args) {
            //調用線程創建類創建線程
    
            //通過繼承Runnable接口,實現run()方法的方式創建線程
            Thread thread = new Thread(new ThreadCreatingByRunnable());
            thread.start();
        }
    }
    
  • 覆寫Callable接口實現多線程(jdk1.5),實現call()方法,有返回值。

    public class ThreadCreatingByCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            Thread.sleep(3000);
            return "hello world call() invoked!";
        }
    }
    

    在Main中進行測試:

    public class Main {
        public static void main(String[] args) throws InterruptedException, ExecutionException {
            //調用線程創建類創建線程
            //覆寫Callable接口實現多線程(jdk1.5),實現call()方法,有返回值。
            ThreadCreatingByCallable threadCreatingByCallable = new ThreadCreatingByCallable();
            //定義FutureTask代表了一個由Callable定義的未來的工作
            // 設置Callable對象,泛型表示Callable的返回類型
            FutureTask<String> futureTask = new FutureTask<>(threadCreatingByCallable);
            // 啟動處理線程
            new Thread(futureTask).start();
            // 同步等待線程運行的結果
            String result = futureTask.get();
            System.out.println(result);
    
        }
    }
    
  • 通過線程池啟動多線程。

1.1.2 Java中的線程: 特征和狀態
  1. 所有的Java 程序,不論並發與否,都有一個名為主線程的Thread 對象。執行該程序時, Java虛擬機( JVM )將創建一個新Thread 並在該線程中執行main()方法。這是非並發應用程序中唯一的線程,也是並發應用程序中的第一個線程。

  2. Java中的線程共享應用程序中的所有資源,包括內存和打開的文件,快速而簡單地共享信息。但是必須使用同步避免數據競爭

  3. Java中的所有線程都有一個優先級,這個整數值介於Thread.MIN_PRIORITY(1)和Thread.MAX_PRIORITY(10)之間,默認優先級是Thread.NORM_PRIORITY(5)。線程的執行順序並沒有保證,通常,較高優先級的線程將在較低優先級的錢程之前執行。

  4. 在Java 中,可以創建兩種線程:

    • 守護線程。
    • 非守護線程。

    區別在於它們如何影響程序的結束。

    Java程序結束執行過程的情形:

    • 程序執行Runtime類的exit()方法, 而且用戶有權執行該方法。
    • 應用程序的所有非守護線程均已結束執行,無論是否有正在運行的守護線程

    守護線程通常用在作為垃圾收集器或緩存管理器的應用程序中,執行輔助任務。在線程start之前調用isDaemon()方法檢查線程是否為守護線程,也可以使用setDaemon()方法將某個線程確立為守護線程。

  5. Thread.States類中定義線程的狀態如下:

    • NEW:Thread對象已經創建,但是還沒有開始執行。
    • RUNNABLE:Thread對象正在Java虛擬機中運行。
    • BLOCKED : Thread對象正在等待鎖定。
    • WAITING:Thread 對象正在等待另一個線程的動作。
    • TIME_WAITING:Thread對象正在等待另一個線程的操作,但是有時間限制。
    • TERMINATED:Thread對象已經完成了執行。

    getState()方法獲取Thread對象的狀態,可以直接更改線程的狀態。

    在給定時間內, 線程只能處於一個狀態。這些狀態是JVM使用的狀態,不能映射到操作系統的線程狀態。

1.1.3 Thread類和Runnable接口

Runnable接口只定義了一種方法:run()方法。這是每個線程的主方法。當執行start()方法啟動新線程時,它將調用run()方法。

Thread類其他常用方法:

  • 獲取和設置Thread對象信息的方法。
    • getId():該方法返回Thread對象的標識符。該標識符是在錢程創建時分配的一個正整數。在線程的整個生命周期中是唯一且無法改變的。
    • getName()/setName():這兩種方法允許你獲取或設置Thread對象的名稱。這個名稱是一個String對象,也可以在Thread類的構造函數中建立。
    • getPriority()/setPriority():你可以使用這兩種方法來獲取或設置Thread對象的優先級。
    • isDaemon()/setDaemon():這兩種方法允許你獲取或建立Thread對象的守護條件。
    • getState():該方法返回Thread對象的狀態
  • interrupt():中斷目標線程,給目標線程發送一個中斷信號,線程被打上中斷標記。
  • interrupted():判斷目標線程是否被中斷,但是將清除線程的中斷標記。
  • isinterrupted():判斷目標線程是否被中斷,不會清除中斷標記。
  • sleep(long ms):該方法將線程的執行暫停ms時間。
  • join():暫停線程的執行,直到調用該方法的線程執行結束為止。可以使用該方法等待另一個Thread對象結束。也可以理解為:當我們調用某個線程的這個方法時,這個方法會掛起調用線程,直到被調用線程結束執行,調用線程才會繼續執行。
  • setUncaughtExceptionHandler():當線程執行出現未校驗異常時,該方法用於建立未校驗異常的控制器。
  • currentThread():Thread類的靜態方法,返回實際執行該代碼的Thread對象。

Thread類常用方法以及join()方法示例:

public class ThreadCreatingForJoinByThread extends Thread{
    @Override
    public void run() {
        for (int i = 1 ; i<=10 ; i++ ){
            System.out.println(Thread.currentThread().getName()+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        //調用線程創建類創建線程

        //thread對象常用方法
        ThreadCreatingForJoinByThread thread = new ThreadCreatingForJoinByThread();
        System.out.println(thread.getName());
        System.out.println(thread.getId());
        System.out.println(thread.getContextClassLoader());
        System.out.println(thread.getPriority());
        System.out.println(thread.getState());
        System.out.println(thread.isDaemon());
        thread.start();
        System.out.println(thread.getState());
        //join()方法調用
        thread.join();
        System.out.println("main running finished");
    }
}
1.1.4 Callable

Callable 接口是一個與Runnable 接口非常相似的接口。一般用於Future模式。Callable 接口的主要特征如下:

  • 接口。有簡單類型參數,與call()方法的返回類型相對應。

  • 聲明了call()方法。執行器運行任務時,該方法會被執行器執行。它必須返回聲明中指定類型的對象。

  • call()方法可以拋出任何一種校驗異常。可以實現自己的執行器並重載afterExecute()方法來處理這些異常。

    ThreadCreatingByCallable threadCreatingByCallable = new ThreadCreatingByCallable();
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                    5,5,1, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10)
            ){
                //如果call方法執行過程中存在異常,則可以在此處處理
                @Override
                protected void afterExecute(Runnable r, Throwable t) {
                    System.out.println("mission has been execute successfully:"+t);
                }
            };
    
            Future<String> future = threadPoolExecutor.submit(threadCreatingByCallable);
            String result = future.get();
            System.out.println(result);
            //關閉線程池
            threadPoolExecutor.shutdown();
    

1.2 synchronized 關鍵字

1.2.1 鎖的對象

synchronized關鍵字“給某個對象加鎖”,示例代碼:

public class MyClass {
    //實例方法 (兩個方法相互等價)
    public synchronized void method1(){
        // ...
    }

    public void method2(){
        synchronized (this){
            // ...
        }
    }
    //靜態方法 (兩個方法相互等價)
    public static synchronized void method3(){
        // ...
    }
    
    public static void method4(){
        synchronized (MyClass.class){
            // ...
        }
    }
}

實例方法的鎖加在對象myClass上;靜態方法的鎖加在MyClass.class上。

1.2.2 鎖的本質

如果一份資源需要多個線程同時訪問,需要給該資源加鎖。加鎖之后,可以保證同一時間只能有一個線程訪問該資源。資源可以是一個變量、一個對象或一個文件等。

image-20211025154048676

鎖是一個“對象”,作用如下:

  1. 這個對象內部得有一個標志位(state變量),記錄自己有沒有被某個線程占用。最簡單的情況是這個state有0、1兩個取值,0表示沒有線程占用這個鎖,1表示有某個線程占用了這個鎖。
  2. 如果這個對象被某個線程占用,記錄這個線程的thread ID。
  3. 這個對象維護一個thread id list,記錄其他所有阻塞的、等待獲取拿這個鎖的線程。在當前線程釋放鎖之后從這個thread id list里面取一個線程喚醒。

要訪問的共享資源本身也是一個對象,例如前面的對象myClass,這兩個對象可以合成一個對象。代碼就變成synchronized(this) {…},要訪問的共享資源是對象a,鎖加在對象a上。當然,也可以另外新建一個對象,代碼變成synchronized(obj1) {…}。這個時候,訪問的共享資源是對象a,而鎖加在新建的對象obj1上。

資源和鎖合二為一,使得在Java里面,synchronized關鍵字可以加在任何對象的成員上面。這意味着,這個對象既是共享資源,同時也具備“鎖”的功能!

1.2.3 實現原理

修飾對象:在對象頭里,有一塊數據叫Mark Word。在64位機器上,Mark Word是8字節(64位)的,這64位中有2個重要字段:鎖標志位和占用該鎖的thread ID。因為不同版本的JVM實現,對象頭的數據結構會有各種差異。

修飾同步代碼塊:中是在對象頭中有一個monitor對象,對應着monitorenter和monitorexit指令,當執行enter指令時嘗試獲取monitor的持有權,獲取成功將計數器從0設為1,如果獲取失敗就阻塞等待別的線程釋放。

修飾方法:的話是ACC_SYNCHRONIZED標識,標明是一個同步方法。JVM通過這個標識才執行相應的同步調用。

1.2.4 優化

在jdk 1.6后,對synchronized鎖進行了優化。

偏向鎖:JVM認為只有某個線程才會執行同步代碼(沒有競爭環境),所以在MarkWord會直接記錄線程ID,只要線程來執行代碼就會對比線程ID是否相等,相等則直接獲取到鎖,不相等就CAS來嘗試修改當前的線程ID,如果CAS修改成功就繼續,如果失敗說明有競爭環境,升級為輕量級鎖。簡單來說就是:如果存在競爭環境,則升級為輕量級鎖。

輕量級鎖:輕量級鎖是相對於重量級鎖而言,輕量級鎖不需要申請互斥量,只需要將markwork中的部分字節CAS更新指向線程的id,如果更新成功則表示已經成功的獲取了鎖,否則說明已經有線程獲取了輕量級鎖,發生了鎖競爭,輕量級鎖開始自旋。

在jdk1.6之前,設置了自旋鎖自旋次數為10次。1.6及之后,優化為自適應自旋鎖。可以根據加鎖的代碼來決定要自選幾次. 如果自旋超過一定次數,或者此時有第三個線程來競爭該鎖時,鎖膨脹為重量級鎖。

重量級鎖:Jvm每次從隊列中取出一個線程來用於鎖競爭候選者即競爭線程.但是並發情況下,尾部list會被大量的並發線程的訪問為了降低競爭,提高獲取線程的速度,JVM將競爭的list拆為了兩份,獲取競爭線程時只從頭部獲取,而新進入的競爭線程則被放到尾部.提高了競爭時的效率.當Owner線程在unlock時會將尾部線程的部分線程遷移到頭部線程中,並且制定頭部線程的某一個線程作為競爭線程,但是並沒有直接將鎖交給競爭線程,而是讓競爭線程自己來獲取鎖,這樣做雖然會犧牲公平性,但是會極大的提升系統的吞吐量。

synchronized是非公平鎖.當線程在進入尾部隊列之前,會嘗試着先自旋獲取鎖,如果獲取失敗才選擇進入尾部隊列。之后的操作參考重量級鎖。

公平鎖底層為將線程放入一個先進先出的隊列中,按照順序一次獲取資源。


1.3 wait與notify

wait方法會讓線程進入等待隊列,若要執行wait方法,線程必須持有鎖,但如果線程進入等待隊列,便會釋放實例的鎖。

notify()方法會將等待隊列中的一個線程取出。那么在等待隊列中的那個線程便會被選中喚醒,然后退出等待隊列。這里需要注意的是,在執行notify喚醒的線程並不會在執行notify的一瞬間重新運行。因為在執行notify的那一瞬間,執行notify的線程還持着鎖,所以其他線程還無法獲取這個實例的鎖。

notifyAll()方法會將等待隊列中的所有線程都取出來,所有等待的線程都會被喚醒。有意思的是,在執行notifyAll()方法時,誰持着鎖呢?當然是執行notifyAll()的線程正持着鎖,因此,喚醒的線程雖然都退出了等待隊列,但都在等待獲取鎖,處於阻塞狀態,只有在執行notifyAll之后的線程釋放鎖以后,其中的一個幸運兒才能夠實際運行。

在調用之前,先判定該線程是否持有該鎖。

1.3.1 生產者-消費者模型

image-20211025162411308

一個內存隊列,多個生產者線程往內存隊列中放數據;多個消費者線程從內存隊列中取數據。要實現這樣一個編程模型,需要做下面幾件事情:

  • 內存隊列本身要加鎖,才能實現線程安全。
  • 阻塞。當內存隊列滿了,生產者放不進去時,會被阻塞;當內存隊列是空的時候,消費者無事可做,會被阻塞。
  • 雙向通知。消費者被阻塞之后,生產者放入新數據,要notify()消費者;反之,生產者被阻塞之后,消費者消費了數據,要notify()生產者。

第1件事情必須要做,第2件和第3件事情不一定要做。例如,可以采取一個簡單的辦法,生產者放不進去之后,睡眠幾百毫秒再重試,消費者取不到數據之后,睡眠幾百毫秒再重試。但這個辦法效率低下,也不實時。所以,我們只討論如何阻塞、如何通知的問題。

如何阻塞?

辦法1:線程自己阻塞自己,也就是生產者、消費者線程各自調用wait()和notify()。

辦法2:用一個阻塞隊列,當取不到或者放不進去數據的時候,入隊/出隊函數本身就是阻塞的。

如何雙向通知?

辦法1:wait()與notify()機制。

辦法2:Condition機制。

單個生產者單個消費者線程的情形:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MessageQueue messageQueue = new MessageQueue();
        new ProducerThread(messageQueue).start();
        new ConsumerThread(messageQueue).start();
        Thread.sleep(2000);
        System.exit(0);
    }
}
public class ProducerThread extends Thread{
    private final MessageQueue messageQueue;
    private final Random random = new Random();
    private int index = 0;
    public ProducerThread(MessageQueue messageQueue) {
        this.messageQueue = messageQueue;
    }


    @Override
    public void run() {
        while (true){
            String message = String.valueOf(index++);
            messageQueue.put(message);
            System.out.println("生產數據: "+message);
            try {
                Thread.sleep(random.nextInt(100));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
public class ConsumerThread extends Thread{
    private final MessageQueue messageQueue;
    private final Random random = new Random();
    public ConsumerThread(MessageQueue messageQueue) {
        this.messageQueue = messageQueue;
    }

    @Override
    public void run() {
        while (true){
            String result = null;
            try {
                result = messageQueue.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("consumer data is "+result);
            try {
                Thread.sleep(random.nextInt(100));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
@SuppressWarnings("all")
public class MessageQueue {
    private String[] data = new String[10];
    //下一條存儲記錄的下標
    private int putIndex = 0;
    //下一條要獲取的記錄的下標
    private int getIndex = 0;
    //data中元素的個數
    private int size = 0;

    public synchronized void put(String element){
        if (size == data.length){
            try{
                //阻塞,等待
                wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        data[putIndex++] = element;
        //喚醒消費者
        notify();
        size++;
        if (putIndex == data.length){
            putIndex = 0;
        }
    }

    public synchronized String get() throws InterruptedException {
        if (size == 0){
            wait();
        }
        String result = data[getIndex++];
        if(getIndex == data.length) getIndex=0;
        size--;
        //喚醒生產者
        notify();
        return result;
    }
}

多個生產者多個消費者的情形:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        //多個生產者和多個消費者
        MessageQueue2 messageQueue = new MessageQueue2();
        for (int i = 0 ; i < 3 ; i++ ){
            new ConsumerThread(messageQueue).start();
        }
        for (int i = 0 ; i<5 ; i++ ){
            new ProducerThread(messageQueue).start();
        }
    }
}
@SuppressWarnings("all")
public class MessageQueue2 extends MessageQueue{
    private String[] data = new String[10];
    //下一條存儲記錄的下標
    private int putIndex = 0;
    //下一條要獲取的記錄的下標
    private int getIndex = 0;
    //data中元素的個數
    private int size = 0;

    private void commonPut(String element){
        data[putIndex++] = element;
        //喚醒消費者
        notify();
        size++;
        if (putIndex == data.length){
            putIndex = 0;
        }
    }

    @Override
    public synchronized void put(String element) {
        if (size == data.length){
            try{
                //阻塞,等待
                wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            //利用迭代,重新獲取共享鎖
            put(element);
        }else {
            commonPut(element);
        }

    }

    private String commonGet(){
        String result = data[getIndex++];
        if(getIndex == data.length) getIndex=0;
        size--;
        //喚醒生產者
        notify();
        return result;
    }

    @Override
    public synchronized String get() throws InterruptedException {
        if (size == 0){
            wait();
            //利用迭代,重新獲取共享鎖
            return get();
        }else{
            return commonGet();
        }

    }
}
1.3.2 為什么必須要和synchronized一起使用

在Java里面,wait()和notify()是Object的成員函數,是基礎中的基礎。為什么Java要把wait()和notify()放在如此基礎的類里面,而不是作為像Thread一類的成員函數,或者其他類的成員函數呢?

兩個線程之間要通信,對於同一個對象來說,一個線程調用該對象的wait(),另一個線程調用該對象的notify(),這兩個操作需要協調,所以該對象本身就需要同步!所以,在調用wait()、notify()之前,要先通過synchronized關鍵字同步給對象,也就是給該對象加鎖。並且調用這些方法之前需要確定是否獲得了該鎖,所以需要和synchronized關鍵字一起使用。

synchronized關鍵字可以加在任何對象的實例方法上面,任何對象都可能成為鎖。因此,wait()和notify()只能放在Object里面了。

1.3.3 為什么wait()的時候必須要釋放鎖

當線程A進入synchronized(obj1)中之后,也就是對obj1上了鎖。此時,調用wait()進入阻塞狀態,一直不能退出synchronized代碼塊;那么,線程B永遠無法進入synchronized(obj1)同步塊里,永遠沒有機會調用notify(),發生死鎖。

在wait()的內部,會先釋放鎖obj1,然后進入阻塞狀態,之后,它被另外一個線程用notify()喚醒,重新獲取鎖!其次,wait()調用完成后,執行后面的業務邏輯代碼,然后退出synchronized同步塊,再次釋放鎖。

wait(){
	//釋放鎖
    //阻塞,等待被其他線程notify
    //重新獲取鎖
}

如此則可以避免死鎖。

1.3.4 wait()與notify()的問題

生產者在通知消費者的同時,也通知了其他的生產者;消費者在通知生產者的同時,也通知了其他消費者。原因在於wait()和notify()所作用的對象和synchronized所作用的對象是同一個,只能有一個對象,無法區分隊列空和列隊滿兩個條件。這正是Condition要解決的問題


1.4 InterruptedException和interrupt()方法

1.4.1 Interrupted異常

什么情況下會拋出Interrupted異常

只有那些聲明了會拋出InterruptedException的函數才會拋出異常,也就是下面這些常用的函數:

public static native void sleep(long millis) throws InterruptedException {...} 
public final void wait() throws InterruptedException {...} public final void join() throws InterruptedException {...}
1.4.2 輕量級鎖阻塞與重量級阻塞

能夠被中斷的阻塞稱為輕量級阻塞,對應的線程狀態是WAITING或者TIMED_WAITING;而像synchronized 這種不能被中斷的阻塞稱為重量級阻塞,對應的狀態是 BLOCKED。如圖所示:調用不同的方法后,一個線程的狀態遷移過程。

image-20211025190129064

初始線程處於NEW狀態,調用start()開始執行后,進入RUNNING或者READY狀態。如果沒有調用任何的阻塞函數,線程只會在RUNNING和READY之間切換,也就是系統的時間片調度。這兩種狀態的切換是操作系統完成的,除非手動調用yield()函數,放棄對CPU的占用。

一旦調用了圖中的任何阻塞函數,線程就會進入WAITING或TIMED_WAITING狀態,兩者的區別只是前者為無限期阻塞,后者則傳入了一個時間參數,阻塞一個有限的時間。如果使用了synchronized關鍵字或者synchronized塊,則會進入BLOCKED狀態。

不太常見的阻塞/喚醒函數,LockSupport.park()/unpark()。這對函數非常關鍵,Concurrent包中Lock的實現即依賴這一對操作原語。

因此thread.interrupted()的精確含義是“喚醒輕量級阻塞”,而不是字面意思“中斷一個線程”。

thread.isInterrupted()Thread.interrupted()的區別

因為 thread.interrupted()相當於給線程發送了一個喚醒的信號,所以如果線程此時恰好處於WAITING或者TIMED_WAITING狀態,就會拋出一個InterruptedException,並且線程被喚醒。而如果線程此時並沒有被阻塞,則線程什么都不會做。但在后續,線程可以判斷自己是否收到過其他線程發來的中斷信號,然后做一些對應的處理。

這兩個方法都是線程用來判斷自己是否收到過中斷信號的,前者是實例方法,后者是靜態方法。二者的區別在於,前者只是讀取中斷狀態,不修改狀態;后者不僅讀取中斷狀態,還會重置中斷標志位。


1.5 線程的優雅關閉

1.5.1 stop與destory函數

線程是“一段運行中的代碼”,一個運行中的方法。運行到一半的線程能否強制殺死?

不能。在Java中,有stop()、destory()等方法,但這些方法官方明確不建議使用,並在jdk 11中廢除。原因很簡單,如果強制殺死線程,則線程中所使用的資源,例如文件描述符、網絡連接等無法正常關閉。

因此,一個線程一旦運行起來,不要強行關閉,合理的做法是讓其運行完(也就是方法執行完畢),干凈地釋放掉所有資源,然后退出。如果是一個不斷循環運行的線程,就需要用到線程間的通信機制,讓主線程通知其退出。

1.5.2 守護線程
public class Main {
    public static void main(String[] args) {
        MyDaemonThread myDaemonThread = new MyDaemonThread();
        myDaemonThread.setDaemon(true);
        myDaemonThread.start();
        new MyThread().start();

    }
}
public class MyDaemonThread extends Thread{
    @Override
    public void run() {
        while (true){
            try{
                System.out.println("waking ...");
                Thread.sleep(500);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("normal thread running");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

對於上面的程序,在thread.start()前面加一行代碼thread.setDaemon(true)。當main(...)函數退出后,線程thread就會退出,整個進程也會退出。

當在一個JVM進程里面開多個線程時,這些線程被分成兩類:守護線程和非守護線程。默認都是非守護線程。

在Java中有一個規定:當所有的非守護線程退出后,整個JVM進程就會退出。意思就是守護線程“不算作數”,守護線程不影響整個 JVM 進程的退出。

例如,垃圾回收線程就是守護線程,它們在后台默默工作,當開發者的所有前台線程(非守護線程)都退出之后,整個JVM進程就退出了。

1.5.3 設置關閉的標志位

開發中一般通過設置標志位的方式,停止循環運行的線程。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        StopFlagThread stopFlagThread = new StopFlagThread();
        stopFlagThread.start();
        Thread.sleep(500);
        stopFlagThread.changeRunning();
        stopFlagThread.join();
    }
}
public class StopFlagThread extends Thread{
    private boolean running = true;

    @Override
    public void run() {
        while(running){
            System.out.println("thread is running");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void changeRunning(){
        this.running = false;
    }
}

但上面的代碼有一個問題:如果MyThread t在while循環中阻塞在某個地方,例如里面調用了object.wait()函數,那它可能永遠沒有機會再執行 while( !stopped)代碼,也就一直無法退出循環。

此時,就要用到InterruptedException()與interrupt()函數。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM