在分享線程的監(jiān)控之前,我們要來先講講線程的基礎知識,一般來說只要我們基礎牢固,在寫代碼的時候大部分情況下不容易犯錯。但在 Android 團隊人數達到幾十人甚至上百人的時候,我們就無法確保所有的同學都能按部就班的寫好代碼了,所以我們還是要有監(jiān)控,但光有監(jiān)控是不行的還需要有理論基礎,這樣的話出現了問題才能分析解決。有很多同學認為線程有什么好了解的,無非就是 synchronized 、volatile、newThread,啟動線程。很多同學可能連線程池和 lock 鎖都沒接觸過,說句實話早先年我也跟大家一樣,因為項目中用不上只能自己去看源碼學原理。
1. 上下文切換
在過去單 CPU 時代,單任務在一個時間點只能執(zhí)行單一程序。之后發(fā)展到多任務階段,計算機能在同一時間點并行執(zhí)行多任務或多進程。雖然并不是真正意義上的“同一時間點”,而是多個任務或進程共享一個 CPU,并交由操作系統(tǒng)來完成多任務間對 CPU 的運行切換,以使得每個任務都有機會獲得一定的時間片運行。再后來發(fā)展到多線程技術,使得在一個程序內部能擁有多個線程并行執(zhí)行。一個線程的執(zhí)行可以被認為是一個 CPU 在執(zhí)行該程序。當一個程序運行在多線程下,就好像有多個 CPU 在同時執(zhí)行該程序。多線程比多任務更加有挑戰(zhàn)。多線程是在同一個程序內部并行執(zhí)行,因此會對相同的內存空間進行并發(fā)讀寫操作。這可能是在單線程程序中從來不會遇到的問題。其中的一些錯誤也未必會在單 CPU 機器上出現,因為兩個線程從來不會得到真正的并行執(zhí)行。然而,更現代的計算機伴隨著多核 CPU 的出現,也就意味著 不同的線程能被不同的 CPU 核得到真正意義的并行執(zhí)行。所以,在多線程、多任務情況下,線程上下文切換是必須的,然而對于 CPU 架構設計中的概念,應先熟悉了解,這樣會有助于理解線程上下文切換原理。
多進程多線程在運行的過程中都離不開一個概念,那就是調度。JVM 虛擬機雖是跨平臺但是并未接管線程調度,調度還是由操作系統(tǒng)本身來決定,我們在下次看線程創(chuàng)建底層源碼便會知道。調度會涉及到一個上下文切換的概念,多任務多線程的本質其實就是 CPU 時間片的輪轉,在多任務處理系統(tǒng)中,CPU 需要處理所有程序的操作,當用戶來回切換它們時,需要記錄這些程序執(zhí)行到哪里。上下文切換就是這樣一個過程,允許 CPU 記錄并恢復各種正在運行程序的狀態(tài),使它能夠完成切換操作。簡單一點說就是指 CPU 從一個進程或線程切換到另一個進程或線程。
在上下文切換過程中,CPU 會停止處理當前運行的程序,并保存當前程序運行的具體位置以便之后繼續(xù)運行。從這個角度來看,上下文切換有點像我們同時閱讀幾本書,在來回切換書本的同時我們需要記住每本書當前讀到的頁碼。在程序中,上下文切換過程中的“頁碼”信息是保存在進程控制塊(PCB, process control block)中的。PCB 還經常被稱作“切換楨”(switchframe)?!绊摯a”信息會一直保存到 CPU 的內存中,直到他們被再次使用。PCB 通常是系統(tǒng)內存占用區(qū)中的一個連續(xù)存區(qū),它存放著操作系統(tǒng)用于描述進程情況及控制進程運行所需的全部信息,它使一個在多道程序環(huán)境下不能獨立運行的程序成為一個能獨立運行的基本單位或一個能與其他進程并發(fā)執(zhí)行的進程。
對于一個正在執(zhí)行的進程包括 程序計數器、寄存器、變量的當前值等 ,而這些數據都是 保存在 CPU 的寄存器中的,且這些寄存器只能是正在使用 CPU 的進程才能享用,在進程切換時,首先得保存上一個進程的這些數據(便于下次獲得 CPU 的使用權時從上次的中斷處開始繼續(xù)順序執(zhí)行,而不是返回到進程開始,否則每次進程重新獲得 CPU 時所處理的任務都是上一次的重復,可能永遠也到不了進程的結束出,因為一個進程幾乎不可能執(zhí)行完所有任務后才釋放 CPU ),然后將本次獲得 CPU 的進程的這些數據裝入 CPU 的寄存器從上次斷點處繼續(xù)執(zhí)行剩下的任務。
上下文切換會帶來直接和間接兩種因素影響程序性能的消耗。直接消耗:指的是 CPU 寄存器需要保存和加載, 系統(tǒng)調度器的代碼需要執(zhí)行, TLB 實例需要重新加載, CPU 的 pipeline 需要刷掉;間接消耗:指的是多核的 cache 之間得共享數據, 間接消耗對于程序的影響要看線程工作區(qū)操作數據的大?。?strong>因此我們在多線程操作時應該要考慮兩個問題:第一個是盡量減少上下文的切換次數,第二個是盡量提高 CPU 的使用率。
2. 內存模型
在介紹 Java 內存模型之前,先來看一下到底什么是計算機內存模型,然后再來看 Java 內存模型在計算機內存模型的基礎上做了哪些事情。先看一下為什么要有內存模型。
我們應該都知道,計算機在執(zhí)行程序的時候,每條指令都是在 CPU 中執(zhí)行的,而執(zhí)行的時候,又免不了要和數據打交道。而計算機上面的數據,是存放在主存當中的,也就是計算機的物理內存啦。剛開始,還相安無事的,但是隨著 CPU 技術的發(fā)展,CPU 的執(zhí)行速度越來越快。而由于內存的技術并沒有太大的變化,所以從內存中讀取和寫入數據的過程和 CPU 的執(zhí)行速度比起來差距就會越來越大,這就導致 CPU 每次操作內存都要耗費很多等待時間。所以,人們想出來了一個好的辦法,就是在 CPU 和內存之間增加高速緩存。緩存的概念大家都知道,就是保存一份數據拷貝。它的特點是速度快,內存小,并且昂貴。那么,程序的執(zhí)行過程就變成了:當程序在運行過程中,會將運算需要的數據從主存復制一份到 CPU 的高速緩存當中,那么 CPU 進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之后,再將高速緩存中的數據刷新到主存當中。隨著 CPU 能力的不斷提升,一層緩存就慢慢的無法滿足要求了,就逐漸的衍生出多級緩存。按照數據讀取順序和與CPU結合的緊密程度,CPU 緩存可以分為一級緩存(L1),二級緩存(L3),部分高端 CPU 還具有三級緩存(L3),每一級緩存中所儲存的全部數據都是下一級緩存的一部分。這三種緩存的 技術難度和制造成本是相對遞減的,所以其容量也是相對遞增的。那么,在有了多級緩存之后,程序的執(zhí)行就變成了:當 CPU 要讀取一個數據時,首先從一級緩存中查找,如果沒有找到再從二級緩存中查找,如果還是沒有就從三級緩存或內存中查找。單核 CPU 只含有一套L1,L2,L3緩存。如果 CPU 含有多個核心,即多核 CPU,則每個核心都含有一套L1(甚至和L2)緩存,而共享L3(或者和L2)緩存。
從上面的分析來看,這樣就會導致一個問題,那就是多線程 CUP 緩存一致性的問題,也就是大家常常說的原子性問題,可見性問題和有序性問題等等。其本質其實就是 CPU 緩存優(yōu)化后所帶來的后遺癥。出現問題就得解決問題,按照我們平時普通的思路就是回退版本,廢除掉處理器和處理器的優(yōu)化技術、廢除CPU緩存,讓CPU直接和主存交互,這肯定是不行的。因此內存模型就誕生了,內存模型就是用來解決 CPU 緩存優(yōu)化后所帶來的后遺癥。Java 的內存模型如下:

3. 線程常見問題分析
Java 后臺工程師經常會碰到一些問題像 CPU 飆高、Load 高、響應很慢等等,作為 Android 工程師由于很少會涉及到并發(fā)請求的處理,因此我們很少會刨根問底的去深究線程這一塊。雖然遇到的問題可能會千奇百怪但是問題的本質是不會變的,這也是為什么我一再強調大家要把基礎打牢,要多花些時間在 Linux 內核和系統(tǒng)源碼上面。可以這么說,在 Android 場景下我們遇到的線程問題,只要從 Linux 內核和 JVM 的內存模型這兩個方向去分析即可。
3.1. 線程池該怎么用
線程池參數有非常多:核心線程數、最大線程數,隊列等等。在實際的過程中怎么用呢?其實無非就是前面提到的兩點:
- 第一是盡量減少上下文的切換次數,盡可能少的創(chuàng)建些線程
- 第二是盡量提高 CPU 的使用率,盡可能多的創(chuàng)建些線程
直接看上去這兩點像是沖突了,但在實際的場景中是不沖突的,比如我們在系統(tǒng)架構時分析過 OkHttp 的源碼,我們不妨來看下它內部使用的線程池:
public synchronized ExecutorService executorService() {
if (executorService == null) {
// 核心線程數是 0 ,最大線程數是 Integer.MAX_VALUE
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
3.2. synchronized 與 lock 的區(qū)別
synchronized 的底層實現原理以前分析過這里就不再做過多的介紹,lock 的源碼這個需要大家自己去看看,網上有很多的文章大家也可以去輔助了解一下。這里我們只提一個大家可能沒留意的一個區(qū)別,synchronized 如果競爭不到鎖會導致上下文切換,這也是為什么如果沒有多線程安全的情況下,就不要隨意加鎖的原因。但是 lock 采用的一般是 Unsafe 底層的原理就是等待主線上的數據刷新。看上去好像 Lock 更好些,可以減少上下文的切換次數,其實也不完全正確,具體場景需要具體對待。
視頻鏈接: https://pan.baidu.com/s/1pZA2udae2v3f-Xcc-Oh19g
視頻密碼: 87uh