創建線程的4種方式


創建一個空線程

public class EmptyThreadDemo {
    public static void main(String[] args) {
        //使用Thread類創建和啟動線程
        Thread thread = new Thread();
        Print.tco("線程名稱:"+thread.getName());
        Print.tco("線程Id:"+thread.getId());
        Print.tco("線程狀態:"+thread.getState());
        Print.tco("線程優先級"+thread.getPriority());
        Print.tco(Thread.currentThread().getName()+"運行結束");
        thread.start();
    }
}

首先創建一個空線程,通過該線程在堆內存的引用地址獲取到該線程的名稱,ID,狀態,優先級。
此時線程並沒有啟動,其線程狀態是New。然后用thread.start()啟動該線程,線程會去執行用戶代碼邏輯塊,邏輯塊的入口是run()方法,我們可以看看run方法的源碼:

    public void run() {
        if (target != null) {
            target.run();
        }
    }

target是Thread類中的一個實例屬性,它是這樣定義的。

private Runnable target;

它是一個Runnable類型的屬性,Runnable是一個接口類,里面有定義一個方法便是run(),這也意味着在新線程啟動后,會以run()方法為代碼邏輯塊入口執行用戶代碼,而內部進一步調用了target目標實例執行類的run()方法,而我們並沒有去實現這個方法,所以什么都沒有執行,該線程也稱空線程結束了,整個JVM進程也結束。

工具類

這些工具類的方法后續會用上,便於編碼。

public class ThreadUtil{
    
    public static String getCurThreadName(){
        return Thread.currentThread().getName();
    }
    
    public static void sleepMillSeconds(int millsecond){
        LockSupport.parkNanos(millsecond*100L*100L);
    }

    public static void execute(String cfo){
        synchronized(System.out){
            System.out.println(cfo);
        }
    }
}

public class Print{
    public static void tco(Object s){
        String cfo = "["+ThreadUtil.getCurThreadName()+"]"+s;
        ThreadUtil.execute(cfo);
    }
}

通過繼承Thread類的方式創建線程目標類

前面的例子向我們說明了線程start之后,如何執行用戶定義的線程代碼邏輯。因此我們想要線程去執行我們的代碼就主要有兩種方式:

  • 繼承Thread類去重寫run()方法。
  • 實現Runnable接口的run()方法,並將實現好的接口的實現類以構造參數的形式傳入Thread的target實例屬性中。

接下來我們來以代碼詮釋第一種方式

public class CreateDemo{
    private static final int MAX = 5;
    private static int treadNo = 1;
    static class DemoThread extends Thread{
          public DemoThread(){
              //調用父類的構造方法
              super("DemoThread-"+treadNo++);
        }
          @Override
          public void run(){
              for(int i = 0;i < MAX;i++){
                  Print.tco(getName()+", 輪次為:"+i);
            }
              Print.tco(getName()+" 運行結束.");
        }
    
    public static void main(String[] args){

          Thread thread = null;
          for(int i = 0;i < 2;i++){
              thread = new DemoThread();
              thread.start();
            }
          Print.tco(getCurThreadName()+" 運行結束.");
        }
    
    }
}

例子中,我們建了一個靜態內部類去繼承Thread類,調用其帶String的構造方法構造該實現類,重寫Thread類的run()方法,添加屬於我們的邏輯代碼塊。

這里的代碼邏輯是循環5次,每次輸出當前運行線程的名字以及輪次。

至於為什么是靜態內部類,主要是為了方便調用外部類的屬性,而如果該內部類不是靜態的話還需要new外部類才new當前內部類。當然將其寫為外部類,依然不影響后面的輸出結果。

通過實現Runnable接口創建target執行目標類來創建線程目標類

在我們用代碼演示之前,我們可以來看一下Thread的構造方法有哪些?

圖中我們可以看到,Thread給我們提供了樣式豐富的構造方法,其中有Runnable的也居多。因此我們可以以Runnable為構造參數的形式給Thread實例類傳入target實例屬性。

構造參數String類型實則為所創建線程的名稱。

接着我們來用代碼真正實現

public class CreateDemo2 {

    public static final int MAX = 5;
    static int threadNo = 1;
    static class RunTarget implements Runnable{
        @Override
        public void run() {
            String name = getCurThreadName();
            for (int i = 0; i < MAX; i++) {
                Print.tco(name+",輪次:"+i);
            }
            Print.tco(name+" 運行結束.");
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            Thread thread = new Thread(new RunTarget(),"RunnableThread-"+threadNo++);
            thread.start();
        }
        Print.tco(getCurThreadName()+",運行完畢.");
    }

}

這里我們可以看到我們實現了Rnnable接口的run()方法,將這個target目標執行類以構造參數的形式傳入了我們所創建Thread實例類,當start()的時候,JVM就會啟動線程運行用戶邏輯代碼,也就是我們實現Runnable接口run()方法的邏輯代碼。

通過匿名類來創建Runnable線程目標類

通過優雅的實現方式來創建Runnable線程目標類

public class CreateDemo2_2 {
    public static final int MAX = 5;
    static int threadNo = 1;

    public static void main(String[] args) {
        Thread thread = null;
        for (int i = 0; i < 2; i++) {
            thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < MAX; j++) {
                        Print.tco(getCurThreadName()+",輪次:"+j);
                    }
                    Print.tco(getCurThreadName()+" 運行結束.");
                }
            },"RunnableThread-"+threadNo++);
            thread.start();
        }
        Print.tco(getCurThreadName()+" 運行結束.");
    }
}

通過Lambda表達式創建Runnable線程目標類

通過優雅的實現方式來創建Runnable線程目標類

public class CreateDemo2_3{
  
    private static final int MAX = 5;
    private static int treadNo = 1;
    
    public static void main(String[] args){
         
        Thread tread = null;
        for(int i = 0;i < 2;i++){
             tread = new Thread(()->{
                  for(int j = 0;j < MAX;j++){
                      Print.tco(getCurThreadName()+",輪次為:"+j);
                }
                  Print.tco(getCurThreadName()+" 運行結束.");
            },"RunnableThread-"+treadNo++);
            
            thread.start();
        }
        
        Print.tco(getCurThreadName()+" 運行結束.");
    }    

}


繼承Thread類來創建線程目標類和通過實現Runnable接口創建線程目標類有什么不同嗎?

  • 第一種方式創建線程目標類,由於每次創建類的內存地址都是不一樣的,因此每個數據資源的內存地址都是不一樣的,所以每個線程目標類都有其唯一的數據資源,在執行線程時,只是對着自己的數據資源進行業務處理,不會影響其他線程的數據資源。
    -- 第一種方式創建線程目標類的優點:由於是繼承了Thread類,其子類便享有父類的getName()、getID()、getStatus()等方法,可以很輕松的訪問當前線程的各種信息狀態和對當前線程進行操作。
    -- 第一種方式的缺點: 由於一個類僅僅只能繼承一個父類(不包括接口),所以在當前類繼承了其他父類時,便用不了以繼承Thread的方式來創建線程目標類了。
  • 第二種方式以實現Runnable接口的方式得到target目標類,在用這個target目標類以構造參數的形式傳入Thread實例中,得以創建真正的線程。這里我們可以發現多個線程用的target目標執行實現類都是用的同一個引用地址,也即多個線程使用的數據資源都是同一個。也就是說使用實現Runnable接口來創建線程目標類,其多個線程業務邏輯並行處理同一個數據資源。
    -- 第二種方式創建線程目標類的優點:更好地體現了面向對象的設計思想。通過實現Runnable接口的方式設計多個target執行目標執行類可以更加方便,清晰地執行邏輯和數據存儲的分離。
    -- 第二種方式創建線程目標類的缺點:由於數據資源是被多個線程共享的,所以對數據資源做共享操作的時候會出現線程安全的問題。而且由於target目標類不是繼承Thread的,所以要得到當前線程的信息,只能以Thread.currentThread()來獲取當前在cpu時間片運行的線程來獲取信息。

通過創建FutureTask和實現Callable接口來創建線程目標類

前面的兩種方式其實都有一個共同的缺陷:由於run()方法的返回值類型是void類型,我們在線程異步執行完成之后是拿不到線程執行完成后的結果,很多時候我們想要了解線程異步執行的時候的狀態,結果,前面的兩種方式並不足以滿足我們的需求。

於是為了解決這個問題,在JDK1.5版本提供了一種新的多線程創建方法:便是使用Callable接口和FutureTask相結合來創建線程目標類。

首先我們先從Callable接口的源碼定義來認識一下它

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

我們從源碼認識到,Callable是一個函數式接口,其中有唯一方法為call()方法,其方法的返回值是Callable接口的泛型形參,方法還有一個Exception的異常聲明,容許方法的實現可以有異常拋出,且不做捕獲。

不難看出call()方法的功能比run()方法要豐富多了,它多了返回值,對了異常的聲明,功能十分強大。但是其能代替Runnable實例作為Thread的target執行類嗎?顯然這是不能的,上文我們提到target實例的屬性是Runnable,而其是Callable類型的,所以並不能作為target來運行。

那么我們要通過何種方式來讓線程啟動的時候,進入run()方法里面運行的是call()方法里的代碼邏輯塊呢?

接下來我們來認識一下RunnableFuture接口

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

我們可以看到RunnableFuture接口繼承了Runnable接口,使其實現類可以作為target目標類,同時它還繼承了Future接口,那么這個接口賦予了RunnableFuture什么接口方法呢?我們來查看一下Future接口。

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

通過查看其實現我們可以知道Future接口賦予了RunnableFuture五個接口方法,分別是:

  • cancel():取消異步任務的執行。
  • isCancelled():查看異步任務是否取消了。
  • isDone():查看異步任務是否執行完成。
  • get():阻塞性獲取異步任務的執行的結果。
  • get(long timeout,TimeUnit unit):限時的阻塞性獲取異步任務的執行的結果。

那么此時RunnableFuture接口就擁有了可以作為target實現類,可以獲取線程的執行結果,執行狀態的方法。那么最后就要實現該接口了,JDK已經幫我們實現好了,其名字叫做FutureTask

此時的FutureTask既能作為一個Runnable類型的target執行目標直接被Thread執行,有擁有着可以獲取Callable執行結果,執行狀態的能力。那么FutureTask是如何和Callable聯系上的呢?我們可以查看FutureTask,其中有一個實例屬性:
private Callable<V> callable;
其屬性是用來保存並發執行的Callable類型的任務的,我們再來看看Future實現run方法的內部代碼

    public void run() {
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

    protected void set(V v) {
        if (STATE.compareAndSet(this, NEW, COMPLETING)) {
            outcome = v;
            STATE.setRelease(this, NORMAL); // final state
            finishCompletion();
        }
    }

此時我們恍然大悟,在run()方法中調用了Callable的call()方法,並將方法的返回值"set"起來了,那么它保存在哪呢?它是保存在屬性outcome中,方便get()的獲取。

最終我們可以捋一下Callable接口和FaskTask是怎么創建線程目標類的。

於是該線程的執行流程便是:

  • 首先線程start(),JVM會啟動線程執行用戶代碼邏輯塊,代碼邏輯塊的入口是run(),而run方法中調用了target執行類的run()方法,此時這個target便是我們已構造參數形式傳入到Thread中的FutureTask,調用其run()方法,里面又調用了callable.call()方法,執行結果會保存在屬性outcome中,靜待調用線程調用。

我們用一個例子簡單展現一下

public class CreateDemo3 {
    public static final int MAX_TURN = 5;
    public static final int COMPUTE_TIMES = 100000000;

    static class ReturnableTask implements Callable<Long>{
        @Override
        public Long call() throws Exception {
            long startTime = System.currentTimeMillis();
            Print.tco(getCurThreadName()+" 線程開始運行.");
            Thread.sleep(1000);
            for (int i = 0; i < COMPUTE_TIMES; i++) {
                int j = i * 10000;
            }
            long used = System.currentTimeMillis()-startTime;
            Print.tco(getCurThreadName()+" 線程運行結束.");
            return used;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReturnableTask task = new ReturnableTask();
        FutureTask<Long> futureTask = new FutureTask<Long>(task);
        Thread thread = new Thread(futureTask,"returnableThread");
        thread.start();
        Thread.sleep(500);
        System.out.println(getCurThreadName()+" 讓子彈飛一會兒");
        System.out.println(getCurThreadName()+" 做一點自己的事情");
        for (int i = 0; i < COMPUTE_TIMES; i++) {
            int j = i * 10000;
        }
        System.out.println(ThreadUtil.getCurThreadName()+" 獲取並發任務執行結果.");
        try {
            System.out.println(thread.getName()+" 線程占用時間:"+futureTask.get());
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(ThreadUtil.getCurThreadName()+" 運行結束.");
    }
}

通過線程池來創建線程目標類

前面的許多例子所創建的Thread實例類都在執行完成之后銷毀了,這些線程實例都是不可復用的。實際上線程的創建,銷毀在時間成本上,資源成本(因為線程創建需要JVM分配棧內存等)上耗費都很高,在高並發的場景下,斷然不能頻繁的進行線程的創建和銷毀,需要的是線程的可復用性。此時需要的是池技術,JAVA中提供了一個靜態工廠來創建不同的線程池,該靜態工廠為Executors工廠類。

接下來我們使用一個例子來實現線程池,以及線程的調度執行

/**
 * 第四種方式創建線程類:通過線程池創建線程
 */
public class CreateDemo4 {

    public static final int MAX = 5;

    //創建一個包含三個線程的線程池
    private static ExecutorService pool = Executors.newFixedThreadPool(3);

    static class DemoThread implements Runnable{
        @Override
        public void run() {
            for (int i = 1; i <= MAX; i++) {
                Print.tco(ThreadUtil.getCurThreadName()+",DemoThread輪次:"+i);
                    ThreadUtil.sleepMilliSeconds(10);
            }
        }
    }

    static class ReturnableTask implements Callable<Long>{

        //返回並發執行的時間
        @Override
        public Long call() throws Exception {
            long startTime = System.currentTimeMillis();
            Print.tco(ThreadUtil.getCurThreadName()+" 線程運行開始.");
            for (int i = 1; i <= MAX; i++) {
                Print.tco(ThreadUtil.getCurThreadName()+",Callable輪次:"+i);
                ThreadUtil.sleepMilliSeconds(10);
            }
            long used = System.currentTimeMillis() - startTime;
            Print.tco(ThreadUtil.getCurThreadName()+" 線程運行結束");
            return used;
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        pool.execute(new DemoThread());//執行線程實例,無返回
        pool.execute(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= MAX; i++) {
                    Print.tco(ThreadUtil.getCurThreadName()+",Runnable輪次:"+i);
                        ThreadUtil.sleepMilliSeconds(10);
                }
            }
        });
        Future<Long> submit = pool.submit(new ReturnableTask());
        Long res = submit.get();
        System.out.println("異步任務的執行結果為:"+res);
        Thread.sleep(10);
        System.out.println(ThreadUtil.getCurThreadName()+" 線程結束.");
    }
}

以上就是java中四種創建線程的方式,各有各的特點,不過在實際開發中線程池結合Runnable接口實現的技術會多點。就如SpringBoot的任務調度器其底層的原理其實就是運用了線程池的技術,在此篇文章就不敘述過多了。


免責聲明!

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



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