Java之線程和線程池

 

 

 

 

 

 

 

 

 

 

 

 

 

 

創建線程的四種方法以及區別

1.線程是什麼?

        線程被稱爲輕量級進程,是程序執行的最小單位,它是指在程序執行過程中,能夠執行代碼的一個執行單位。每個程序程序都至少有一個線程,也即是程序本身

2.線程狀態

        Java語言定義了5種線程狀態,在任意一個時間點,一個線程只能有且只有其中一個狀態。,這5種狀態如下:

(1)新建(New):創建後尚未啓動的線程處於這種狀態

(2)運行(Runable):Runable包括了操作系統線程狀態的Running和Ready,也就是處於此狀態的線程有可能正在執行,也有可能正在等待着CPU爲它分配執行時間。

(3)等待(Wating):處於這種狀態的線程不會被分配CPU執行時間。等待狀態又分爲無限期等待和有限期等待,處於無限期等待的線程需要被其他線程顯示地喚醒,沒有設置Timeout參數的Object.wait()、沒有設置Timeout參數的Thread.join()方法都會使線程進入無限期等待狀態;有限期等待狀態無須等待被其他線程顯示地喚醒,在一定時間之後它們會由系統自動喚醒,Thread.sleep()、設置了Timeout參數的Object.wait()、設置了Timeout參數的Thread.join()方法都會使線程進入有限期等待狀態。

(4)阻塞(Blocked):線程被阻塞了,“阻塞狀態”與”等待狀態“的區別是:”阻塞狀態“在等待着獲取到一個排他鎖,這個時間將在另外一個線程放棄這個鎖的時候發生;而”等待狀態“則是在等待一段時間或者喚醒動作的發生。在程序等待進入同步區域的時候,線程將進入這種狀態。

(5)結束(Terminated):已終止線程的線程狀態,線程已經結束執行。

下圖是5種狀態轉換圖:

 

                

3.線程同步方法

線程有4中同步方法,分別爲wait()、sleep()、notify()和notifyAll()。

wait():使線程處於一種等待狀態,釋放所持有的對象鎖。

sleep():使一個正在運行的線程處於睡眠狀態,是一個靜態方法,調用它時要捕獲InterruptedException異常,不釋放對象鎖。

notify():喚醒一個正在等待狀態的線程。注意調用此方法時,並不能確切知道喚醒的是哪一個等待狀態的線程,是由JVM來決定喚醒哪個線程,不是由線程優先級決定的。

notifyAll():喚醒所有等待狀態的線程,注意並不是給所有喚醒線程一個對象鎖,而是讓它們競爭。

4.創建線程的方式

在JDK1.5之前,創建線程就只有兩種方式,即繼承java.lang.Thread類和實現java.lang.Runnable接口;而在JDK1.5以後,增加了兩個創建線程的方式,即實現java.util.concurrent.Callable接口和線程池。下面是這4種方式創建線程的代碼實現。

1)繼承Thread類創建線程

class MyThread extends Thread{
    @Override
    public void run() {
        // 重寫run方法
    }
}

2)實現Runnable接口創建線程

public class ThreadTest {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        Thread tt = new Thread(thread);
    }
}

class MyThread implements Runnable{
    @Override
    public void run() {
        // 重寫run方法
    }
}

3)使用Callable和Future創建線程

public class ThreadTest {

    public static void main(String[] args) throws Exception {
        // 創建Callable
        MyCallable myCallable = new MyCallable();
        // 創建FutureTask
        FutureTask<Integer> futureTask = new FutureTask<Integer>(myCallable);
        // 配置到Thread
        Thread thread = new Thread(futureTask, "AAA");
        // 啓動線程
        thread.start();
        // 獲取返回值
        int result01 = 100;

        while(!futureTask.isDone()){
            // 等待計算結果
        }
        
        // 阻塞計算,如果超過時間 拋出java.util.concurrent.TimeoutException異常
        int result02 = futureTask.get(2, TimeUnit.SECONDS);
        System.out.println("計算結果\t" + (result01 + result02));
    }
}

class MyCallable implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        try { TimeUnit.MILLISECONDS.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }
        return 1024;
    }
}

4)使用線程池例如用Executor框架

    public static void main(String[] args) {
//        ExecutorService threadPool = Executors.newFixedThreadPool(5);// 一池5個處理線程
//        ExecutorService threadPool = Executors.newSingleThreadExecutor();// 一池1個處理線程
        ExecutorService threadPool = Executors.newCachedThreadPool();//一池N處理線程

        // 模擬10個用戶來辦理業務,每個用戶就是一個來自外部的請求線程
        try {
            for (int i = 1; i <= 50; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + "\t辦理業務");
                    try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }

爲什麼使用線程池?

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

線程池主要特點:線程複用、控制最大併發數、管理線程

  1. 降低資源消耗,通過重複利用已創建的線程降低縣城創建和銷燬造成的消耗。
  2. 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  3. 提高現成的可管理型,線程是稀缺資源,如果無限的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。

線程池的種類:

  1. Executors.newScheduledThreadPool();  // 帶調度的線程池
  2. Executors.newWorkStealingPool(int);    // JDK1.8新出
  3. Executors.newFixedThreadPool(int);
  4. Executors.newSingleThreadExecutor();
  5. Executors.newCachedThreadPool();

線程池源碼解析:

1、定長線程池:

  1. 創建一個定長線程池,可控制現成最大併發數,超出的線程會在隊列中等待
  2. newFixedThreadPool創建的線程池corePoolSize和maximumPoolSize值是相等的,它使用LinkedBlockingQueue;
// 定長線程池
public static ExecutorService newFixedThreadPool(int nThreads) {
	return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
								  new LinkedBlockingQueue<Runnable>());
}

2、單個線程線程池:

  1. 創建一個線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序執行。
  2. newSingleThreadExecutor將corePoolSize和maximunPoolSize都設置爲1,它使用的LinkedBlockingQueue。
// 單個線程的線程池
public static ExecutorService newSingleThreadExecutor() {
	return new FinalizableDelegatedExecutorService
		(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
								new LinkedBlockingQueue<Runnable>()));
}

3、可緩存線程池:

  1. 創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。
  2. newCachedThreadPool將corePoolSize設置爲0,將maximumPoolSize設置爲Integer.MAX_VALUE,它使用的SynchronousQueue,也就是說來了任務就創建線程運行,當線程空閒超過60秒,就銷燬線程。
// 緩衝池線程
public static ExecutorService newCachedThreadPool() {
	return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
								  new SynchronousQueue<Runnable>());
}

4、線程池的七大參數

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
  1. int corePoolSize:線程池中的常駐核心線程數
  2. int maximumPoolSize:線程池能夠容納同時執行的最大線程數,此值必須大於等於1
  3. long keepAliveTime:多餘的空閒線程的存活時間,當前線程池數量超過corePoolSize時,當空閒時間達到keepAliveTime值時,多餘空閒線程會被銷燬直到只剩下corePoolSize個線程爲止
  4. TimeUnit unit:keepAliveTime的單位
  5. BlockingQueue<Runnable> workQueue:任務隊列,被提交但尚未被執行的任務。
  6. ThreadFactory threadFactory:表示生成線程池中工作線程的線程工廠,用於創建線程一般用默認的即可
  7. RejectedExecutionHandler handler:拒絕策略,表示當隊列滿了並且工作線程大於等於線程池的最大線程數

 

 

  1. 在創建了線程池後,等待提交過來的任務請求。
  2. 檔調用execute()方法添加一個請求任務時,線程池會做如下判斷:
    1. 如果正在運行的線程數量小於corePoolSize,那麼馬上創建線程運行這個任務
    2. 如果正在運行的線程數大於或等於corePoolSize,那麼將這個任務放入隊列
    3. 如果這時候隊列滿了且正在運行的線程數量還小於maximumPoolSize,那麼還是要創建非核心線程立刻運行這個任務
    4. 如果隊列滿了且正在運行的線程數量大於或等於maximumPoolSize,那麼線程池會自動飽和拒絕策略來執行。
  3. 當一個線程完成任務時,它會從隊列中取下一個任務來執行
  4. 當一個線程無事可做超過一定時間(KeepAliveTime)時,線程池會判斷
    1. 如果當前運行的線程數大於codePoolSize,那麼這個線程就被停掉
    2. 所以線程池的所有任務完成後它最終會收縮到corePoolSize的大小

 

線程池的拒絕策略

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

  • AbortPolicy(默認):直接拋出java.util.concurrent.RejectedExecutionException異常阻止系統正常運行。
  • CallerRunsPolicy:"調用者運行"一種調節機制,該策略既不會拋棄任務,也不會拋出異常,而是將某些任務回退到調用者,從而降低新任務的流量。
  • DiscardOldestpolicy:拋棄隊列中等待最久的任務,然後把當前任務加入隊列中嘗試再次提交任務。
  • DiscardPolicy:直接丟棄任務,不予任何處理也不拋出異常。如果允許任務丟失,這是最好的一種方案。
AbortPolicy:丟棄任務,拋出異常
    拋出java.util.concurrent.RejectedExecutionException異常

DiscardPolicy:丟棄任務,不拋出異常

CallerRunsPolicy:任務回退給調用者
    pool-1-thread-1	辦理業務	1
    main	        辦理業務	2

DiscardOldestPolicy:丟棄隊列中排在首位的數據


class MyTask implements Runnable {

    private int taskId;
    private String taskName;

    public MyTask(int taskId, String taskName) {
        this.taskId = taskId;
        this.taskName = taskName;
    }
    public int getTaskId() {
        return taskId;
    }
    public void setTaskId(int taskId) {
        this.taskId = taskId;
    }
    public String getTaskName() {
        return taskName;
    }
    public void setTaskName(String taskName) {
        this.taskName = taskName;
    }

    @Override
    public void run() {
        try {
            System.out.println("run taskId =" + this.taskId);
            Thread.sleep(5 * 1000);
            //System.out.println("end taskId =" + this.taskId);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String toString() {
        return Integer.toString(this.taskId);
    }

}

public class MyThreadDemo2 {

    public static void main(String[] args) {
        /**
         * 在使用有界隊列時,若有新的任務需要執行,如果線程池實際線程數小於corePoolSize,則優先創建線程,
         * 若大於corePoolSize,則會將任務加入隊列,
         * 若隊列已滿,則在總線程數不大於maximumPoolSize的前提下,創建新的線程,
         * 若線程數大於maximumPoolSize,則執行拒絕策略。或其他自定義方式。
         *
         */
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                1,              //coreSize
                2,              //MaxSize
                60,             //60
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(3),         //指定一種隊列 (有界隊列)
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy()
        );

        MyTask mt1 = new MyTask(1, "任務1");
        MyTask mt2 = new MyTask(2, "任務2");
        MyTask mt3 = new MyTask(3, "任務3");
        MyTask mt4 = new MyTask(4, "任務4");
        MyTask mt5 = new MyTask(5, "任務5");
        MyTask mt6 = new MyTask(6, "任務6");

        pool.execute(mt1);
        pool.execute(mt2);
        pool.execute(mt3);
        pool.execute(mt4);
        pool.execute(mt5);
        pool.execute(mt6);

        pool.shutdown();
    }
}

問題2:你在工作中單一/固定數的/可變的三種創建線程池的方法,你用哪個多?超級大坑

回答:java自帶的基本不用,高併發直接引起OOM,一般需要有一個管理者,如自己寫個類來管理,或者交給Spring來管理,主要是控制線程數量避免引起OOM。

問題3:你在工作中是如何使用線程池的,是否自定義過線程池使用

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
 1,
 10,
 1L,
 TimeUnit.SECONDS,
 new LinkedBlockingQueue<>(10),
 Executors.defaultThreadFactory(),
 new ThreadPoolExecutor.CallerRunsPolicy());

問題4:合理配置線程池是如何考慮的?

首先判斷業務密度

// 查看計算機核心數
System.out.println(Runtime.getRuntime().availableProcessors());

CPU密集型:該任務需要大量的運算,而沒有阻塞,CPU一直全力運行。 一般公式:CPU核心數+1個線程的線程池

IO密集型:任務線程並不是一直在執行任務,則應配置儘可能多的線程,如CPU核心數*2

             參考公式:CPU核心數 / (1 - 阻塞係數)    阻塞係數在0.8~0.9之間

             比如:8核CPU  8 / (1 -0.9) = 80 個線程數

問題5:線程池的submit和execute方法區別?

  1. 接收的參數不一樣
  2. submit有返回值,而execute沒有
  3. submit方便Exception處理

死鎖

是指兩個或兩個以上的進程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力干涉那他們都將無法推進下去,如果系統資源充足,進程的資源請求能夠得到滿足,死鎖出現的可能性就很低,否則會因爭奪有限的資源而陷入死鎖。

產生死鎖的原因:

  • 系統資源不足
  • 進程運行推進的順序不合適
  • 資源分配不當

問題排查:

  • jps命令定位進程號
  • jstack找到思索查看堆棧軌跡

 

 

 

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