無處不在的線程,多線程,阻塞隊列,並發
編程世界無新鮮事,看你FQ翻得厲不厲害
場景:現在的軟件開發迭代速度(一周一更新,甚至一天一發布)真是太快了,今天進行軟件更新的時候,看到了有趣的現象,這不就是線程池,ThreadPoolExecutor,阻塞隊列,任務(下載和安裝)最好的案例嘛!經常看到很多博文在寫多線程,並發,隊列,卻舉不出現實生活的場景例子,都在背書嗎(天下文章一大抄,看你會抄不會抄)。
現象圖示:
我開啟了全部更新38個要更新的app,可最多時看到了3個在同時下載,剩下的下載任務在排隊(隊列),安裝過程中,明明已經下載了多個app,可同一時刻只有一個在安裝,其他下載好的app也在排隊
0. 線程thread
說起線程,不得不提起進程,
線程,還真不好下定義,你要問十個人就會有十種答案,我就當線程就是一個可以執行的任務程序(比如上面圖片里的下載和安裝)。java里線程主要通過繼承java.lang.Thread類或實現java.lang.Runnable接口,其實Thread也是實現了Runnable接口的類,所有,線程還是圍繞着java.lang.Thread類擴展包裝,比如下面要要說的線程池。核心類如下
一個線程創建之后,總是處於其生命周期的4個狀態之一中。線程的狀態表明此線 程當前正在進行的活動,而線程的狀態是可以通過程序來進行控制的,就是說,可以對線程進行操作來改變狀態。這些操作包括啟動(start)、終止(stop)、睡眠(sleep)、掛起 (suspend)、恢復(resume)、等待(wait)和通知(notify)。每一個操作都對應了一個方法,這些方法是由java.lang提供的。
線程狀態在Java中是通過一個Thread的內部枚舉State標識的。
創建狀態(Thread.State.NEW)
如果創建了一個線程而沒有啟動它,那么,此線程就處於創建狀態。比如,下述語句執行 以后,使系統有了一個處於創建狀態的線程myThread:
Thread t= new ThreadClass();
其中,ThreadClass()是Thread的子類,而Thread是由java.lang提供的。
處於創建狀態的線程還沒有獲得應有的資源,所以,這是一個空的線程。線程只有通過啟動后,系統才會為它分配資源。對處於創建狀態的線程可以進行兩種操作:一是啟動 (start)操作,使其進入可運行狀態,二是終止(stop)操作,使其進入消亡狀態。如果進入到消 亡狀態,那么,此后這個線程就不能進入其他狀態,也就是說,它不再存在了。
start方法是對應啟動操作的方法,其具體功能是為線程分配必要的系統資源;將線程設置為可運行狀態,從而可以使系統調度這個線程。
通過調用t.start()啟動一個線程,使該線程進入可運行(Thread.State.RUNNABLE)的狀態。
由JVM的決定去調度(Scheduler) 在可運行狀態(Runnable)下的線程,使該線程處於運行 (Running) 狀態,由於JVM的調度會出現不可控性,即不是優先級高的先被調用,可能先調用,也可能后調用的的情況。運行狀態(Running)下,調用禮讓yield()方法,可以使線程回到可運行狀態(Runnable)下,再次JVM的調度(並不依賴優先級)。
線程執行完畢或異常退出會進入終止狀態(Thread.State.TERMINATED)。
其余的還有幾個狀態:
Thread.State.BLOCKED
受阻塞並且正在等待監視器鎖的某一線程的線程狀態。處於受阻塞狀態的某一線程正在等待監視器鎖,以便進入一個同步的塊/方法,或者在調用 Object.wait 之后再次進入同步的塊/方法。
Thread.State.WAITING
某一等待線程的線程狀態。某一線程因為調用下列方法之一而處於等待狀態:
不帶超時值的 Object.wait
不帶超時值的 Thread.join
LockSupport.park
處於等待狀態的線程正等待另一個線程,以執行特定操作。 例如,已經在某一對象上調用了 Object.wait() 的線程正等待另一個線程,以便在該對象上調用 Object.notify() 或 Object.notifyAll()。已經調用了 Thread.join() 的線程正在等待指定線程終止。
TIMED_WAITING具有指定等待時間的某一等待線程的線程狀態。某一線程因為調用以下帶有指定正等待時間的方法之一而處於定時等待狀態:
Thread.sleep
帶有超時值的 Object.wait
帶有超時值的 Thread.join
LockSupport.parkNanos
LockSupport.parkUntil
謹記: 在給定時間點上,一個線程只會處於一種狀態,狀態轉換圖
線程優先級
java線程的優先級用整數表示,取值范圍是1~10,Thread類有以下三個靜態常量:
static int MAX_PRIORITY
線程可以具有的最高優先級,取值為10。
static int MIN_PRIORITY
線程可以具有的最低優先級,取值為1。
static int NORM_PRIORITY
分配給線程的默認優先級,取值為5。
Thread類的setPriority()和getPriority()方法分別用來設置和獲取線程的優先級。
每個線程都有默認的優先級,主線程的默認優先級為Thread.NORM_PRIORITY。
線程的優先級有繼承關系,比如A線程中創建了B線程,那么B將和A具有相同的優先級。
JVM提供了10個線程優先級,但與常見的操作系統都不能很好的映射。如果希望程序能移植到各個操作系統中,應該僅僅使用Thread類有以下三個靜態常量作為優先級,這樣能保證同樣的優先級采用了同樣的調度方式。
1. 阻塞隊列
BlockingQueue隊列是一種數據結構,它有兩個基本操作:在隊列尾部加人一個元素,和從隊列頭部移除一個元素就是說,隊列以一種先進先出的方式管理數據,如果你試圖向一個已經滿了的阻塞隊列中添加一個元素或者是從一個空的阻塞隊列中移除一個元素,將導致線程阻塞。
在多線程進行合作時,阻塞隊列是很有用的工具。工作者線程可以定期地把中間結果存到阻塞隊列中而其他工作者線程把中間結果取出並在將來修改它們。隊列會自動平衡負載。如果第一個線程集運行得比第二個慢,則第二個線程集在等待結果時就會阻塞。如果第一個線程集運行得快,那么它將等待第二個線程集趕上來。
而BlockingQueue隊列也是一組數據集合,它繼承了Queue接口,而Queue接口繼承了Collection接口。
阻塞隊列提供的相關操作和特點
在java包"java.util.concurrent"中提供了若干種隊列,大神給你寫好了
ArrayBlockingQueue |
一個由數組結構組成的有界阻塞隊列 |
LinkedBlockingQueue | 一個由鏈表結構組成的有界阻塞隊列 |
PriorityBlockingQueue | 一個支持優先級排序的無界阻塞隊列 |
DelayQueue | 一個使用優先級隊列實現的無界阻塞隊列 |
SynchronousQueue |
一個不存儲元素的阻塞隊列 |
LinkedTransferQueue |
一個由鏈表結構組成的無界阻塞隊列 |
LinkedBlockingDeque |
一個由鏈表結構組成的雙向阻塞隊列 |
ArrayBlockingQueue
底層用數組實現的有界阻塞隊列,默認情況下不保證線程公平的訪問隊列(按照阻塞的先后順序訪問隊列),隊列可用的時候,阻塞的線程都可以爭奪隊列的訪問資格,當然也可以使用以下的構造方法創建一個公平的阻塞隊列。ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10, true)。
偽代碼:
(其實就是通過將ReentrantLock設置為true來 達到這種公平性的:即等待時間最長的線程會先操作)。用ReentrantLock condition 實現阻塞。
有界就是隊列的長度有限制,例如數組隊列,在構建的時候就指定了長度。無界就是可以無限地添加。
LinkedBlockingQueue
底層基於鏈表實現的有界阻塞隊列。此隊列的默認和最大長度為Integer.MAX_VALUE。此隊列按照先進先出的原則對元素進行排序。這個隊列的實現原理和ArrayBlockingQueue實現基本相同。也是采用ReentrantLock 控制並發,不同的是它使用兩個獨占鎖來控制消費和生產。即用takeLock和putlock,這樣的好處是消費者和生產者可以並發執行,對吞吐量有提升。
PriorityBlockingQueue
PriorityBlockingQueue是一個帶優先級的隊列,而不是先進先出隊列。元素按優先級順序被移除,該隊列也沒有上限(PriorityBlockingQueue是對 PriorityQueue的再次包裝,是基於堆數據結構的,而PriorityQueue是沒有容量限制的,與ArrayList一樣,所以在優先阻塞 隊列上put時是不會受阻的。雖然此隊列邏輯上是無界的,但是由於資源被耗盡,所以試圖執行添加操作可能會導致 OutOfMemoryError),但是如果隊列為空,那么取元素的操作take就會阻塞,所以它的檢索操作take是受阻的。也是用ReentrantLock控制並發。
DelayQueue
DelayQueue是在PriorityQueue基礎上實現的,底層也是數組構造方法,是一個存放Delayed 元素的無界阻塞隊列,只有在延遲期滿時才能從中提取元素。該隊列的頭部是延遲期滿后保存時間最長的 Delayed 元素。如果延遲都還沒有期滿,則隊列沒有頭部,並且poll將返回null。當一個元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一個小於或等於零的值時,則出現期滿,poll就移除這個元素了。此隊列不允許使用 null 元素。
SynchronousQueue
一個沒有容量的隊列 ,不會存儲數據,每執行一次put就要執行一次take,否則就會阻塞。未使用鎖。通過cas實現,吞吐量異常高。內部采用的就是ArrayBlockingQueue的阻塞隊列,所以在功能上完全可以用ArrayBlockingQueue替換,但是SynchronousQueue是輕量級的,SynchronousQueue不具有任何內部容量,我們可以用來在線程間安全的交換單一元素。所以功能比較單一,優勢就在於輕量。
LinkedBlockingDeque
LinkedBlockingDeque是雙向鏈表實現的雙向並發阻塞隊列。該阻塞隊列同時支持FIFO和FILO兩種操作方式,即可以從隊列的頭和尾同時操作(插入/刪除);並且,該阻塞隊列是支持線程安全,當多線程競爭同一個資源時,某線程獲取到該資源之后,其它線程需要阻塞等待。此外,LinkedBlockingDeque還是可選容量的(防止過度膨脹),即可以指定隊列的容量。如果不指定,默認容量大小等於Integer.MAX_VALUE。
LinkedTransferQueue
jdk7才提供這個類,這個類實現了TransferQueue接口,也是基於鏈表的,對於所有給定的生產者都是先入先出的。與其他阻塞隊列的區別是:其他阻塞隊列,生產者生產數據,如果隊列沒有滿,放下數據就走,消費者獲取數據,看到有數據獲取數據就走。而LinkedTransferQueue生產者放數據的時候,如果此時消費者沒有獲取,則需阻塞等待直到有消費者過來獲取數據。有點類似SynchronousQueue,但是LinkedTransferQueue是被設計有容量的。LinkedTransferQueue 通過使用CAS來實現並發控制,是一個無界的安全隊列。其長度可以無限延伸,當然帶來的問題也是顯而易見的。
2. 線程池ThreadPool
線程池,可以理解為存放線程的容器。
既然可以通過new出線程,那為什么要線程池呢,因為有以下優點:
(1)重用存在的線程,減少對象創建、消亡的開銷,性能佳。
(2)可有效控制最大並發線程數,提高系統資源的使用率,同時避免過多資源競爭,避免堵塞。
(3)提供定時執行、定期執行、單線程、並發數控制等功能。
而單獨建立線程(特別是項目組開發人員多的時候,各創建各自的線程),卻有以下缺點:
(1) 每次new Thread新建對象性能差。因為每次都會創建一個對象。這是既耗時又消耗資源的。
(2) 線程缺乏統一管理,可能會造成自鎖,或者是內存溢出。
(3)缺乏更多功能,如定時執行、定期執行、線程中斷。
Java通過Executors提供四種線程池,分別為:
newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
newFixedThreadPool 創建一個定長線程池,可控制線程最大並發數,超出的線程會在隊列中等待。
newScheduledThreadPool 創建一個定長線程池,支持定時及周期性任務執行。
newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
不過這幾種創建線程池方便,可隱藏了細節也不好,既然四種創建線程池最后是通過java.util.concurrent.ThreadPoolExecutor類構造,不如直接考察它,代碼如下:
請務必搞懂方方法里的參數:
(1)int corePoolSize(核心線程數):
線程池新建線程的時候,如果當前線程總數小於corePoolSize,則新建的是核心線程,核心線程默認情況下會一直存活在線程池中;如果設置了 allowCoreThreadTimeOut 為 true,那么核心線程如果不干活的話,超過一定時間,就會被銷毀掉。
(2)int maximumPoolSize(線程池能容納的最大線程數量):
線程總數 = 核心線程數 + 非核心線程數。
(3)long keepAliveTime(非核心線程空閑存活時長):
非核心線程空閑時長超過該時長將會被回收
(4)TimeUnit unit 空閑線程的存活時間
在這里表示的是時間的單位,比如說秒。
(5)BlockingQueue workQueue(任務隊列),就上面說的幾種隊列:
當所有的核心線程都在干活時,新添加的任務會被添加到這個隊列中等待處理,如果隊列滿了,則新建非核心線程執行任務。常用的workQueue類型:
ArrayBlockingQueue:這里表示接到新任務,如果沒有達到核心線程數,則新建核心線程執行任務,如果達到了,則入隊等候,如果隊列已滿,則新建非核心線程執行任務,又如果總線程數到了 maximumPoolSize,並且隊列也滿了,則發生錯誤。
LinkedBlockingQueue:這里表示接到新任務,如果當前線程數小於核心線程數,則新建核心線程處理任務;如果當前線程數等於核心線程數,則進入隊列等待。
DelayQueue:這里表示接到新任務,先入隊,達到了指定的延時時間,才執行任務。
SynchornousQueue:這里表示接到新任務,直接交給線程處理,如果其他的線程都在工作,那就創建一個新的線程來處理這個任務。
(6).ThreadFactory threadFactory(線程工廠):
用來創建線程池中的線程。
(7).RejectedExecutionHandler handler(拒絕策略):
指的之超過了maximumPoolSize,無法再處理新的任務,就會直接拒絕,提供了以下 4 種策略:
AbortPolicy:默認策略,在拒絕任務時,會拋出RejectedExecutionException。
CallerRunsPolicy:只要線程池未關閉,該策略直接在調用者線程中,運行當前的被丟棄的任務。
DiscardOldestPolicy:該策略將丟棄最老的一個請求,也就是即將被執行的任務,並嘗試再次提交當前任務。
DiscardPolicy:該策略默默的丟棄無法處理的任務,不予任何處理
附圖兩張:
找到ThreadPoolExecutor和BlockingQueue了嗎
3. 進入正題:下載和安裝
普及了0,1,2后,開始說正事,怕直接說多線程並發,隊列,任務,不好接受。
最后代碼如下:
測試下載安裝(可以自定義線程池執行器,自定義工廠,自定義策略)
執行效果圖
總結:好好學習,天天向上,寫也很累(有時候構思一篇文章兩三個小時很快就過去了,甚至一下午),也是思考的過程,看別人的文章像過天書一樣和聽別人講座,容易忘記(可以問問你的朋友同學同事等),要自己匯總,記憶會更深刻。
有時間了接着拆分!!!
擴展閱讀參考:
0. https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html
1. 能有比官方更權威的嗎 https://docs.oracle.com/javase/tutorial/essential/concurrency/runthread.html
2. Java Concurrency in Practice http://jcip.net/, 國內已有翻譯版
3. http://gee.cs.oswego.edu/ 你們用的並發包java.util.concurrent(簡稱JUC),便出於他和其他人之手,經常看到國內的碼農在拼命研究並發源碼(也有研究spring源碼的),不知道到什么程度了,了解他這個人嗎。。。擔心就算看懂了代碼(我想只要是個碼農都能看得懂),也只是看懂了代碼,了解他的思想嗎,他當時是如何籌划構思出來的,別人的技術,能學通50%(畢竟人家花了二十年循序漸進才做出來的庫和框架比如spring,http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html,是經驗和閱歷的成果),能靈活運用就算成功
******************************************************************************************************
4. Java阻塞隊列實現原理分析 https://developer.51cto.com/art/201704/536821.htm
5. 聊聊並發(七)——Java 中的阻塞隊列 https://www.infoq.cn/article/java-blocking-queue/
6. Java線程池架構原理和源碼解析(ThreadPoolExecutor)https://mp.weixin.qq.com/s?__biz=MjM5NTg2NTU0Ng==&mid=214688037&idx=6&sn=d1c989e7f539732cda5ceaa6cabd8b29
7. Java線程池使用說明 https://mp.weixin.qq.com/s?__biz=MzIyNjA1MjAyNg==&mid=212879024&idx=1&sn=a05bf0b28846850a0730e5844f95126d
8. Java線程池---Executor框架源碼深度解析https://mp.weixin.qq.com/s?__biz=MzUyNDk2MTE1Mg==&mid=2247483663&idx=1&sn=cd57cf503c31eb6e4a423173520081f7
9. 深入源碼分析Java線程池的實現原理https://mp.weixin.qq.com/s/-89-CcDnSLBYy3THmcLEdQ
10. 深度解讀 Java 線程池設計思想及源碼實現 https://mp.weixin.qq.com/s?__biz=MzUxNDA1NDI3OA==&mid=2247486041&idx=1&sn=2bc12fd0b57bedb84eb11aca8a574306
11. 探索Java 同步機制 https://www.ibm.com/developerworks/cn/java/j-lo-synchronized/