1. 程式人生 > >併發程式設計之執行緒建立到銷燬、常用API

併發程式設計之執行緒建立到銷燬、常用API

  在前面一篇介紹了執行緒的生命週期【併發程式設計之多執行緒概念 】,在本篇將正式介紹如何建立、中斷執行緒,以及執行緒是如何銷燬的。最後,我們會講解一些常見的執行緒API。

 

執行緒建立

  Java 5 以前,實現執行緒有兩種方式:擴充套件java.lang.Thread類,實現java.lang.Runnable介面。這兩種方式都是都是直接建立執行緒,而每次new Thread都會消耗比較大的資源,導致每次新建物件時效能差;而且執行緒缺乏統一管理,可能無限制新建執行緒,相互之間競爭,很可能佔用過多系統資源導致宕機或OOM。同時,new Thread的執行緒缺乏更多功能,如定時執行、定期執行、執行緒中斷。

  Java 5開始,JDK提供了4中執行緒池(newFixedThreadPool、newCachedThreadPool、newScheduledThreadPool、newSingleThreadExecutor)來獲取執行緒。這樣做的好處是:可以重用已經存在的執行緒,減少物件建立、消亡的開銷,效能佳;而且執行緒池可有效控制最大併發執行緒數,提高系統資源的使用率,同時避免過多資源競爭,避免堵塞。通過特定的執行緒池也可以實現定時執行、定期執行、單執行緒、併發數控制等功能。

  建立執行緒的程式碼實現

  • 擴充套件java.lang.Thread類
    • 自定義一個類繼承java.lang.Thread
    • 重寫Thread的run(),把自定義執行緒的任務定義在run方法上
    • 例項化自定義的Thread物件,並呼叫start()啟動執行緒
//1.自定義一個類繼承Thread類
public class ExThread extends Thread {
    //2.重寫run()
    @Override
    public void run() {
       for (int i = 0; i < 100; i++) {
           System.out.println(Thread.currentThread().getName()+”:”+i);
       }
    }

    public static void main(String[] args) {
        //3.建立Thread子類物件
        ExThread exThread = new ExThread();
        //4.呼叫start方法啟動自定義執行緒
        exThread.start();
    }
}

 

  • 實現java.lang.Runnable介面
    • 自定義一個類實現Runnable介面
    • 實現Runnable介面中的run(),把自定義執行緒的任務定義在run方法上
    • 建立Runnable實現類的物件
    • 建立Thread物件,並且把Runnable實現類的物件作為引數傳遞
    • 呼叫Thread物件的start()啟動自定義執行緒
//1.自定義一個類實現Runnable介面
public class ImThread implements Runnable{
    //2.實現run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+”:”+i);
        }  
    }
   
    public static void main(String[] args) {
        //3.建立Runnable實現類物件
        ImThread imThread = new ImThread();
        //4.建立Thread物件
        Thread thread = new Thread(imThread);
        //5.呼叫start()開啟執行緒
        thread.start();
    }
}

  

  • newFixedThreadPool

    建立一個固定執行緒數的執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CreateThreadByFixedPool {

    /**
     * Cover Runnable.run()
     */
    private static void run(){
        System.out.println(Thread.currentThread().getName()+" is running...");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        ExecutorService pool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            pool.execute(CreateThreadByFixedPool::run);
        }
    }
}

 

  • newCachedThreadPool

    建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒.

    執行緒池的容量為無限大,當執行第二個任務時第一個任務已經完成,會複用執行第一個任務的執行緒,而不用每次新建執行緒。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CreateThreadByCachedPool {

    public static void main(String[] args) {

        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            pool.execute(() -> System.out.println(Thread.currentThread().getName()+" is running..."));
        }
    }
}

 

  • newScheduledThreadPool

    建立一個固定執行緒數的執行緒池,支援定時及週期性任務執行。

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class CreateThreadByScheduledPool {

    public static void main(String[] args) {

        ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
//delay 2s excute. pool.schedule(() -> System.out.println(Thread.currentThread().getName()+" delays 2s "), 2, TimeUnit.SECONDS);
//delay 2s and every 3s excute. pool.scheduleAtFixedRate(() -> System.out.println(Thread.currentThread().getName()+" delays 2s every 3s execte"), 2, 3, TimeUnit.SECONDS); } }

 

  • newSingleThreadExecutor

    建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。

public class CreateThreadBySingleThreadPool {

    public static void main(String[] args) {

        ExecutorService pool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            pool.execute(() ->{
                System.out.println(String.format("The thread %d (%s) is running...",
                        index,Thread.currentThread().getName()));
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

 

  Thread負責執行緒本身相關的職責和控制,Runnable負責邏輯業務。

  在實現自定義執行緒時,推薦使用Runnable介面,因為其具有以下幾個優點:
  • Java是單繼承多實現的,實現介面有利於程式拓展
  • 實現Runnable介面可為多個執行緒共享run() 【繼承Thread類,重寫run()只能被該執行緒使用】
  • 不同執行緒使用同一個Runnable,不用共享資源

 

執行緒中斷

    interrupt()方法可以用來請求終止執行緒。
  • 當對一個執行緒呼叫interrupt方法時,執行緒的中斷狀態(boolean標誌)會被置位。
  • 判斷當前執行緒是否中斷,可使用Thread.currentThread.isInterrupt()
  • 中斷並不是強制終止執行緒,中斷執行緒只是引起當前執行緒的注意,由它自己決定是否響應中斷。【有些(非常重要的)執行緒會處理完異常後繼續執行,並不理會中斷;但是更加普遍的情況是:執行緒簡單的將中斷作為一個終止請求。】

 

執行緒銷燬

    執行緒銷燬的幾種情景:
  • 執行緒結束,自行關閉
  • 異常退出
  • 通過interrupt()修改isInterrupt()標誌進行結束
public static void main(String[] args) throws InterruptedException {
        Thread t  = new Thread(){
            @Override
            public void run() {
                System.out.println("I will start work.");
                while(!isInterrupted()){
                    System.out.println("working....");
                }
                System.out.println("I will exit.");
            }
        };
        t.start();
        TimeUnit.MICROSECONDS.sleep(100);
        System.out.println("System will exit.");
        t.interrupt(); 
    }
  • 使用volatile修飾的開關flag關閉執行緒(因為執行緒的interrupt標識很可能被擦除)【chapter04.FlagThreadExit】
public class FlagThreadExit {

    static class MyTask extends Thread{
        
        private volatile boolean closed = false;
        
        @Override
        public void run() {
            System.out.println("I will start work.");
            while(!closed && !isInterrupted()){
                System.out.println("working....");
            }
            System.out.println("I will exit.");
        }
        
        public void closed(){
            this.closed = true;
            this.interrupt();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        MyTask task = new MyTask();
        task.start();
        TimeUnit.MICROSECONDS.sleep(100);
        System.out.println("System will exit.");
        task.closed();
    }
}

 

多執行緒API

方法 返回值 作用  
yield() static void 暫停當前正在執行的執行緒物件,並執行其他執行緒。 只有優先順序大於等於該執行緒優先順序的執行緒(包括該執行緒)才有機會被執行 釋放CPU資源,不會放棄monitor鎖
sleep() static void 使當前執行緒休眠,其它任意執行緒都有執行的機會 釋放CPU資源,不會放棄monitor鎖
wait() void 使當前執行緒等待 Object的方法
interrupt() void 中斷執行緒 可中斷方法
interrupted() static boolean 判斷當前執行緒是否中斷  
isInterrupted() boolean 測試執行緒是否已經中斷  
join() void 線上程A內,join執行緒B,執行緒A會進入BLOCKED狀態,直到執行緒B結束生命週期或者執行緒A的BLOCKED狀態被另外的執行緒中斷 可中斷方法
 
  • 可中斷方法被打斷後會收到中斷異常InterruptedException.
  • yield()和sleep()的比較
    • 都是Thread類的靜態方法
    • 都會使當前處於執行狀態的執行緒放棄CPU
    • yield只會讓位給相同或更高優先順序的執行緒,sleep讓位給所有的執行緒
    • 當執行緒執行了sleep方法後,將轉到阻塞狀態,而執行了yield方法之後,則轉到就緒狀態;
    • sleep方法有可能丟擲異常,而yield則沒有【sleep是可中斷方法,建議使用sleep】
  • sleep和wait的比較
    • wait和sleep方法都可以使執行緒進入阻塞狀態
    • wait和sleep都是可中斷方法
    • wait使Object的方法,sleep是Thread的方法
    • wait必須在同步程式碼中執行,而sleep不需要
    • 在同步程式碼中,sleep不會釋放monitor鎖,而wait方法會釋放monitor鎖
    • sleep方法在短暫休眠後主動退出阻塞,而(沒有指定時間的)wait方法則需要被其它執行緒notify或interrupt才會退出阻塞

 

wait使用

  • 必須在同步當法中使用wait和notify方法(wait和notify的前提是必須持有同步方法的monitor的所有權)
  • 同步程式碼的monitor必須與執行wait和notify方法的物件一致
public class WaitDemo {

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        pool.execute(() -> {
            synchronized (WaitDemo.class){
                System.out.println("Enter Thread1...");
                System.out.println(Thread.currentThread().getName()+" is waiting...");
                try {
                    WaitDemo.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread1 is going...");
                System.out.println("Shut down Thread1.");
            }
        });

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        pool.execute(() ->{
            synchronized (WaitDemo.class) {
                System.out.println("Enter Thread2...");
                System.out.println(Thread.currentThread().getName()+" is notifying other thread...");
                WaitDemo.class.notify();
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread2 is going...");
                System.out.println("Shut down Thread2.");
            }

        });
    }

}

 

 

補充

  • 棄用stop()和suspend()的原因
                stop()用來終止一個執行緒,但是不安全的; stop()會終止當前執行緒的所有未結束的方法,包括run()。當前執行緒被終止,立即釋放被它鎖住的鎖。這會導致物件處於不一致的狀態。【在轉賬過程中,可能錢剛轉出,還未轉入另一個賬戶,執行緒就被中斷。導致物件被破壞,資料出錯,其他執行緒得到的將會是錯誤的物件資料】                 suspend()用來阻塞一個執行緒,直至另一個執行緒呼叫resume()。suspend()會經常導致死鎖。 呼叫suspend()的時候,目標執行緒會被掛起,但是仍然會持有之前獲得的鎖定。此時,其他執行緒都不能訪問被鎖定的資源。如果呼叫suspend()的執行緒試圖獲取同一個鎖,那麼執行緒死鎖(被掛起的執行緒等著恢復,而將其掛起的執行緒等待鎖資源)
  • suspend()的替代方案
                應在自己的Thread類中置入一個標誌,指出執行緒應該活動還是掛起。若標誌指出執行緒應該掛起,便用wait()命其進入等待狀態。若標誌指出執行緒應當恢復,則用一個notify()重新啟動執行緒。  
  •  在實際開發中,呼叫start()啟動執行緒的方法已不再推薦。應該從執行機制上減少需要並行執行的任務數量。如果有很多工,要為每個任務建立一個獨立執行緒的程式設計所付出的代價太大了。可以使用執行緒池來解決這個問題。
  •     執行緒資訊檢視工具:JDK自帶的Jconsole
&nbs