Android開發 多線程之換個視角理解

Avengong 2021-08-15 18:16:41 阅读数:529

本文一共[544]字,预计阅读时长:1分钟~
android 程之 理解

寫在前面

理解多線程並發和鎖的關鍵在於正確的理清當前代碼正處在哪個線程的執行環境下。說白了,這個同步代碼/同步方法誰都可以來執行的,關鍵是有沒有其他人在用這個鎖。

1 Thread如何理解

1.1 Thread類與普通類的區別?

線程與線程類是不同的概念。 線程是系統CPU資源調度的基本單元,它是一個抽象的概念。Thread類和其他別的類沒有什麼區別。它只是對線程有著“管理”作用。而真正執行的代碼都是在run()方法中,或者外部傳入的runnable中。

1.2 中介作用

在線程的start方法之前,所有代碼都在老線程上運行,調用start方法之後,內部會調用VMTthread.create,實際上真正在新線程上運行的只有run方法。這個角度理解,thread類只是一個中介,任務就是啟動一個新線程來運行用戶指定的runnable,而不關心是內部的還是外部傳入的。

2 線程的狀態

  1. New 線程創建
  2. Runnable 可執行狀態
  3. Running 執行狀態
  4. wait:當前線程調用某個對象object的wait方法,只有當子線程也同樣調用該對象object的 notify/notifyAll方法才會喚醒當前線程。系統可能會有多個線程在wait,所以有notifyAll方法。
  5. block:遇到同步時候(同步方法、同步代碼塊、class類對象作為鎖時,類中的靜態方法),如果鎖已經被其他的線程占用,那麼就會進入阻塞狀態,直到獲取到鎖。
  6. timed_wait: sleep/join, 等的一定時間才執行。
  7. terminated:線程運行完畢

join()用來保證兩個線程的順序執行:

Thread t1=new Thread(xx);
Thread t2=new Thread(xx);
t1.start();
t1.join();
t2.start();
複制代碼

上面代碼錶示,只有當t1執行完畢,t2才會執行。

2.1 wait和notify是如何綁定的?

通過同一個object對象。當一個線程調用某個object對象的wait方法時候,系統會在object中記錄該請求,如果是多個線程調用則會有waitinglist,而當另外一個線程調用object的notify/notifyAll來喚醒一個/多個waitinglist中的線程。

2.2 線程調用wait()方法的條件?

  • 執行這個object的synchronized方法
  • 執行一段synchronized代碼,且是基於這個object做的同步
  • 如果object是一個class類,可以實行synchronized static 方法

也就說: 一個線程獲得了對象object鎖lock,它才可以調用wait方法,而調用wait方法後該線程會釋放鎖,從而可以讓別的線程來獲取。

2.4 線程什麼時候會釋放鎖?

  1. 當線程執行完畢
  2. 線程調用wait()方法

2.5 wait方法和sleep方法區別

  • wait方法必須要在同步代碼中調用,sleep沒有限制
  • wait方法會釋放CPU、釋放鎖,sleep釋放CPU,不釋放鎖(容易引起死鎖)

3 Java內存模型的本質(重點)

JMM java內存管理模型,提出了主存和線程本身的工作內存概念,如圖:

image.png

說明: 主存即內存。本地內存即CPU緩存(包括三級緩存、寄存器、WCbuffer等)。

一個單核CPU在一個線程上執行指令,如果需要切換線程它會把當前線程的執行現場保存到內存中去,方便後續恢複,然後清空PC計數器,加載新線程的指令地址。因此,單核CPU不存在同步的問題。

當多核CPU分別在執行自己線程指令時,如果存在共享同一個變量,那麼就有可能存在競爭關系,因為每個CPU的核都有自己的本地緩存,而二者通信是通過內存中共享變量的方式來實現的。這就存在這個變量同步不及時的情况,所以需要同步。

其實本質上本地內存是一個抽象的概念。實際上指的是CPU中的L1、L2和寄存器等相關的緩存。 現在的設備都是多個CPU多核同時工作。如:CPU執行PC(程序計數器)中的某條指令需要一個變量,那麼CPU不是直接去內存中操作該變量。而是先把變量讀取到L3->L2->L1(三級緩存),最後到寄存器緩存起來,然後CPU在去寄存器中讀取該變量。當然CPU不會一次只讀取一個變量,而是一次讀取一個緩存行Cache Line,一個緩存行的大小是64個Byte。如果在需要下一個變量則會先從寄存器和緩存中查找,這樣就比去操作內存塊很多。最後,把計算結果刷新到寄存器,同步到主存(也就是內存)中。

4 同步相關問題(重點)

4.1 重排序和happens-before

4.1.1 重排序

不管什麼用到語言,我們寫的代碼最終都會轉成匯編指令,而匯編指令與機器指令(如:01010)是一一對應的。因此當CPU在執行當前指令的時候處於讀等待,CPU不工作了?豈不是浪費?為了提高性能,它會嘗試下一條指令能不能先執行了?,如果可以,那麼CPU就不會閑下來了。

但有個前提,這個指令跟前一個指令沒有依賴關系才會執行。 有了亂序執行這個機制,一連串的指令就看起來變得可以並行執行了(其實沒有,只是利用了CPU處於讀等待的空隙做事情)。

因此,為了提高執行效率,編譯器和CPU都會進行指令的重排序。

4.1.2 happens-before

顧名思義,如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。

不用太糾結這個概念。這只是一個規範而已,本質上就是說這個規範實現的代碼肯定是加了內存屏障的。

如下這些代碼實現方式,就是符合happens-before規則的:

  • 程序順序規則:一個線程中的每一個操作,happens-before於該線程中的任意後續操作。
  • 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
  • volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
  • 傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。
  • start規則:如果線程A執行操作ThreadB.start()啟動線程B,那麼A線程的ThreadB.start()操作happens-before於線程B中的任意操作、
  • join規則:如果線程A執行操作ThreadB.join()並成功返回,那麼線程B中的任意操作happens-before於線程A從ThreadB.join()操作成功返回。

4.2 synchronize

synchronize不管是修飾方法還是代碼塊,都需要用到鎖對象。而鎖對象的鎖是有狀態的,它會昇級也會降級。鎖的是對象,而不是把代碼塊鎖住了!

  • 無鎖態
  • 偏向鎖
  • 輕量級鎖(也叫自旋鎖)
  • 重量級鎖 用戶態向內核態申請鎖,消耗鎖資源。mutex

4.2.1 對象頭

在jvm中,每個Object對象在內存中的布局有三部分組成:

  1. 對象頭 : 包含markword(32比特系統是4個字節,64比特是8個字節)、class對象指針占4個字節
  2. 實例對象 :
  3. padding對齊: 為了讓對象能够被8整除,需要補齊的字節數

image.png 舉個例子: Object0=new Object(); o對象占多少內存?

假如是64比特系統:

對象頭+實例對象,也就是 8+4+0=12,不能被8整除,所以還需要加上padding 4. 因此,最終的o對象占用了16個字節。

請注意!!! markword就是用來存儲鎖信息的地方。 一共32/64比特,多少比特沒有太大關系。我們只需要知道裏面有什麼即可。

image.png

4.2.2 鎖的昇級過程

  1. 一開始new 出鎖對象時,還沒有線程進入臨界區,此時是無鎖態。
  2. 有線程進入,則改成偏向鎖,同時markword存入線程的id。
  3. 如果是同一線程則還是偏向鎖。
  4. 當另外線程也進入臨界區,請求鎖對象,發現對象頭已經有偏向鎖了。產生了競爭!!那不好意思,先撤銷偏向鎖,然後兩個線程通過CAS自旋的方式開始爭搶鎖對象,都往鎖對象頭裏面寫入自己線程棧的lock record。一旦有線程爭搶成功,那麼其他線程就會失敗,此時鎖對象變成了輕量級鎖。
  5. 失敗的線程會一直CAS循環下去,此間也可能還會有其他線程參與進來自旋。
  6. 當自旋次數超過一定值如10次,或者參與自旋的線程數太多。系統會進行幹預。
  7. 這樣幹耗著會浪費CPU資源,所以幹脆昇級為重量級鎖。其他線程全部進入mutex中的隊列中去排隊,線程進入wait或者block狀態,不消耗CPU。
  8. 但synchronize修飾的是非公平的隊列。

講了這麼多,synchronize的底層到底是怎麼實現的?

其實還是 lock cmpxchg 指令。

4.3 volatile

volatile修飾變量後有兩個作用: 1,內存可見性 線程間工作內存和主存實現了及時同步 2,防止指令重排 這對這個變量的操作被JMM加入內存屏障來保證指令不會亂序執行。

volatile到底是怎麼解决指令重排的??

JVM層通過加入內存屏障,是一個邏輯實現,是jvm的要求規範而已,具體要看匯編語言。

  1. loadload 屏障 讀
  2. storestore 屏障 寫
  3. loadstore 屏障
  4. storeload 屏障

四個邏輯。 具體 就是在volatile讀/寫的前後加入內存屏障,保證順序執行。內存屏障前後的指令不能重排序!

匯編層面: 最終就是調用了 lock: andl 指令。錶示在寄存器中加0操作。

為什麼這條指令能實現內存可見和禁止指令重排序??

內存可見性: 該指令能够將當前處理器對應緩存內容刷新到內存,並且是其他處理器的緩存失效。 重排序: 該指令本身就是內存屏障,它前面的指令和後面的指令都不能重排序。

4.4 CAS和原子操作

4.4.1 樂觀鎖和悲觀鎖

  • 悲觀鎖: 在訪問共享資源的時候總是認為別人會來搶,所以只要訪問臨界區就直接上鎖。通俗講就是因為怕被搶,所以無腦上鎖。比如用synchronize來對臨界區上鎖。
  • 樂觀鎖: 在訪問共享資源時候,認為別的線程不會來搶資源。所以是“無鎖”狀態。但是可以通過CAS來保證數據的安全。

4.4.2 CAS

CAS: compare and swap 比較並且交換。 目的: 在沒有鎖的狀態下,可以保證多個線程對一個值的更新。

CAS實現思想:

  • E:拿到變量當前原始值(期望值)
  • V:計算的結果值
  • N:再次獲取變量的值(當前值)

舉個例子: 假設i=0,對i做++操作。 CAS的過程是這樣的:

  1. 拿當i的前值 E=0;
  2. 計算結果值V=1;
  3. 再次拿i的當前,有可能如下情况: N=0;N=3(因為某個線程更改了)
  4. 如果E=N,錶示沒有被修改,我們可以更新,直接修改i=1。
  5. 如果E!=N,錶示被修改過,我們這次修改就不能執行。然後我們在重頭開始,再次比較,最終實現交換。

流程圖:

image.png

- 實現的本質:

AtomicInteger內部就是通過CAS的方式來保證線程安全的。

內部會調用UnSafe類的方法。

private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
/**
* Atomically decrements by one the current value.
*
* @return the previous value
*/
public final int getAndDecrement() {
//U錶示 UnSafe類
return U.getAndAddInt(this, VALUE, -1);
}
複制代碼

UnSafe類直接調用的是C++層的native方法compareAndSwapInt()

public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
// while中 native 方法
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
// native 方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
複制代碼

通過源碼發現compareAndSwapInt()最終會調用c++層的 Atomic::cmpxchg方法,有如下指令:

//_asm_ 錶示匯編指令
//LOCK_IF_MP: 如果是multi-processs 多處理器, 現在處理器都是多CPU的。
_asm_ volatile (LOCK_IF_MP(%4) "cmpxchg1 %1, (%3)")...//後面省略
複制代碼

如果是多個處理器則 進行lock。為什麼? 多個CPU就會出現多線程同時執行,出現並發問題。

而CAS方法會直接通過匯編指令:lock cmpxchg 指令 來完成CAS真正的操作。 cmpxchg 這個指令也就體現了比較和交換的本質了。

所以, 現在的問題變成 lock cmpxhg 指令 是如何實現線程安全的? 寫入的過程是沒有原子性保證的。 由 lock 指令來保證原子性。 最終由硬件來支持,硬件怎麼實現的啊? 好啦,到這裏就可以啦。 硬件通過鎖定北橋信號?(我也不清楚了啊)。

4.5 AQS

...

版权声明:本文为[Avengong]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/08/20210815181419007c.html