java多線程與併發一站式教程

1、多線程基礎

1.1、線程生命週期

1.1.1、新建

 

1.1.2、就緒

當線程對象調用了start()方法之後,該線程處於就緒狀態。Java虛擬機會爲其創建方法調用棧和程序計數器,等待調度運行

 

1.1.3、運行

 

1.1.4、阻塞

阻塞分爲三種:

  • 等待阻塞(o.wait->等待對列): 運行(running)的線程執行o.wait()方法,JVM會把該線程放入等待隊列(waitting queue)中
  • 同步阻塞(lock->鎖池) 運行(running)的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池(lock pool)中
  • 其他阻塞(sleep/join) 運行(running)的線程執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入可運行(runnable)狀態

 

1.1.5、死亡

 

1.2、終止線程的方式

1.2.1、正常運行結束

 

1.2.2、使用退出標誌

while(!exit){

    //do something

}

這裏的exit是自定義的變量

 

1.2.3、使用interrupt()方法來中斷線程

分兩種情況:

a)線程處於阻塞狀態:如使用了sleep,同步鎖的wait,socket中的receiver,accept等方法時,會使線程處於阻塞狀態。當調用線程的interrupt()方法時,會拋出InterruptException異常。阻塞中的那個方法拋出這個異常,通過代碼捕獲該異常,然後break跳出循環狀態,從而讓我們有機會結束這個線程的執行。通常很多人認爲只要調用interrupt方法線程就會結束,實際上是錯的, 一定要先捕獲InterruptedException異常之後通過break來跳出循環,才能正常結束run方法。

public class StopThreadTest implements Runnable{
    public static void main(String[] args) throws InterruptedException {

        Thread t = new Thread(new StopThreadTest());
        t.start();
        Thread.sleep(3000);
        t.interrupt();
    }

    @Override
    public void run() {
        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
            System.out.println("hello");
        }
    }
}

 

b)線程未處於阻塞狀態:使用isInterrupted()判斷線程的中斷標誌來退出循環。當使用interrupt()方法時,中斷標誌就會置true,和使用自定義的標誌來控制循環是一樣的道理

public class StopThreadTest implements Runnable{
    public static void main(String[] args) throws InterruptedException {

        Thread t = new Thread(new StopThreadTest());
        t.start();
        Thread.sleep(3000);
        t.interrupt();
    }

    @Override
    public void run() {
        while(!Thread.currentThread().isInterrupted()){
            System.out.println("hello");
        }
    }
}

 

1.2.3、stop方法強制結束

程序中可以直接使用thread.stop()來強行終止線程,但是stop方法是很危險的,就象突然關閉計算機電源,而不是按正常程序關機一樣,可能會產生不可預料的結果

不安全主要是:thread.stop()調用之後,創建子線程的線程就會拋出ThreadDeatherror的錯誤,並且會釋放子線程所持有的所有鎖。一般任何進行加鎖的代碼塊,都是爲了保護數據的一致性,如果在調用thread.stop()後導致了該線程所持有的所有鎖的突然釋放(不可控制),那麼被保護數據就有可能呈現不一致性,其他線程在使用這些被破壞的數據時,有可能導致一些很奇怪的應用程序錯誤。因此,並不推薦使用stop方法來終止線程

可能會造成死鎖

 

1.3、sleep與wait方法區別

  • 對於sleep()方法,我們首先要知道該方法是屬於Thread類中的。而wait()方法,則是屬於Object類中的
  • sleep()方法導致了程序暫停執行指定的時間,讓出cpu該其他線程,但是他的監控狀態依然保持者,當指定的時間到了又會自動恢復運行狀態
  • 在調用sleep()方法的過程中,線程不會釋放對象鎖
  • 而當調用wait()方法的時候,線程會放棄對象鎖,進入等待此對象的等待鎖定池,只有針對此對象調用notify()方法後本線程才進入對象鎖定池準備獲取對象鎖進入運行狀態

 

1.4、守護線程daemon

  • 守護線程也稱服務線程,他是後臺線程,它有一個特性,即爲非守護線程提供服務,在沒有非守護線程可服務時會自動離開
  • 垃圾回收線程就是一個經典的守護線程,當我們的程序中不再有任何運行的Thread,程序就不會再產生垃圾,垃圾回收器也就無事可做,所以當垃圾回收線程是JVM上僅剩的線程時,垃圾回收線程會自動離開
  • 守護線程不依賴於終端,但是依賴於系統,與系統“同生共死”。當JVM中所有的線程都是守護線程的時候,JVM就可以退出了;如果還有一個或以上的非守護線程則JVM不會退出
public class StopThreadTest implements Runnable{


    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(new StopThreadTest());
        Thread t2 = new Thread(new StopThreadTest());
        t1.setDaemon(true);
        t2.setDaemon(true);
        t1.start();
        t2.start();

        System.out.println(t1.isDaemon());
        System.out.println(t2.isDaemon());
        System.out.println(Thread.currentThread().isDaemon());

    }

    @Override
    public void run() {

        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getId());
        }
    }
}

輸出結果:

true

true

false

如果t1和t2都設置爲守護線程,那麼當main線程結束後(main線程是非守護線程),系統中就沒有非守護線程了,所以t1和t2線程都會自動退出,不會一直打印各自的線程ID

 

1.5、線程基本方法

1.5.1、wait

調用該方法的線程進入WAITING狀態,只有等待另外線程的通知或被中斷纔會返回,需要注意的是調用wait()方法後,會釋放對象的鎖。因此,wait方法一般用在同步方法或同步代碼塊中


1.5.2、線程睡眠(sleep)

sleep導致當前線程休眠,與wait方法不同的是sleep不會釋放當前佔有的鎖,sleep(long)會導致線程進入TIMED-WATING(超時等待)狀態,而wait()方法會導致當前線程進入WATING狀態


1.5.3、線程讓步(yield)

yield會使當前線程讓出CPU執行時間片,與其他線程一起重新競爭CPU時間片。一般情況下,優先級高的線程有更大的可能性成功競爭得到CPU時間片,但這又不是絕對的,有的操作系統對線程優先級並不敏感

 

1.5.4、notify與notifyAll

Object 類中的 notify() 方法,喚醒在此對象監視器上等待的單個線程,如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程,選擇是任意的,被喚醒的線程將以常規方式與在該對象上主動同步的其他所有線程進行競爭。類似的方法還有 notifyAll() ,喚醒再次監視器上等待的所有線程

 

2、線程池

2.1、線程池原理

線程池做的工作主要是控制運行的線程的數量,處理過程中將任務放入隊列,然後在線程創建後啓動這些任務,如果線程數量超過了最大數量超出數量的線程排隊等候,等其它線程執行完畢,再從隊列中取出任務來執行

他的主要特點爲:線程複用;控制最大併發數;管理線程

 

2.2、線程複用原理

每一個 Thread 的類都有一個 start 方法。 當調用start啓動線程時Java虛擬機會調用該類的 run 方法。 那麼該類的 run() 方法中就是調用了 Runnable 對象的 run() 方法。 我們可以繼承重寫 Thread 類,在其 start 方法中添加不斷循環調用傳遞過來的 Runnable 對象。 這就是線程池的實現原理。循環方法中不斷獲取 Runnable 是用 Queue 實現的,在獲取下一個 Runnable 之前可以是阻塞的

 

2.3、自定義線程池

2.3.1、線程池類

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * 自定義定長線程池
 */
public class MyThreadPool {
    /**
     * 線程池數組
     */
    private Thread[] threadArr;
    private int threadNum;
    /**
     * 線程隊列
     */
    private BlockingQueue<Runnable> runnableQueue = new ArrayBlockingQueue<Runnable>(100);

    /**
     * 定長線程池構造方法
     */
    public MyThreadPool(int threadNum){
        this.threadNum = threadNum;
        initThreadPool();
    }

    /**
     * 初始化線程池
     */
    private void initThreadPool(){
        threadArr = new WorkThread[threadNum];
        for (int i = 0; i < threadNum; i++) {
            threadArr[i] = new WorkThread(this);
            threadArr[i].start();
        }
    }

    /**
     * 線程池執行器
     */
    public void execute(Runnable r){
        runnableQueue.add(r);
    }

    public BlockingQueue<Runnable> getRunnableQueue() {
        return runnableQueue;
    }

    public void setRunnableQueue(BlockingQueue<Runnable> runnableQueue) {
        this.runnableQueue = runnableQueue;
    }
}

 

2.3.2、工作線程類

public class WorkThread extends Thread{
    private MyThreadPool myThreadPool;
    public WorkThread(MyThreadPool myThreadPool){
        this.myThreadPool = myThreadPool;
    }

    @Override
    public void run() {
        while(true){
            try {
                Runnable r = myThreadPool.getRunnableQueue().take();
                r.run();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

2.3.3、任務類

public class MyTask implements Runnable{
    private String name;

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

    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name);
        }
    }
}

 

2.3.4、測試類

public class MyThreadPoolTest {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(5);

        MyTask task1 = new MyTask("name1");
        MyTask task2 = new MyTask("name2");
        MyTask task3 = new MyTask("name3");
        MyTask task4 = new MyTask("name4");
        MyTask task5 = new MyTask("name5");

        MyTask task6 = new MyTask("name6");
        MyTask task7 = new MyTask("name7");

        myThreadPool.execute(task1);
        myThreadPool.execute(task2);
        myThreadPool.execute(task3);
        myThreadPool.execute(task4);
        myThreadPool.execute(task5);
        myThreadPool.execute(task6);
        myThreadPool.execute(task7);
    }

}

 

2.4、線程池組成

一般的線程池主要分爲以下4個組成部分:

  • 線程池管理器:用於創建並管理線程池
  • 工作線程:線程池中的線程
  • 任務接口:每個任務必須實現的接口,用於工作線程調度其運行
  • 任務隊列:用於存放待處理的任務,提供一種緩衝機制 Java中的線程池是通過Executor框架實現的,該框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor ,Callable和Future、FutureTask這幾個類

 

2.4.1、類關係圖 

 

 

2.4.2、ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

 

  • corePoolSize:指定了線程池中的線程數量
  • maximumPoolSize:指定了線程池中的最大線程數量
  • keepAliveTime:當前線程池數量超過corePoolSize時,多餘的空閒線程的存活時間,即多次時間內會被銷燬
  •  unit:keepAliveTime的單位
  •  workQueue:任務隊列,被提交但尚未被執行的任務
  •  threadFactory:線程工廠,用於創建線程,一般用默認的即可
  •  handler:拒絕策略,當任務太多來不及處理,如何拒絕任務

 

2.4.3、拒絕策略

線程池中的線程已經用完了,無法繼續爲新任務服務,同時,等待隊列也已經排滿了,再也塞不下新任務了。這時候我們就需要拒絕策略機制合理的處理這個問題

JDK內置的拒絕策略如下:

  • AbortPolicy : 直接拋出異常,阻止系統正常運行
  • CallerRunsPolicy : 只要線程池未關閉,該策略直接在調用者線程中,運行當前被丟棄的任務。顯然這樣做不會真的丟棄任務,但是,任務提交線程的性能極有可能會急劇下降
  • DiscardOldestPolicy : 丟棄最老的一個請求,也就是即將被執行的一個任務,並嘗試再次提交當前任務
  •  DiscardPolicy : 該策略默默地丟棄無法處理的任務,不予任何處理。如果允許任務丟失,這是最好的一種方案
  • 以上內置拒絕策略均實現了RejectedExecutionHandler接口,若以上策略仍無法滿足實際需要,完全可以自己擴展RejectedExecutionHandler接口

 

2.4.4、工作過程

1、線程池剛創建時,裏面沒有一個線程。任務隊列是作爲參數傳進來的。不過,就算隊列裏面有任務,線程池也不會馬上執行它們

2、當調用 execute() 方法添加一個任務時,線程池會做如下判斷:

    a)如果正在運行的線程數量小於 corePoolSize,那麼馬上創建線程運行這個任務

    b)如果正在運行的線程數量大於或等於 corePoolSize,那麼將這個任務放入隊列

    c)如果這時候隊列滿了,而且正在運行的線程數量小於 maximumPoolSize,那麼還是要創建非核心線程立刻運行這個任務

    d)如果隊列滿了,而且正在運行的線程數量大於或等於 maximumPoolSize,那麼線程池會拋出異RejectExecutionException

3、當一個線程完成任務時,它會從隊列中取下一個任務來執行。

4、當一個線程無事可做,超過一定的時間(keepAliveTime)時,線程池會判斷,如果當前運行的線程數大於 corePoolSize,那麼這個線程就被停掉。所以線程池的所有任務完成後,它最終會收縮到 corePoolSize 的大小

 

2.4.5、線程池的作用

假設一個服務器完成一項任務所需時間爲:T1 創建線程時間,T2 在線程中執行任務的時間,T3 銷燬線程時間。

如果:T1 + T3 遠大於 T2,則可以採用線程池,以提高服務器性能

  • 線程池技術正是關注如何縮短或調整T1,T3時間的技術,從而提高服務器程序性能的
  • 顯著減少了創建線程的數目
  • 管理線程,避免增加創建線程和銷燬線程的資源損耗

 

2.4.6、不建議使用 Executors靜態工廠構建現成的線程池

阿里巴巴Java開發手冊,明確指出不允許使用Executors靜態工廠構建線程池
原因如下:
線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險

Executors返回的線程池對象的弊端如下:

  • FixedThreadPool 和 SingleThreadPool:允許的請求隊列(底層實現是LinkedBlockingQueue)長度爲Integer.MAX_VALUE,可能會堆積大量的請求,從而導致OOM
  • CachedThreadPool 和 ScheduledThreadPool:允許的創建線程數量爲Integer.MAX_VALUE,可能會創建大量的線程,從而導致OOM
     

 

 

3、阻塞隊列

3.1、阻塞隊列API

 

3.2、阻塞隊列家族

  • ArrayBlockingQueue :由數組結構組成的有界阻塞隊列
  • LinkedBlockingQueue :由鏈表結構組成的有界阻塞隊列
  • PriorityBlockingQueue :支持優先級排序的無界阻塞隊列
  • DelayQueue:使用優先級隊列實現的無界阻塞隊列
  • SynchronousQueue:不存儲元素的阻塞隊列
  • LinkedTransferQueue:由鏈表結構組成的無界阻塞隊列
  • LinkedBlockingDeque:由鏈表結構組成的雙向阻塞隊列

 

 

 

 

 

 

 

 

 

 

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