線程監控 - 線程基礎知識掃盲

在分享線程的監控之前,我們要來先講講線程的基礎知識,一般來說只要我們基礎牢固,在寫代碼的時候大部分情況下不容易犯錯。但在 Android 團隊人數達到幾十人甚至上百人的時候,我們就無法確保所有的同學都能按部就班的寫好代碼了,所以我們還是要有監控,但光有監控是不行的還需要有理論基礎,這樣的話出現了問題才能分析解決。有很多同學認爲線程有什麼好了解的,無非就是 synchronized 、volatile、newThread,啓動線程。很多同學可能連線程池和 lock 鎖都沒接觸過,說句實話早先年我也跟大家一樣,因爲項目中用不上只能自己去看源碼學原理。

1. 上下文切換

在過去單 CPU 時代,單任務在一個時間點只能執行單一程序。之後發展到多任務階段,計算機能在同一時間點並行執行多任務或多進程。雖然並不是真正意義上的“同一時間點”,而是多個任務或進程共享一個 CPU,並交由操作系統來完成多任務間對 CPU 的運行切換,以使得每個任務都有機會獲得一定的時間片運行。再後來發展到多線程技術,使得在一個程序內部能擁有多個線程並行執行。一個線程的執行可以被認爲是一個 CPU 在執行該程序。當一個程序運行在多線程下,就好像有多個 CPU 在同時執行該程序。多線程比多任務更加有挑戰。多線程是在同一個程序內部並行執行,因此會對相同的內存空間進行併發讀寫操作。這可能是在單線程程序中從來不會遇到的問題。其中的一些錯誤也未必會在單 CPU 機器上出現,因爲兩個線程從來不會得到真正的並行執行。然而,更現代的計算機伴隨着多核 CPU 的出現,也就意味着 不同的線程能被不同的 CPU 核得到真正意義的並行執行。所以,在多線程、多任務情況下,線程上下文切換是必須的,然而對於 CPU 架構設計中的概念,應先熟悉瞭解,這樣會有助於理解線程上下文切換原理。

多進程多線程在運行的過程中都離不開一個概念,那就是調度。JVM 虛擬機雖是跨平臺但是並未接管線程調度,調度還是由操作系統本身來決定,我們在下次看線程創建底層源碼便會知道。調度會涉及到一個上下文切換的概念,多任務多線程的本質其實就是 CPU 時間片的輪轉,在多任務處理系統中,CPU 需要處理所有程序的操作,當用戶來回切換它們時,需要記錄這些程序執行到哪裏。上下文切換就是這樣一個過程,允許 CPU 記錄並恢復各種正在運行程序的狀態,使它能夠完成切換操作。簡單一點說就是指 CPU 從一個進程或線程切換到另一個進程或線程。

在上下文切換過程中,CPU 會停止處理當前運行的程序,並保存當前程序運行的具體位置以便之後繼續運行。從這個角度來看,上下文切換有點像我們同時閱讀幾本書,在來回切換書本的同時我們需要記住每本書當前讀到的頁碼。在程序中,上下文切換過程中的“頁碼”信息是保存在進程控制塊(PCB, process control block)中的。PCB 還經常被稱作“切換楨”(switchframe)。“頁碼”信息會一直保存到 CPU 的內存中,直到他們被再次使用。PCB 通常是系統內存佔用區中的一個連續存區,它存放着操作系統用於描述進程情況及控制進程運行所需的全部信息,它使一個在多道程序環境下不能獨立運行的程序成爲一個能獨立運行的基本單位或一個能與其他進程併發執行的進程。

對於一個正在執行的進程包括 程序計數器、寄存器、變量的當前值等 ,而這些數據都是 保存在 CPU 的寄存器中的,且這些寄存器只能是正在使用 CPU 的進程才能享用,在進程切換時,首先得保存上一個進程的這些數據(便於下次獲得 CPU 的使用權時從上次的中斷處開始繼續順序執行,而不是返回到進程開始,否則每次進程重新獲得 CPU 時所處理的任務都是上一次的重複,可能永遠也到不了進程的結束出,因爲一個進程幾乎不可能執行完所有任務後才釋放 CPU ),然後將本次獲得 CPU 的進程的這些數據裝入 CPU 的寄存器從上次斷點處繼續執行剩下的任務。

上下文切換會帶來直接和間接兩種因素影響程序性能的消耗。直接消耗:指的是 CPU 寄存器需要保存和加載, 系統調度器的代碼需要執行, TLB 實例需要重新加載, CPU 的 pipeline 需要刷掉;間接消耗:指的是多核的 cache 之間得共享數據, 間接消耗對於程序的影響要看線程工作區操作數據的大小;因此我們在多線程操作時應該要考慮兩個問題:第一個是儘量減少上下文的切換次數,第二個是儘量提高 CPU 的使用率。

2. 內存模型

在介紹 Java 內存模型之前,先來看一下到底什麼是計算機內存模型,然後再來看 Java 內存模型在計算機內存模型的基礎上做了哪些事情。先看一下爲什麼要有內存模型。

我們應該都知道,計算機在執行程序的時候,每條指令都是在 CPU 中執行的,而執行的時候,又免不了要和數據打交道。而計算機上面的數據,是存放在主存當中的,也就是計算機的物理內存啦。剛開始,還相安無事的,但是隨着 CPU 技術的發展,CPU 的執行速度越來越快。而由於內存的技術並沒有太大的變化,所以從內存中讀取和寫入數據的過程和 CPU 的執行速度比起來差距就會越來越大,這就導致 CPU 每次操作內存都要耗費很多等待時間。所以,人們想出來了一個好的辦法,就是在 CPU 和內存之間增加高速緩存。緩存的概念大家都知道,就是保存一份數據拷貝。它的特點是速度快,內存小,並且昂貴。那麼,程序的執行過程就變成了:當程序在運行過程中,會將運算需要的數據從主存複製一份到 CPU 的高速緩存當中,那麼 CPU 進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之後,再將高速緩存中的數據刷新到主存當中。隨着 CPU 能力的不斷提升,一層緩存就慢慢的無法滿足要求了,就逐漸的衍生出多級緩存。按照數據讀取順序和與CPU結合的緊密程度,CPU 緩存可以分爲一級緩存(L1),二級緩存(L3),部分高端 CPU 還具有三級緩存(L3),每一級緩存中所儲存的全部數據都是下一級緩存的一部分。這三種緩存的 技術難度和製造成本是相對遞減的,所以其容量也是相對遞增的。那麼,在有了多級緩存之後,程序的執行就變成了:當 CPU 要讀取一個數據時,首先從一級緩存中查找,如果沒有找到再從二級緩存中查找,如果還是沒有就從三級緩存或內存中查找。單核 CPU 只含有一套L1,L2,L3緩存。如果 CPU 含有多個核心,即多核 CPU,則每個核心都含有一套L1(甚至和L2)緩存,而共享L3(或者和L2)緩存。

從上面的分析來看,這樣就會導致一個問題,那就是多線程 CUP 緩存一致性的問題,也就是大家常常說的原子性問題,可見性問題和有序性問題等等。其本質其實就是 CPU 緩存優化後所帶來的後遺症。出現問題就得解決問題,按照我們平時普通的思路就是回退版本,廢除掉處理器和處理器的優化技術、廢除CPU緩存,讓CPU直接和主存交互,這肯定是不行的。因此內存模型就誕生了,內存模型就是用來解決 CPU 緩存優化後所帶來的後遺症。Java 的內存模型如下:

3. 線程常見問題分析

Java 後臺工程師經常會碰到一些問題像 CPU 飆高、Load 高、響應很慢等等,作爲 Android 工程師由於很少會涉及到併發請求的處理,因此我們很少會刨根問底的去深究線程這一塊。雖然遇到的問題可能會千奇百怪但是問題的本質是不會變的,這也是爲什麼我一再強調大家要把基礎打牢,要多花些時間在 Linux 內核和系統源碼上面。可以這麼說,在 Android 場景下我們遇到的線程問題,只要從 Linux 內核和 JVM 的內存模型這兩個方向去分析即可。

3.1. 線程池該怎麼用

線程池參數有非常多:核心線程數、最大線程數,隊列等等。在實際的過程中怎麼用呢?其實無非就是前面提到的兩點:

  • 第一是儘量減少上下文的切換次數,儘可能少的創建些進程
  • 第二是儘量提高 CPU 的使用率,儘可能多的創建些進程

直接看上去這兩點像是衝突了,但在實際的場景中是不衝突的,比如我們在系統架構時分析過 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 的區別

synchronized 的底層實現原理以前分析過這裏就不再做過多的介紹,lock 的源碼這個需要大家自己去看看,網上有很多的文章大家也可以去輔助瞭解一下。這裏我們只提一個大家可能沒留意的一個區別,synchronized 如果競爭不到鎖會導致上下文切換,這也是爲什麼如果沒有多線程安全的情況下,就不要隨意加鎖的原因。但是 lock 採用的一般是 Unsafe 底層的原理就是等待主線上的數據刷新。看上去好像 Lock 更好些,可以減少上下文的切換次數,其實也不完全正確,具體場景需要具體對待。

視頻鏈接: https://pan.baidu.com/s/1pZA2udae2v3f-Xcc-Oh19g
視頻密碼: 87uh

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章