1、線程池ThreadPool相關
在java.util.concurrent包下,提供了一系列與線程池相關的類。合理的使用線程池,可以帶來多個好處:
(1)降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗;
(2)提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行;
(3)提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。
線程池可以應對突然大爆發量的訪問,通過有限個固定線程爲大量的操作服務,減少創建和銷燬線程所需的時間。
與線程執行、線程池相關類的關係如圖:
我們一般通過工具類Executors的靜態方法(如newFixedThreadPool())來獲取ThreadPoolExecutor線程池或靜態方法(如newScheduledThreadPool())來獲取ScheduleThreadPoolExecutor線程池。如下使用:
ExecutorService threadpool= Executors.newFixedThreadPool(10);
我們指定了獲取10個數量的固定線程池,Executors中有很多重載的獲取線程池的方法,比如可以通過自定義的ThreadFactory來爲每個創建出來的Thread設置更爲有意義的名稱。Executors創建線程池的方法內部也就是new出新的ThreadPoolExecutor或ScheduleThreadPoolExecutor,給我們配置了很多默認的設置。如下:
上面通過ThreadPoolExecutor的構造方法,爲我們創建了一個線程池,很多參數Executors工具類自動爲我們配置好了。創建一個ThreadPoolExecutor線程池一般需要以下幾個參數:
(1)corePoolSize(線程池的基本大小):當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閒的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大於線程池基本大小時就不再創建。如果調用了線程池的prestartAllCoreThreads方法,線程池會提前創建並啓動所有基本線程。
(2)maximumPoolSize(線程池最大大小):線程池允許創建的最大線程數。如果隊列滿了,並且已創建的線程數小於最大線程數,則線程池會再創建新的線程執行任務。值得注意的是如果使用了無界的任務隊列這個參數就沒什麼效果。
(3)keepAliveTime(線程活動保持時間):線程池的工作線程空閒後,保持存活的時間。所以如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高線程的利用率。
(4)TimeUnit(線程活動保持時間的單位):可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS)等。
(5)workQueue(任務隊列):用於保存等待執行的任務的阻塞隊列。 可以選擇以下幾個阻塞隊列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue
(6)threadFactory:用於設置創建線程的工廠,可以通過線程工廠給每個創建出來的線程設置更有意義的名字。
(7)handler(飽和策略):當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略默認情況下是AbortPolicy,表示無法處理新任務時拋出異常。
我們儘量優先使用Executors提供的靜態方法來創建線程池,如果Executors提供的方法無法滿足要求,再自己通過ThreadPoolExecutor類來創建線程池。
提交任務的兩種方式:
(1)通過execute()方法,如:
(2)通過submit()方法,如:
使用submit 方法來提交任務,它會返回一個Future對象,通過future的get方法來獲取返回值,get方法會阻塞住直到任務完成,而使用get(long
timeout, TimeUnit unit)方法則會阻塞一段時間後立即返回,這時有可能任務沒有執行完。
線程池工作流程分析:(來自參考文章)
從上圖我們可以看出,當提交一個新任務到線程池時,線程池的處理流程如下:
1、首先線程池判斷基本線程池是否已滿(< corePoolSize
?)?沒滿,創建一個工作線程來執行任務。滿了,則進入下個流程。
2、其次線程池判斷工作隊列是否已滿?沒滿,則將新提交的任務存儲在工作隊列裏。滿了,則進入下個流程。
3、最後線程池判斷整個線程池是否已滿(< maximumPoolSize ?)?沒滿,則創建一個新的工作線程來執行任務,滿了,則交給飽和策略來處理這個任務。
也就是說,線程池優先要創建出基本線程池大小(corePoolSize)的線程數量,沒有達到這個數量時,每次提交新任務都會直接創建一個新線程,當達到了基本線程數量後,又有新任務到達,優先放入等待隊列,如果隊列滿了,纔去創建新的線程(不能超過線程池的最大數maxmumPoolSize)。
關於線程池的配置原則可閱讀參考文章。
ThreadPoolExecutor簡單實例:
測試類:
打印結果如下:
pool-1-thread-1>存入:200
pool-1-thread-1>存入:200
pool-1-thread-2>取錢:200
pool-1-thread-1>存入:200
pool-1-thread-2>取錢:200
pool-1-thread-1>存入:200
pool-1-thread-2>取錢:200
pool-1-thread-1>存入:200
pool-1-thread-2>取錢:200
pool-1-thread-2>取錢:200
null
可以看到,打印出來的future.get()獲取的結果爲null,這是因爲Runnable是沒有返回值的,需要返回值要使用Callable,這裏就不再細說了,具體可參考如下文章:
2、生產者和消費者模型
生產者消費者模型,描述是:有一塊緩衝區作爲倉庫,生產者可以將產品放入倉庫,消費者可以從倉庫中取走產品。解決消費者和生產者問題的核心在於保證同一資源被多個線程併發訪問時的完整性。一般採用信號量或加鎖機制解決。下面介紹Java中解決生產者和消費者問題主要三種仿:
(1)wait() / notify()、notifyAll()
wait和notify方法是Object的兩個方法,因此每個類都會擁有這兩個方法。
wait()方法:使當前線程處於等待狀態,放棄鎖,讓其他線程執行。
notify()方法:喚醒其他等待同一個鎖的線程,放棄鎖,自己處於等待狀態。
如下例子:
生產者:
消費者:
測試類:
打印結果:
消費操作-->數量:30,庫存不足,消費阻塞!------庫存:0
生產操作-->數量:10,成功入庫~------庫存:10
生產操作-->數量:70,成功入庫~------庫存:80
生產操作-->數量:10,成功入庫~------庫存:90
生產操作-->數量:10,成功入庫~------庫存:100
生產操作-->數量:20,超出倉庫容量,生產阻塞!------庫存:100
消費操作-->數量:10,消費成功~------庫存:90
生產操作-->數量:20,成功入庫~------庫存:110
生產操作-->數量:10,超出倉庫容量,生產阻塞!------庫存:110
消費操作-->數量:20,消費成功~------庫存:90
消費操作-->數量:30,消費成功~------庫存:60
生產操作-->數量:10,成功入庫~------庫存:70
在倉庫中,喚醒我們使用的是notify()而沒有使用notifyAll(),是因爲在這裏,如果測試數據設置不當很容易造成死鎖(比如一下喚醒了所有的生產進程),因爲使用wait和notify有一個缺陷:
邏輯本應該要這樣設計的,在produce()操作後,只要喚醒等待同一把鎖的消費者進程,在consume()後,喚醒等待同一把鎖的生產者進程,而notify()或notifyAll()將生產者和消費者線程都喚醒了。下面的第二種方法可以解決這個問題。
wait和notify在“類消費者和生產者”問題上也很有用,比如,在A類的某個方法中調用了傳進來的B對象的一個方法,A類方法的後面代碼依賴於剛剛調用的B的返回值,但是B對象的這個方法是一個異步的操作,此時就可以在A方法中調用完B對象的方法後自我阻塞,即調用wait()方法,而在B對象的那個方法中,待異步操作完成後,調用notify(),喚醒處於等待同一鎖對象的線程。如下:
A類的某個方法中:
//運行到此處,說明是認證成功的,有兩種可能,一是運行速度很快調用notificationService.getXmppManager()後直接返回了結果,二是B中處理完了調用notify方法
Log.d(LOGTAG, "authenticated already. send SetTagsIQ now...");
B中處理完後:
//客戶端連接認證成功後,喚醒擁有xmppManager鎖的對象
synchronized (xmppManager) {
xmppManager.notifyAll();
}
(2)await() / signal()
在JDK1.5之引入concurrent包之後,新引入了await()和signal()方法來做同步,功能和wait()和notify()方法相同,可以完全取代,但await()和signal()需要和Lock機制(關於Lock機制前面已總結)結合使用,更加靈活。正如第一種所說,可以通過調用Lock的newCondition()方法依次獲取兩個條件變量,一個針對倉庫空的,一個針對倉庫滿的條件變量,通過添加變量進行同步控制。
修改倉庫類Storage:
打印結果:
消費操作-->數量:30,庫存不足,消費阻塞!------庫存:0
消費操作-->數量:10,庫存不足,消費阻塞!------庫存:0
消費操作-->數量:20,庫存不足,消費阻塞!------庫存:0
生產操作-->數量:70,成功入庫~------庫存:70
生產操作-->數量:10,成功入庫~------庫存:80
生產操作-->數量:10,成功入庫~------庫存:90
生產操作-->數量:10,成功入庫~------庫存:100
生產操作-->數量:10,超出倉庫容量,生產阻塞!------庫存:100
消費操作-->數量:30,消費成功~------庫存:70
消費操作-->數量:10,消費成功~------庫存:60
消費操作-->數量:20,消費成功~------庫存:40
生產操作-->數量:10,成功入庫~------庫存:50
生產操作-->數量:20,成功入庫~------庫存:70
使用await和signal後,加鎖解鎖操作就交給了Lock,不用再使用synchronized同步(具體可看前面總結的同步的實現方法),在produce中滿倉後阻塞,生產完後喚醒等待的消費線程,consume中庫存不足後阻塞,消費完後喚醒等待的生產者線程,表示可以消費了。
(3)BlockingQueue阻塞隊列方式
在上一節關於線程池的總結中,我們看到了要創建一個線程池如ThreadPoolExecutor,需要傳入一個任務隊列即BlockingQueue,BlockingQueue(接口)用於保存等待執行的任務的阻塞隊列。
可以選擇以下幾個阻塞隊列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue。
>ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按
FIFO(先進先出)原則對元素進行排序。
>LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO
(先進先出) 排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。
>SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
>PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。
BlockingQueue的所有實現類內部都是已經實現了同步的隊列,實現的方式採用的是上面介紹的第二種await()/signal() + Lock同步的機制。在生成阻塞隊列時,可以指定隊列大小。用於阻塞操作的方法主要爲:
put()方法:插入一個元素,如果超過容量則自我阻塞,等待喚醒;
take()方法:取走一個元素,如果容量不足了,自我阻塞,等待喚醒;
put和take內部自己實現了await和signal、lock的機制處理,不再需要我們做相應操作。修改Storage代碼如下:
打印結果:
消費操作--庫存不足!
消費操作--庫存不足!
消費操作--庫存不足!
生產操作-->數量:70,成功入庫~------庫存:45
消費操作-->數量:30,消費成功~------庫存:45
生產操作-->數量:10,成功入庫~------庫存:56
生產操作-->數量:20,成功入庫~------庫存:75
生產操作-->數量:10,成功入庫~------庫存:85
生產操作-->數量:10,成功入庫~------庫存:89
消費操作-->數量:10,消費成功~------庫存:60
生產操作-->數量:10,成功入庫~------庫存:70
消費操作-->數量:20,消費成功~------庫存:70
可以看到,Storage中produce和consume方法中我們直接通過put和take方法往容器中添加或移除產品即可,沒有進行邏輯控制(其實上面兩個方法中if都可以去掉,只是爲了打印效果才加上的),這是因爲BlockingQueue內部已經實現了,不需要我們再次控制。
同時,我們看到打印的庫存信息出現了不匹配,這個主要是因爲我們的打印語句Systm.out.println()沒有被同步導致的,因爲同步語句只是在put和take方法內部,而我們打印語句中使用了data這個共享變量。這裏因爲我們需要看效果,所以才加的打印語句,並不影響我們對BlockingQueue的使用。
因此,在Java中,使用BlockingQueue阻塞隊列的方式可以很方便的爲我們處理生產者消費則問題,推薦使用。
在我們的編程生涯中,我們自己要去寫生產者和消費者問題,多是前面第一種介紹的“類似消費者生產者問題”上。
解決生產者和消費者問題還有管道的方式,即在生產者和消費者之間建立一個管道緩衝區,Java中用PipedInputStream
/ PipedOutputStream實現,由於這種方式對於傳輸對象不易封裝,因此實用性不高,就不具體介紹了。
3、sleep和wait的區別
sleep是Thread的靜態方法,wait是Object的方法。兩個方法都會暫停當前線程
(1)sleep使當前線程阻塞,讓出CPU,給其他線程執行的機會;如果當前線程擁有鎖,不會釋放鎖,也即“睡着我也要擁有鎖”。睡眠時間一到,進入就緒狀態,如果當前CPU空閒,纔會繼續執行。
(2)wait方法調用後,當前線程進入阻塞狀態,進入到和該對象(即誰調用了wait()方法,如list.wait())相關的等待池中。,讓出CPU,給其他線程執行的機會;當超時間過了或者別的線程調用了notify()或notifyAll()方法時纔會喚醒當前等待同一把鎖的線程。
(3)wait方法必須要放在同步塊中,如syncbronized或Lock同步中。
所以sleep和wait的主要區別是:
sleep:保持鎖,睡眠時間到進入就緒狀態;
wait:釋放鎖,等待其他線程的notify操作或超時喚醒。