JAVA 多線程與高并發(fā)學習筆記(一)——線程創(chuàng)建

好久沒寫筆記了,重新回歸Java,打好基礎。

Java 進程中每一個線程都對應著一個 Thread 實例,其中保存著線程的描述信息。

Thread

Java 使用 Thread 類表示線程,首先看一個簡單的示例。

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread();
        System.out.println("線程名稱: " + thread.getName());
        System.out.println("線程ID: " + thread.getId());
        System.out.println("線程狀態(tài): " + thread.getState());
        System.out.println("線程優(yōu)先級: " + thread.getPriority());
        thread.start();
    }

運行示例,可以看到如下輸出:

thread1.png

Thread 類中比較重要的屬性包括:

  • tid,線程ID,通過 getId() 方法可以獲取線程ID,線程 ID 在進程中是唯一的。
  • name,線程名稱,通過 getName() 方法可以獲取線程名稱,通過 setName(String name) 方法可以設置線程名稱。
  • priority, 線程優(yōu)先級,通過 getPriority() 方法可以獲取線程優(yōu)先級,通過 setPriority(int priority) 方法可以設置線程優(yōu)先級。Java線程最小值為1,最大值為10,默認為5。
  • daemon,標記線程是否為守護線程,setDaemon(boolean on) 方法可以設置線程是否為守護線程。默認值為 false,表示為普通的用戶線程,不是守護線程。
  • threadState,線程狀態(tài),以整數形式表示,通過 getState() 方法可以返回當前線程的狀態(tài)。返回值為如下枚舉:
public static enum State {
    NEW,                // 新建
    RUNNABLE,           // 就緒、運行
    BLOCKED,            // 阻塞
    WAITING,            // 等待
    TIMED_WAITING,      // 休時等得
    TERMINATED;         // 結束
}

Thread 類中還有幾個常用的方法:

  • start() 方法,用來啟動一個線程。
  • run() 方法,作為線程代碼邏輯的入口方法。當調用 start() 方法啟動一個線程后,只要線程獲取了 CPU 執(zhí)行時間,就會進入 run() 方法執(zhí)行用戶代碼。

另外,通過靜態(tài)方法 currentThread() 可以獲取當前線程的實例對象。

通過繼承 Thread 類創(chuàng)建線程

通過繼承 Thread 類創(chuàng)建線程包含兩個步驟:

  1. 編寫一個繼承 Thread 類的新線程類。
  2. 重寫 run() 方法,將要執(zhí)行的業(yè)務代碼添加到其中。

下面看一個簡單的示例:

public class createThread1 {

    static int threadNo = 1;
    static class MyThread extends Thread {
        public MyThread() {
            super("MyThread-" + threadNo);
            threadNo++;
        }

        public void run() {
            for(int i = 0; i< 3; i++) {
                System.out.println(getName() + ",輪次: " + i);
            }
            System.out.println(getName() + "運行結束.");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = null;
        for(int i = 0; i < 2; i++) {
            thread = new MyThread();
            thread.start();
        }
    }

}

運行代碼,可以得到輸出:

thread2.png

通過實現(xiàn) Runnable 接口創(chuàng)建線程

如果看下 Thread 類的源碼,我們會發(fā)現(xiàn)它實現(xiàn)了一個接口 Runnable。Runnable 源碼如下,其中只包含一個抽象方法 run()

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Thread 類中包含著一個擁有 Runnable 參數的構造器,利用這個構造器我們就可以生成 Thread 類。

public Thread(Runnable target)
public Thread(Runnable target, String name)

我們可以通過實現(xiàn) Runnable 接口來創(chuàng)建一個線程。具體步驟如下:

  1. 定義一個實現(xiàn)了 Runnable 接口的類。
  2. 實現(xiàn) Runnable 接口的 run() 方法,將代碼邏輯放入其中。
  3. 通過 Thread 類創(chuàng)建線程對象,將現(xiàn)了 Runnable 接口的類實例傳入其中。

看一個例子:

public class createThread2 {
    static int threadNo = 1;

    static class RunTarget implements Runnable {
        public void run() {
            for(int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + ", 輪次: " + i);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = null;
        for(int i = 0; i < 2; i++) {
            Runnable target = new RunTarget();
            thread = new Thread(target, "MyRunnableThread-" + threadNo);
            threadNo++;
            thread.start();
        }
    }
}

運行代碼,輸出如下:

thread3.png

我們還可以利用匿名類和 lambda 表達式,更加優(yōu)雅的使用 Runnbale 接口創(chuàng)建線程。

將上面的例子改造成匿名類:

public class createThread2 {
    static int threadNo = 1;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = null;
        for(int i = 0; i < 2; i++) {
            thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j = 0; j < 3; j++) {
                        System.out.println(Thread.currentThread().getName() + ", 輪次:" + j);
                    }
                }
            }, "MyRunnableThread-" + threadNo);
            threadNo++;
            thread.start();
        }
    }
}

將上面的例子改造成 Lambda 表達式:

public class createThread2 {
    static int threadNo = 1;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = null;
        for(int i = 0; i < 2; i++) {
            thread = new Thread(() -> {
                for(int j = 0; j < 3; j++) {
                    System.out.println(Thread.currentThread().getName() + ", 輪次:" + j);
                }

            }, "MyRunnableThread-" + threadNo);
            threadNo++;
            thread.start();
        }
    }
}

使用 Runnable 接口創(chuàng)建現(xiàn)成的方式優(yōu)點是可以更好地分離邏輯和數據,多個線程能夠并行處理同一個資源。

通過 CallableFutureTask 創(chuàng)建線程

通過繼承 Thread 類或者實現(xiàn) Runnable 接口創(chuàng)建線程的缺陷是無法獲取異步結果。

如果要使用異步執(zhí)行,那么就要用 Callable 接口和 FutureTask 類結合創(chuàng)建線程。

Callable 接口

Callable 接口源碼如下,它是一個泛型接口,也是一個函數式接口。其唯一的抽象方法 call() 有返回值,該方法還有一個 Exception 的異常聲明,允許方法的實現(xiàn)版本的內部異常直接拋出,并且允許不予捕獲。

@FunctionalInterface
public interface Callable<V> {   
    V call() throws Exception;
}

RunnableFuture 接口

RunnableFuture 接口的源碼如下,它也是一個泛型接口,并且繼承了 RunnableFuture 接口,這樣它一方面可以作為 Thread 線程實例的 target 實例,另一方面可以異步執(zhí)行。

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

下面看一下為什么繼承了 Future 接口就可以異步執(zhí)行了。

Future 接口

Future 接口是用來處理異步任務的,它主要提供了三大功能:

  1. 能夠取消異步執(zhí)行中的任務。
  2. 判斷異步任務是否執(zhí)行完成。
  3. 獲取異步任務完成后的執(zhí)行結果。
public interface Future<V> {
    boolean cancel(boolean mayInterruptRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

各個方法說明:

  • V get():獲取異步任務執(zhí)行的結果。注意,這個方法的調用是阻塞性的,如果異步任務沒有執(zhí)行完成,異步結果獲取線程會一直阻塞,直到異步任務執(zhí)行完成,將結果返回給調用線程。
  • V get(long timeout, TimeUnit unit):設置時限并獲取異步任務執(zhí)行的結果,如果阻塞時間超過設定的時限,該方法就拋出異常。
  • boolean isDone():獲取異步任務是否執(zhí)行完成,執(zhí)行完成則返回 true。
  • boolean isCancelled():獲取異步任務是否取消,任務完成前取消則返回 true
  • boolean cancel(boolean mayInterruptRunning):取消異步任務的執(zhí)行。

FutureTask

Future 只是個接口,而 FutureTask 類是它的一個默認實現(xiàn),更準確的說, FutureTask 類實現(xiàn)了 RunnableTask 接口。

FutureTask 類中有一個 Callable 類型的成員,用來保存任務。FutureTaskrun() 方法會執(zhí)行 call() 方法。

創(chuàng)建線程

步驟如下:

  1. 創(chuàng)建一個 Callable 接口的實現(xiàn)類,并實現(xiàn)其 call() 方法,在其中編寫異步執(zhí)行邏輯,可以有返回值。
  2. 使用 Callable 實現(xiàn)類的實例構造一個 FutureTask 實例。
  3. 使用 FutureTask 實例作為 Thread 構造器 target 的入參,構造 Thread 線程實例。

這樣線程就構造好了,線程啟動后,我們還可以調用 FutureTask 對象的 get() 方法阻塞性地獲得并發(fā)線程的執(zhí)行結果。

public class createThread3 {

    static class MyTask implements Callable<Long> {
        public Long call() throws Exception {
            long startTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + " 線程開始運行.");
            Thread.sleep(500);

            for(int i = 0; i < 100000000; i++) {
                int j = i * 10000;
            }

            long used = System.currentTimeMillis() - startTime;
            System.out.println(Thread.currentThread().getName() + " 線程結束運行.");
            return used;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyTask task = new MyTask();
        FutureTask<Long> futureTask = new FutureTask<>(task);
        Thread thread = new Thread(futureTask, "MyThread");
        thread.start();
        Thread.sleep(500);
        System.out.println(Thread.currentThread().getName() + " 先歇一會.");
        for(int i = 0; i < 100000000 / 2; i++) {
            int j = i * 10000;
        }
        System.out.println(Thread.currentThread().getName() + " 獲取并發(fā)任務的執(zhí)行結果.");
        try {
            System.out.println(thread.getName() + " 線程占用時間: " + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 運行結束.");
    }
}

通過 FutureTaskget 方法獲取結果,有 2 種情況:

  • 異步任務執(zhí)行完成,直接返回結果。
  • 異步任務沒有執(zhí)行完成,一直阻塞到執(zhí)行完成返回結果。

通過線程池創(chuàng)建線程

前面創(chuàng)建的線程在使用完之后就銷毀了,比較浪費時間和資源,為了不頻繁創(chuàng)建和銷毀線程,需要對線程進行復用,這時候就要用到線程池技術。

Java 種提供了一個靜態(tài)工廠 Executors 類來創(chuàng)建不同的線程池。簡單示例如下:

private static ExecutorService pool = Executors.newFixedThreadPool(3);

ExecutorService 是 Java 提供的線程池接口,可以通過它的實例提交或者執(zhí)行任務。ExecutorService 實例負責對池中的線程進行管理和調度,并且可以控制最大并發(fā)線程數,同時提供了定時執(zhí)行、定頻執(zhí)行、單線程、并發(fā)數控制等功能。

ExecutorService 線程池提交異步執(zhí)行 target 目標任務的常用方法:

// 執(zhí)行一個 Runnable 類型的目標實例,無返回
void execute(Runnable command);
// 提交一個 Callable 類型的目標實例,返回一個 Future 異步任務實例
<T> Future<T> submit(Callable<T> task);
// 提交一個 Runnable 類型的目標實例,返回一個 Future 異步任務實例
Future<?> submit(Runnable task);

下面看一個實戰(zhàn)例子。

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

public class createThread4 {
    private static ExecutorService pool = Executors.newFixedThreadPool(3);

    static class MyThread implements Runnable {
        @Override
        public void run() {
            for(int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + ", 輪次: " + i);
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class MyTask implements Callable<Long> {
        public Long call() throws Exception {
            long startTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + " 線程開始運行.");
            Thread.sleep(500);

            for(int i = 0; i < 100000000; i++) {
                int j = i * 10000;
            }

            long used = System.currentTimeMillis() - startTime;
            System.out.println(Thread.currentThread().getName() + " 線程結束運行.");
            return used;
        }
    }

    public static void main(String[] args) throws Exception{
        pool.execute(new MyThread());
        pool.execute(new Runnable() {
            @Override
            public void run() {
                for(int j = 0; j < 5; j++) {
                    System.out.println(Thread.currentThread().getName() + ", 輪次: " + j);
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Future future = pool.submit(new MyTask());
        Long result = (Long)future.get();
        System.out.println("異步任務的執(zhí)行結果為: " + result);
    }
}

運行結果如下:

thread4.png

可以看到線程池中的線程默認名稱和普通線程也有所不同。

注意其中 ExecutorService 線程池的 executesubmit 方法有如下區(qū)別:

  1. 接收參數不一樣。 submit 方法可以接收無返回值的 Runnable 類型和有返回值的 Callable 類型,execute僅接收無返回值的 Runnable 類型或者 Thread 實例。

  2. submit 方法有返回值,而 execute 方法沒有。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容