Java併發與多線程(3):線程池、等待喚醒

一、等待喚醒機制

1、線程間的通信

概念:多個線程在處理同一個資源,但是處理的動作(線程的任務)卻不相同。
比如:線程A用來生成包子的,線程B用來喫包子的,包子可以理解爲同一資源,線程A與線程B處理的動作,一個是生產,一個是消費,那麼線程A與線程B之間就存在線程通信問題。
在這裏插入圖片描述
(1)爲什麼要處理線程間通信:
多個線程併發執行時, 在默認情況下CPU是隨機切換線程的,當我們需要多個線程來共同完成一件任務,並且我們希望他們有規律的執行, 那麼多線程之間需要一些協調通信,以此來幫我們達到多線程共同操作一份數據。
(2)如何保證線程間通信有效利用資源:
就是多個線程在操作同一份數據時, 避免對同一共享變量的爭奪(效率至上)。也就是我們需要通過一定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制

2、等待喚醒機制

這是多個線程間的一種協作機制。談到線程我們經常想到的是線程間的競爭(race),比如去爭奪鎖,但這並不是故事的全部,線程間也會有協作機制。就好比在公司裏你和你的同事們,你們可能存在在晉升時的競爭,但更多時候你們更多是一起合作以完成某些任務。

(1)wait/notify 就是線程間的一種協作機制。
就是在一個線程進行了規定操作後,就進入等待狀態(wait()),等待其他線程執行完他們的指定代碼過後 再將其喚醒(notify());在有多個線程進行等待時, 如果需要,可以使用 notifyAll()來喚醒所有的等待線程

(2)等待喚醒中的方法
等待喚醒機制就是用於解決線程間通信的問題的,使用到的3個方法的含義如下:

  1. wait:線程不再活動,不再參與調度,進入 wait set (集合)中,因此不會浪費 CPU 資源,也不會去競爭鎖了,這時的線程狀態即是 WAITING。它還要等着別的線程執行一個特別的動作,也即是“通知(notify)”在這個對象上等待的線程從wait set 中釋放出來,重新進入到調度隊列(ready queue)中
  2. notify:則選取所通知對象的 wait set 中的一個線程(隊列存儲)釋放;例如,餐館有空位置後,等候就餐最久的顧客最先入座。
  3. notifyAll:則釋放所通知對象的 wait set 上的全部線程。

注意:
哪怕只通知了一個等待的線程,被通知線程也不能立即恢復執行,因爲它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖,所以她需要再次嘗試去獲取鎖(很可能面臨其它線程的競爭),成功後才能在當初調用 wait 方法之後的地方恢復執行。

總結如下:
(1)如果能獲取鎖,線程就從 WAITING 狀態變成 RUNNABLE 狀態;
(2)否則,從 wait set 出來,又進入 entry set,線程就從 WAITING 狀態又變成 BLOCKED 狀態

調用鎖對象的wait和notify方法需要注意的細節

  1. wait方法與notify方法必須要由同一個鎖對象調用。因爲:對應的鎖對象可以通過notify喚醒使用同一個鎖對
    象調用的wait方法後的線程。
  2. wait方法與notify方法是屬於Object類的方法的。因爲:鎖對象可以是任意對象,而任意對象的所屬類都是繼
    承了Object類的。
  3. wait方法與notify方法必須要在同步代碼塊或者是同步函數中使用。因爲:必須要通過鎖對象調用這2個方
    法。

3、生產者與消費者問題

等待喚醒機制其實就是經典的“生產者與消費者”的問題。

等待喚醒機制如何有效利用資源:
包子鋪線程生產包子,喫貨線程消費包子。當包子沒有時(包子狀態爲false),喫貨線程等待,包子鋪線程生產包子
(即包子狀態爲true),並通知喫貨線程(解除喫貨的等待狀態),因爲已經有包子了,那麼包子鋪線程進入等待狀態。

接下來,喫貨線程能否進一步執行則取決於鎖的獲取情況。如果喫貨獲取到鎖,那麼就執行喫包子動作,包子喫完(包子狀態爲false),並通知包子鋪線程(解除包子鋪的等待狀態),喫貨線程進入等待。包子鋪線程能否進一步執行則取決於鎖的獲取情況。
在這裏插入圖片描述
代碼演示:

/**
 * 包子類
 * @author Mango
 */
public class BaoZi {

    String pi;
    String xian;
    boolean flag = false;

}
/**
 * 包子鋪
 */
public class BaoZiShop extends Thread{

    private BaoZi baoZi;

    public BaoZiShop(String name, BaoZi baoZi) {
        super(name);
        this.baoZi = baoZi;
    }

    @Override
    public void run() {
        int count = 0;
        //造包子
        while(true) {
            synchronized (baoZi) {
                if (baoZi.flag) {
                    try {
                        baoZi.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                //沒有包子 造包子
                System.out.println("包子鋪開始做包子了");
                if(count %2 ==0) {
                    baoZi.pi = "冰皮";
                    baoZi.xian = "五仁";
                }else {
                    baoZi.pi = "薄皮";
                    baoZi.xian = "牛肉";
                }
                count++;

                baoZi.flag = true;
                System.out.println(baoZi.pi + baoZi.xian + "造好了");
                baoZi.notify();
            }
        }
    }
}
/**
 * 顧客
 * @author Mango
 */
public class Customer extends Thread{

    private BaoZi baoZi;

    public Customer(String name,BaoZi baoZi) {
        super(name);
        this.baoZi = baoZi;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (baoZi) {
                if(!baoZi.flag) {
                    //沒包子
                    try {
                        baoZi.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("顧客正在喫" + baoZi.pi + baoZi.xian + "包子");
                baoZi.flag = false;
                baoZi.notify();
            }
        }
    }

}

二、線程池

1、線程池思想概述

如果併發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低
系統的效率,因爲頻繁創建線程和銷燬線程需要時間

那麼有沒有一種辦法使得線程可以複用,就是執行完一個任務,並不被銷燬,而是可以繼續執行其他的任務?在Java中可以通過與連接池類似的線程池來達到這樣的效果。

2、線程池概念

線程池:其實就是一個容納多個線程的容器,其中的線程可以反覆使用,省去了頻繁創建線程對象的操作,無需反覆創建線程而消耗過多資源。

線程池原理如圖:
在這裏插入圖片描述
合理利用線程池能夠帶來三個好處:

  1. 降低資源消耗。減少了線程創建銷燬的過程(j節約資源),每個工作線程都可以被重複利用,可執行多個任務。
  2. 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  3. 提高線程的可管理性。可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因爲消耗過多的內
    存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最後死機)。

3、線程池的使用

Java裏面線程池的頂級接口是java.util.concurrent.Executor ,但是嚴格意義上講Executor 並不是一個線程
池,而只是一個執行線程的工具。真正的線程池接口是java.util.concurrent.ExecutorService 。
在這裏插入圖片描述
要配置一個線程池是比較複雜的,尤其是對於線程池的原理不是很清楚的情況下,很有可能配置的線程池不是較優的,因此在java.util.concurrent.Executors 線程工廠類裏面提供了一些靜態工廠,生成一些常用的線程池。官方建議使用Executors工程類來創建線程池對象。

Executors類中有個創建線程池的方法如下:
(1)public static ExecutorService newFixedThreadPool(int nThreads) :返回線程池對象。(創建的是有界線
程池,也就是池中的線程個數可以指定最大數量)

獲取到了一個線程池ExecutorService 對象,那麼怎麼使用呢,在這裏定義了一個使用線程池對象的方法如下:
(1)public Future<?> submit(Runnable task) :獲取線程池中的某一個線程對象,並執行Future接口:用來記錄線程任務執行完畢後產生的結果。線程池創建與使用。

使用線程池中線程對象的步驟:

  1. 創建線程池對象。
  2. 創建Runnable接口子類對象。(task)
  3. 提交Runnable接口子類對象。(take task)
  4. 關閉線程池(一般不做)。

Runnable實現類代碼:

/**
 * 創建一個Runnable接口的實現類
 * @author Mango
 */
public class DomeRunnable implements Runnable {

    /**
     * 重寫接口的run方法,設置線程任務
     */
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i );
        }
    }
}
/**
 * 測試線程池
 */
public class Demo2 {
    public static void main(String[] args) {
        //創建線程池
        ExecutorService service = Executors.newFixedThreadPool(2);
		//創建Runnable實例對象
        DomeRunnable runnable = new DomeRunnable();

		//從線程池中獲取線程對象,然後調用runnable對象中的run()
        service.submit(runnable);
        service.submit(runnable);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章