
銀文傑
本書主要對Java集合框架(JCF)和Java併發工具包(JUC)進行介紹,包括它們的適用場景、使用方法、技術理論和運行原理。爲了讓讀者能夠輕鬆閱讀本書,本書中所有內容都採用由淺入深的方式進行介紹,先保證讀者會用這些技術,再介紹這些技術的運行原理。本書分爲3部分,第1部分爲Java編程入門知識,方便初學者對JCF相關知識進行查漏補缺,第2 部分和第3部分對基礎知識有一定的要求,適合有一些Java編程基礎的技術人員閱讀。
這本書作爲爲數不多講併發和集合的書,對我幫助很大。筆記如下: 進程與線程之間的區別?進程是資源分配最小單位,線程是程序執行的最小單位 進程有自己獨立的地址空間,每啓動一個進程,系統都會爲其分配地址空間,建立數據表來維護代碼段、堆棧段和數據段,線程沒有獨立的地址空間,它使用相同的地址空間共享數據 CPU切換一個線程比切換進程花費小 創建一個線程比進程開銷小 線程佔用的資源要⽐進程少很多 線程之間通信更方便,同一個進程下,線程共享全局變量,靜態變量等數據,進程之間的通信需要以通信的方式(IPC)進行;(但多線程程序處理好同步與互斥是個難點) 多進程程序更安全,生命力更強,一個進程死掉不會對另一個進程造成影響(源於有獨立的地址空間),多線程程序更不易維護,一個線程死掉,整個進程就死掉了(因爲共享地址空間) 進程對資源保護要求高,開銷大,效率相對較低,線程資源保護要求不高,但開銷小,效率高,可頻繁切換 線程在進程下行進(單純的車廂無法運行) 一個進程可以包含多個線程(一輛火車可以有多個車廂) 不同進程間數據很難共享(一輛火車上的乘客很難換到另外一輛火車,比如站點換乘) 同一進程下不同線程間數據很易共享(A車廂換到B車廂很容易) 進程要比線程消耗更多的計算機資源(採用多列火車相比多個車廂更耗資源) 進程間不會相互影響,一個線程掛掉將導致整個進程掛掉(一列火車不會影響到另外一列火車,但是如果一列火車上中間的一節車廂着火了,將影響到所有車廂) 進程可以拓展到多機,進程最多適合多核(不同火車可以開在多個軌道上,同一火車的車廂不能在行進的不同的軌道上) 進程使用的內存地址可以上鎖,即一個線程使用某些共享內存時,其他線程必須等它結束,才能使用這一塊內存。(比如火車上的洗手間)-”互斥鎖” 進程使用的內存地址可以限定使用量(比如火車上的餐廳,最多隻允許多少人進入,如果滿了需要在門口等,等有人出來了才能進去)-“信號量” 什麼是線程池?爲什麼要使用線程池?1. 從科技技術發展的角度來看,摩爾定律受制於現有工藝水平遇到了無法突破的瓶頸,多核 CPU 並行計算提升服務器的性能成爲了主流,隨之出現了多線程藝術2. 從業務開發的角度來看,假如一個方法的調用鏈過長,會導致程序的運行時間過長,進而導致系統的吞吐量過長,所以我們採用多線程對方法的調用鏈進行優化,對於調用關係耦合度不是太高採用多線程走異步的方式進行優化,這樣可以降低程序的執行的時間,對於多線程我們創建線程是採用的 new Thread() 的方式去創建對象,但是這樣的方式是有弊端的 1. 弊端1 會有 OOM 內存溢出問題:比如當前方法突然遇上高併發請求,當前方法遇上 5000 個請求,如果當前網絡 IO 模型是 BIO 模型的話,BIO 是同步阻塞模式,就是服務器實現模式是一個連接一個線程,在不考慮 web 容器開啓最大線程數的配置下,這個時候服務器會開啓 5000 個線程去處理請求,執行請求的 5000 個線程去調用執行方法發現裏面有 new Thead() ,那麼這樣會創建 5000 個線程,這個時候總共爲 5000 * 2 = 10000 條線程,而創建線程是需要向 JVM 申請內存的,所以大量線程瞬間向 JVM 申請內存,會造成 OOM 內存溢出問題 2. 弊端二 會有資源開銷和耗時問題:一個對象的生命週期分爲對象的創建、對象的使用、對象的回收,真正有效的時間是對象的使用,同理線程作爲類的實例,真正有效的時間是線程的工作時間,而線程的創建和銷燬是非常消耗時間。如果線程數量非常多的情況下,可能會導致線程創建和銷燬的時機遠大於線程執行的時間,這個開銷有點虧,另外 JVM 垃圾回收這些線程,會導致回收的停頓時間延長,最後,線程的創建相對於普通的對象要更爲複雜,一是除了初始化堆空間,還多了一個至少分配 1MB 的棧空間,而是創建線程需要在操作系統內核註冊本地線程,這個過程會涉及系統調用,而系統調用是有開銷非常大的 3. 弊端三 會有線程不可管理性,因爲創建線程後是不可管理的,一旦 CPU 調度成功,這個線程的可管理性幾乎爲零3. 所以,誕生了線程池去解決這些弊端,線程池採用池化的思想,一旦有任務,無需去創建一個線程去執行,而是線程池裏面已經存在的線程去執行任務,執行完任務之後,直接退回到線程池即可,不需要回收,所以線程池的好處很明顯 1. 一是提高任務的執行速度,換句話說提高系統的響應速度,任務直接交給已經創建的線程去執行 2. 而是減少資源開銷,因爲不需要頻繁地創建線程和銷燬線程了,也沒有大量的 CPU 上下文切換開銷 3. 三是提高線程的管理性,線程池可以對線程進行管理(調整核心線程數、最大線程數)、監控、調優等 4. 四是降低使用的複雜度,將任務的提交和執行解耦,任務直接提交給線程池即可。具體的執行流程由線程池來做,用戶無需關心4. 一個完整的線程池,應該包含了以下幾個核心部分 1. 任務提交:提供接口接受任務的提交 2. 任務管理: 選擇合適的隊列對提交的任務進行管理,包括對拒絕策略的設置 3. 任務執行:由工作線程來執行提交的任務 4. 線程池管理:包括基本參數設置、任務監控、工作線程管理5. 頂級接口 Executor 只定義了 execute(Runnable command)方法解耦的任務的提交和執行,至於具體任務怎麼執行則交給他的實現者去自定義實現6. ExecutorService 接口繼承 Executor,在這個基礎上提供可以管理服務和執行結果的能力,ExecutorService 所提供的 submit 方法可以返回任務的執行結果,而 shutdown 可以用於關閉服務7. AbstractExecutorService 抽象類繼承 ExecutorService 接口,對 ExecutorService 相關方法提供了默認實現,用 RunnableFuture 的實現類 FutureTask 包裝 Runnable 任務,交給 execute() 方法去執行,然後可以從該 FutureTask 阻塞獲取執行結果,並且對批量任務的提交做了編排8. ThreadPoolExecutor 是線程池的最終實現類,定義了任務管理策略和線程池管理能力。 線程池有哪些核心參數 都有哪些核心參數?包括核心線程數(corePoolSize)、最大線程數(maximumPoolSize)、空閒線程超時時間(keepAliveTime)、時間單位(unit)、阻塞隊列(workerQueue)、拒絕策略(handler)、線程工廠(ThreadFactory) 當用戶線程提交任務時線程池的執行流程如下: 如果任務爲空,則拋出 NullPointerException 獲取當前線程池的狀態 + 線程個數變量的組合值,前 3 位表示狀態,後 29 位表示線程池個數 如果當前線程個數小於核心線程數,就新建一個線程來處理提交的任務 如果當前線程個數大於核心線程數,判斷線程池是否處於 Running 狀態,是則將任務添加到則阻塞隊列等待執行,這裏需要判斷線程池狀態是因爲可能線程池已經處於非 Running 狀態,而在非 Running 狀態下是要拋棄新任務的 如果添加任務成功,則再次獲取當前線程池的狀態 + 線程個數變量的組合值,這是因爲添加任務到線程池之後,線程池的狀態可能發生變化了 所以當前線程狀態不是 RUNNING 則把任務從阻塞隊列移除,並執行拒絕策略 如果二次校驗通過,則重新判斷當前線程池是否爲空,爲空則添加一個線程 如果執行到這個 if 說明添加任務失敗,隊列已滿,則嘗試開啓新線程來執行任務,如果當前線程數大於最大線程數則執行拒絕策略 以上這個執行流程是 JUC 標準線程池提供的執行流程,主要用在 CPU 密集型場景下 像 Tomcat、Dubbo 這類框架,它們內部的線程池主要用來處理網絡 IO 任務的,所以它們都對 JUC 線程池的執行流程進行了調整來支持 IO 密集型場景使用 它們提供了阻塞隊列 TaskQueue,該隊列繼承 LinkedBlockingQueue,重寫了 offer() 方法來調整執行的流程,如下: 如果 parent 爲 null,直接調用父類的 offer 方法,這裏的 parent 就是所屬的線程池對象 如果當前線程池等於最大線程數,則直接調用父類的 offer 方法入隊 如果當前未執行的任務數量小於當前線程數,說明有空閒線程數,那麼直接調用父類 offer 方法 入隊 如果當前線程數小於最大線程數,則直接返回 false,然後回到 JUC 線程池的執行流程 其他情況都直接入隊 如果 Tomcat 接收的請求數量大於核心線程數,請求就會被放到隊列中,等待覈心線程處理,如果併發量很大,就會在隊列中堆積大量任務,這樣會降低請求的總體響應速度 所以 Tomcat 並沒有使用 JUC 原生線程池,而是利用 TaskQueue 的 offer() 方法巧妙的修改了 JUC 的執行流程 判斷如果當前線程數 < 核心線程數,則新建一個線程來處理提交的任務 如果核心線程數 < 當前線程數 < 最大線程數,則創建新的線程執行提交的任務 如果當前線程數 >= 最大線程數,則將任務放入到任務隊列待執行 如果隊列已滿,則執行拒絕策略 而且 Tomcat 會做核心線程預熱,在創建好線程池接着會去創建核心線程並啓動,服務啓動後就可以接收客戶端請求進行處理了,避免了冷啓動問題 線程池的 Worker 線程模型,繼承 AQS 實現了鎖機制,線程啓動後執行 runWorker() 方法,runWorker() 方法調用 getTask() 方法從阻塞隊列中獲取任務,獲取到任務先執行 beforeExecute() 鉤子函數,再執行任務,然後執行 afterExecute() 鉤子函數,若超時獲取不到任務會調用 processWorkerExit() 方法執行 Worker 線程的清理工作。 Java 官方提供了哪幾種線程池,分別有什麼特點?newCachedThreadPool,是一種可以緩存的線程池,它可以用來處理大量短期的突發流量 1. 它的特點有三個 1. 最大線程數是 Integer.MaxValue 2. 線程存活時間是 60 s 3. 阻塞隊列用的是 SynchronousQueue,這是一種不存在任何元素的阻塞對象 4. 也就是每提交一個任務到線程池,都會分配一個工作線程來處理,由於最大線程數沒有限制 5. 所以它可以處理大量的任務,另外每個工作線程又可以存活 60 s,使得這些工作線程可以緩存起來應對更多任務的處理2. newFixedThreadPool,是一種固定線程數量的線程池 1. 它的特點是核心線程和最大線程數量都是一個固定的值 2. 沒有所謂的非空閒時間 3. 阻塞隊列爲無界隊列 LinkedBlockingQueue,可能會導致 OOM 4. FixedThreadPool 適用於處理 CPU 密集型的任務,確保 CPU 在長期被工作線程使用的情況下,儘可能少的分配,即適用於執行長期的任務 5. 如果任務比較多工作線程處理不過來,就會加入到阻塞裏面等待 6. 工作流程: 1. 提交任務,如果線程數少於核心線程數,創建核心線程執行任務 2. 如果線程數等於核心線程,把任務添加到 LinkedBlockingQueue 阻塞隊列 3. 如果線程執行完任務,去阻塞隊列去任務,繼續執行3. newSingleThreadPool,只有一個工作線程的線程池 1. 並且線程數量無法動態更改,因此可以保證所有的任務都按照 FIFO 的方式順序執行4. newScheduledThreadPool,具有延遲執行功能的線程池,適用與週期性執行任務的場景,需要限制線程數量的場景 1. 最大線程是 Integer.MAX_VALUE,也有 OOM 的風險 2. 阻塞隊列是 DelayedWorkQueue,keepAliveTime 爲 0 3. scheduleAtFixedRate():按某種速率週期執行 4. scheduleWithFixedDelay():在某個延遲後執行 5. 工作機制: 1. 線程從 DelayQueue 中獲取已到期的 ScheduledFuture 2. 線程執行這個 ScheduledFutureTask 3. 線程修改 ScheduledFutureTask的 time 變量爲下次將執行的時間 4. 線程把這個修改 time 之後的 ScheduledFutureTask 放回 DelayQueue 中5. newWorkStealingPool,Java8 裏面新加入的一個線程池 1. 它內部會構建一個 ForkJoinPool,利用工作竊取的算法並行處理請求6. 這些線程池都是通過工具類 Executors 來構建的,線程池的最終實現類是 ThreadPoolExecutor。 線程池提交 execute 和 submit 有什麼區別?從是否有返回值的角度來看,execute 用於提交不需要返回值的任務, submit() 方法用於提交需要返回值的任務,線程池會返回一個 future 的對象,通過這個 future 對象可以判斷任務是否執行成功,並且可以通過 future 的 get 方法來獲取返回值 execute() 無返回值,submit() 有返回值,會返回一個 FutureTask,然後調用 get() 方法阻塞獲取返回值 FutureTask 實現了 RunnableFuture 接口,而 RunnableFuture 繼承了 Runnbale、Future 接口,Runnbale 只定義了一個 run 抽象方法,並且接口上加了 @FunctionalInterface,表示該接口只能定義一個抽象方法 我們調用 submit() 方法提交的任務,會被包裝爲 FutureTask 對象,FutureTask 類提供了七種任務狀態和五個成員變量 分別是 初始態的 NEW、任務執行完成,還未設置任務結果的 COMPLETING 、任務執行結束後的狀態 NROMAL、任務執行中發生異常後的 EXCEPTIONAL、調用 cancel 方法執行成功後的 CANCELLED、任務正在被中斷時的 INTERRUPTING 狀態、任務執行被中斷後的 INTERRUPTED 狀態 volatile修飾的int類型的state變量、callable、Object 類型的 outcome、volatile 修飾的 Thread 的 runner 變量、volatile 修飾的 WaitNode 類型的 waiters, 創建 FutureTask 對象時 state 設置爲 NEW、callable 賦值爲我們傳入的任務 run 方法中會去執行 callable 任務,執行之前會去判斷任務處於 NEW 狀態並且通過 cas 設置 runner 爲當前線程成功,然後去調用 call方法 執行任務,執行成功後會調用 set 方法將結果賦值給 outcome 任務執行拋出異常會將異常信息調用 setException 賦值給 outcome 在 set 和 setException 這兩個方法中,都會先將狀態變爲 COMPLETING,再變爲 NORMAL 或者 EXCEPTIONAL,是爲了保證 NORMAL 或者 EXCEPTIONAL 已經完成了 outcome 賦值 finishCompletion 會通過 LockSupport.unpark 去喚醒因調用 get 而阻塞的線程 waiters 調用 get 方法會阻塞獲取結果,如果 state > COMPLETING,說明任務已經執行完成了,則直接通過 report() 方法返回結果或者拋出異常,反之 <= COMPLETING,說明任務還在執行中或者還未開始,則調用 awaitDone() 進行阻塞等待 awaitDone() 則通過 state 狀態判斷來決定直接返回還是將當前線程添加到 waiters 裏,然後調用 LockSupport.park 方法掛起當前線程 還有個重要的 cancel 方法,如果 state 不是 NEW 或者通過 CAS 賦值給 INTERRUPTING/CANCELLED 失敗則直接返回,反之如果 mayInterrupIfRunning = true,表示可能中斷在運行中線程,則中斷線程,state 變爲 INTERRUPTED,最後去喚醒等待的線程 從提交的任務類型角度來看,execute() 只能提交 Runnable 類型的任務,而 submit() 既能提交 Runnable 類型的任務,也能提交 Callable 類型的任務 從 API 層面理解,execute() 是在 Executor 接口中定義的,而submit() 方法是在 ExecutorService 接口中定義的,具體的實現是由 AbstarctExecutorService 進行 從處理異常的角度來看,execute 會直接拋出任務執行時的異常,可以使用 try catch 來捕獲,和普通線程的處理方式完全一致,而 submit 會喫掉異常,可以通過 Future 的 get 方法將任務執行時的異常重新拋出。
一本不錯的書,耐着性子讀完了,對於多線程,集合有了更深刻的認識。作者不僅僅講述了技術的原理還對其技術思想進行了講解,知其然知其所以然。美中不足的是內容難度大,沒照顧好一部分新手(說的就是我了),閱讀起來理解難度挺大,除此之外,本書的風格太過幹,閱讀起來很枯燥,比較考驗耐心。
深度不夠,通篇API,Vector 真的沒必要再講了
寫得好呀,幫我梳理清楚了很多問題
值得品讀,除了個別地方不夠嚴謹外,內容挺有深度,甚至比其他專門寫高併發的書寫得更深入