[自用向]粗略複習——線程基礎(基礎的不得了)

取材於網絡,忘記哪些帖子惹,挺多的。主要是我自己防止自己忘記記錄的。

1.線程

是大家比較熟悉的概念,線程和進程都有五個階段:創建、就緒、運行、阻塞、終止。多線程即一個程序有多個順序流在執行。
實現的方法 有三種:Thread、Runnable 和 Callable接口與Future、線程池結合。

2.首先說java.lang.Thread

Thread類,是很方便的一種,使用起來很快速,如果我們只是想啓一個線程,沒特殊要求,可以直接用Thread。使用的方法也極其簡單,繼承,並填充run方法。等使用的時候直接new出來,然後使用start方法即可,使用十分簡單。

public class MyThread extends Thread {

    private String name;

    public MyThread(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("線程 " + name + i);
            try {
                sleep((int)Math.random()*100);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

test:

new MyThread("A").start();
new MyThread("B").start();

這就行了,程序運行main方法的時候,java會啓動一個線程,主線程隨着main創建。調用start方法之後,另外兩個線程也啓動了。Start方法調用之後,並不是立即執行了多線程的代碼,而是先把線程轉爲Runnable狀態,操作系統決定代碼何時運行。多線程的程序實際上的亂序執行的,執行結果是隨機的。

3.然後說一下Runnable

是接口,目標類需要實現接口,然後重寫run方法,使用也是很簡單,new Thread()裏面參數填我們自己創建的runnable實現類就可以了。然後還是調用start方法。
好處也很明顯,畢竟java類單繼承,但是可以實現多個接口。

public class MyRunnable implements Runnable{

    private String name;

    public MyRunnable(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("線程 " + name + i);
            try {
                sleep((int) Math.random() * 100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

test:

new Thread(new MyRunnable("A")).start();
new Thread(new MyRunnable("B")).start();

當類實現了Runnable接口之後,此類就具有多線程的特徵,接下來做的就是填充run方法。看源碼可知,Thread類實際上也是實現了Runnable的類。啓動的時候,是先通過Thread類構造方法,調用生成的Thread對象的start方法。可以說多線程的都是通過Thread的start方法來運行的。Thread的API是多線程的關鍵。
要注意一點!!!線程池只能放入實現Runnable和Callable類線程,不能直接放入繼承Thread的類。

4.最後就是Callable

一般來說,使用Runnable接口和繼承Thread實現線程是無法給我們返回結果的,如果需要結果,那麼可以使用Callable,但是Callable只能在ExecutorService的線程池裏跑,同時可以通過返回的Future對象查詢執行狀態,Future取得了執行異步任務的結果。
詳細說一下,先看個例子

public class MyCallable implements Callable {
    
    private String name;

    public MyCallable(String name) {
        this.name = name;
    }

    @Override
    public Object call() throws Exception {
        for (int i = 0; i < 10; i++) {
            System.out.println("線程 " + name + i);
            try {
                sleep((int) Math.random() * 100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return name;
    }
    
}

寫倒是同樣很簡單,但是怎麼去使用呢?
最好是通過線程池去啓動:

ExecutorService executor = Executors.newCachedThreadPool();
MyCallable c1 = new MyCallable("A");
MyCallable c2 = new MyCallable("B");
MyCallable c3 = new MyCallable("C");
Future<Integer> result1 = executor.submit(c1);
Future<Integer> result2 = executor.submit(c2);
Future<Integer> result3 = executor.submit(c3);
System.out.println("task1運行結果:"+result1.get());
System.out.println("task2運行結果:"+result2.get());
System.out.println("task3運行結果:"+result3.get());
executor.shutdown();

這裏用了緩存型池。後續補充說明。
看完例子我們稍稍微微的看一下源碼。

Callable接口的源碼很簡單,只有一個call方法,返回的一個泛型,這個就是線程返回的結果。然後使用ExecutorService接口是負責線程池調度的。
裏面比較重要的是:

submit(Callable<T>  task)
submit(Callable<T>  task,T result)
Submit(Runnable task)
boolean awaitTermination(long timeout, TimeUnit unit)
Vord shutdown()

前三個很明顯,就是幫助我們開啓線程的,類似start
第四個是阻塞,監測ExecutorService是否關閉,如果關閉了返回true,否則false。一般和shutdown組合使用。
最後的shutdown顧名思義,平滑的關閉ExecutorService。調用時,停止接收新的任務並且等待已經提交的任務執行完。所有任務完成之後,線程池關閉。

再看Future接口:

get() 是等待線程結果返回,會阻塞
get(long timeout,TimeUnit unit) 設置超時時間
cancel() 取消任務執行,任務已經完成或者已經取消則失敗,參數是是否應該試圖停止任務的方式去中斷線程。
還有isCancelled和isDone都是判斷狀態的。

關於Callable只有這麼多嗎???當然不是!!!有個很重要的類叫做FutureTask,它是Future的實現類,並且實現了Runnable接口,所以可以通過Executor來執行,也可以傳遞給Thread對象執行。
如果要在主線程執行比較耗時的工作,同時不想阻塞主線程,可以交給Future對象在後臺完成。Executor框架利用FutureTask完成異步任務,一般來說用它進行耗時的計算。主線程可以在完成自己的任務之後,再去其獲取結果。FutureTask的使用有兩種情況:

  • 1.作爲new Thread()的參數。
  • 2.ExecutorService.submit() 扔線程池裏。
public static void main(String[] args) {

    // 1.直接啓動線程
    MyCallable task1 = new MyCallable("A");
    MyCallable task2 = new MyCallable("B");

    FutureTask<Integer> result1 = new FutureTask<Integer>(task1);
    FutureTask<Integer> result2 = new FutureTask<Integer>(task2);

    Thread thread1 = new Thread(result1);
    Thread thread2 = new Thread(result2);

    thread1.start();
    thread2.start();
    
    // 2.線程池啓動
    ExecutorService executor = Executors.newCachedThreadPool();
    Future<Integer> result3 = executor.submit(task1);
    Future<Integer> result4 = executor.submit(task2);
    executor.shutdown();
    
}

發現了嗎?其實用了線程池反而更簡單了。。。

但是有個問題,如果我們開了很多很多線程,這些線程的執行時間是未知的,但是我們有需要返回結果,如果我們只是通過Future和FutureTask去取結果效率就很低,因爲我們需要通過循環不斷遍歷線程池裏面的線程,判斷執行狀態並取得結果。於是針對這種情況,有一個CompletionService。
其原理在於將線程池執行結果放到一個Blockqueueing裏面,這裏線程執行結果進入Blockqueueing的順序只與線程的執行時間有關。
CompletionService是一個接口,裏面有五個方法分別是:

    //提交線程任務
    Future<V> submit(Callable<V> task);
    //提交線程任務
    Future<V> submit(Runnable task, V result);
    //阻塞等待
    Future<V> take() throws InterruptedException;
    //非阻塞等待
    Future<V> poll();
    //帶時間的非阻塞等待
    Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;

這個接口有一個實現類ExecutorCompletionService。
實現類的源碼可知,最終線程要提交到Executor裏面去運行,所以構造函數中需要Executor參數。每當線程執行完畢之後會往阻塞隊列添加一個Future。
舉例時間:
只用Future:

//不使用 CompletionService 只用 Future
public static void futureTest() throws ExecutionException, InterruptedException {

    ExecutorService executor = Executors.newCachedThreadPool();
    List<Future<Integer>> result = new ArrayList<>();
    // 假設有10個線程
    for (int i = 0; i < 10; i++) {
        Future<Integer> submit = executor.submit(new MutiThreadFuture(i));
        result.add(submit);
    }
    executor.shutdown();
    // 依次等待返回結果
    for (Future<Integer> future : result) {
        System.out.println("返回結果" + future.get());
    }

}

使用了CompletionService:

public static void CompleteTest() throws InterruptedException, ExecutionException {
    ExecutorService executor = Executors.newCachedThreadPool();
    // 完成服務
    CompletionService<Integer> completionService = new ExecutorCompletionService<>(executor);
    for (int i = 0; i < 10; i++) {
        completionService.submit(new MutiThreadFuture<>(i));
    }
    // 依次等待返回結果
    for (int i = 0; i < 10; i++) {
        System.out.println("結果" + completionService.take().get());
    }

}

得到的結果:

pool-1-thread-3
pool-1-thread-2
pool-1-thread-6
pool-1-thread-4
pool-1-thread-1
返回結果0
返回結果1
返回結果2
返回結果3
pool-1-thread-5
返回結果4
返回結果5
pool-1-thread-7
返回結果6
pool-1-thread-9
pool-1-thread-8
返回結果7
返回結果8
pool-1-thread-10
返回結果9
------------------------------------------------------分割線-----------------------------------------------
pool-2-thread-2
結果1
pool-2-thread-6
pool-2-thread-1
結果5
pool-2-thread-10
結果0
結果9
pool-2-thread-3
pool-2-thread-4
結果2
結果3
pool-2-thread-7
pool-2-thread-5
結果4
結果6
pool-2-thread-8
pool-2-thread-9
結果7
結果8

根據結果能看得出來,使用了Completion之後的結果的輸出和線程放入沒啥關係。

5.簡單說一下線程池

剛纔在上面一直在用的線程池是這個:newCachedThreadPool
但實際上還有三種線程池可供選擇:

  • newFixedThreadPool 固定容量的線程池,線程達到最大值的時候,線程池的規模不會再變化。
  • newCachedThreadPool 緩存的線程池,線程達到最大值之後,將會回收空的線程。當需求增加,線程數量也會增加,沒有上限。
  • newSingleThreadPoolExecutor 單線程的線程池,串行執行。
  • newScheduledThreadPool 固定容量的線程池,以延遲或者定時的方式去執行,一般來說會選擇消息隊列或xxl-job去做定時,而不是這個。

使用線程池有啥好處呢?首先效率高,重用現有的線程,可以在處理多個請求的時候分擔線程產生和銷燬的開銷。請求達到一定數量的時候,工作線程已經存在,響應性大大提高。線程池的容量是可以控制的,儘可能的利用好資源。

使用線程池的方法也很簡單!就像上面寫的,首先寫一個線程類,三種方式任意。
然後建立一個線程池,四個任意,推薦newCachedThreadPool
(也可以通過newFixedThreadPool 設置線程池大小,有效利用資源)
最後調用線程池操作,executorService.execute(你的線程)
然後就完成了!!!是不是很EZ??!

那麼要分析一下:

1.什麼情況用CachedThreadPool

它首先會創造足夠多的線程去執行任務,隨着程序的執行,有點線程執行完,可以循環使用,這時候不必新建線程。客戶端線程和線程池之間會有一個任務隊列,程序要關閉時,需要注意兩件事!1.入隊的任務現在什麼情況。2.正在運行這個任務現在怎麼樣了。有兩種方式去關閉線程池,這很重要!1.入隊任務全部執行完畢(shutdown())。2.捨棄這些任務直接結束(shutdownNow())。根據具體情節,程序員自己要做取捨。 注意:主線程的執行和線程池裏面的線程分開,很有可能主線的線程結束了,但是線程池還在運行。

2.什麼情況用FixedThreadPool

它固定了容量,這個模最大式線程數目是一定的。當確定任務資源佔用的情況之後,想要控制資源利用,那麼可以使用這個線程池。線程執行完成就從線程池直接移出,不能保證順序性,全看線程之間的競爭。

3.什麼情況用newSingleThreadExecutor

它能保證的是線程的執行順序,並且能夠保證線程結束之後,下一條線程再被開啓。

4.什麼情況用newScheduledThreadPool

它功能看起來很強大,的確很強大,因爲可以設置時間和線程執行的先後間隔,但是大多數情況下,我會選擇直接使用消息隊列或者定時器。
說了四個線程池,他們有個共同的特性,都傳了不同的參數去調用了同一個接口,ThreadPoolExecutor。然後這個ThreadPoolExecutor就厲害了。
找了個帶註釋的源碼部分:

//運行狀態標誌位
    volatile int runState;
    static final int RUNNING    = 0;
    static final int SHUTDOWN   = 1;
    static final int STOP       = 2;
    static final int TERMINATED = 3;
 
    //線程緩衝隊列,當線程池線程運行超過一定線程時並滿足一定的條件,待運行的線程會放入到這個隊列
    private final BlockingQueue<Runnable> workQueue;
    //重入鎖,更新核心線程池大小、最大線程池大小時要加鎖
    private final ReentrantLock mainLock = new ReentrantLock();
    //重入鎖狀態
    private final Condition termination = mainLock.newCondition();
    //工作都set集合
    private final HashSet<Worker> workers = new HashSet<Worker>();
    //線程執行完成後在線程池中的緩存時間
    private volatile long  keepAliveTime;
    //核心線程池大小 
    private volatile int   corePoolSize;
    //最大線程池大小 
    private volatile int   maximumPoolSize;
    //當前線程池在運行線程大小 
    private volatile int   poolSize;
    //當緩衝隊列也放不下線程時的拒絕策略
    private volatile RejectedExecutionHandler handler;
    //線程工廠,用來創建線程
    private volatile ThreadFactory threadFactory;   
    //用來記錄線程池中曾經出現過的最大線程數
    private int largestPoolSize;   
   //用來記錄已經執行完畢的任務個數
   private long completedTaskCount;   
 
    ................
}

execute(Runnable command)方法很重要

       if (command == null)
            throw new NullPointerException();
            int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);

註釋寫了很多,一共分三步:
1.如果運行的線程,小於corePoolSize那麼嘗試開新線程。addWorker方法的原子性,檢查runState和workerCount,可以通過返回false防止出現錯誤。
2.如果一個任務成功排隊,我們仍然要再次檢查一下是否應該添加一個線程。或者線程池會關閉。我們重新檢查狀態,如果沒有線程就新開一個,如果停止就有必要回滾隊列。
3.如果沒法排隊了,就要添加新線程了,如果失敗了,我們就會發現隊列飽和或者是線程池被關閉,所以會拒絕任務。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章