1. 程式人生 > >【模組一】JAVA篇--Java執行緒併發☞參考答案

【模組一】JAVA篇--Java執行緒併發☞參考答案

執行緒:

多執行緒實現的四種方式:

  1. 繼承Thread類,重寫run()方法
    (1)定義Thread類的子類,並重寫該類的run方法,該run方法的方法體就代表了執行緒要完成的任務。因此把run()方法稱為執行體。
    (2)建立Thread子類的例項,即建立了執行緒物件。
    (3)呼叫執行緒物件的start()方法來啟動該執行緒。
  2. 實現Runnable介面,重寫run()方法
    (1)定義runnable介面的實現類,並重寫該介面的run()方法,該run()方法的方法體同樣是該執行緒的執行緒執行體。
    (2)建立 Runnable實現類的例項,並依此例項作為Thread的target來建立Thread物件,該Thread物件才是真正的執行緒物件。
    (3)呼叫執行緒物件的start()方法來啟動該執行緒。
  3. 實現Callable介面,重寫call()方法,通過FutureTask包裝器來建立Thread執行緒
    (1)建立Callable介面的實現類,並實現call()方法,該call()方法將作為執行緒執行體,並且有返回值。
    (2)建立Callable實現類的例項,使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了該Callable物件的call()方法的返回值。
    (3)使用FutureTask物件作為Thread物件的target建立並啟動新執行緒。
    (4)呼叫FutureTask物件的get()方法來獲得子執行緒執行結束後的返回值
    採用實現Runnable、Callable介面的方式創見多執行緒時,優勢是:
           執行緒類只是實現了Runnable介面或Callable介面,還可以繼承其他類。
           在這種方式下,多個執行緒可以共享同一個target物件,所以非常適合多個相同執行緒來處理同一份資源的情況,從而可以將CPU、程式碼和資料分開,形成清晰的模型,較好地體現了面向物件的思想。
    劣勢是:
          程式設計稍微複雜,如果要訪問當前執行緒,則必須使用Thread.currentThread()方法。
          使用繼承Thread類的方式建立多執行緒時優勢是:
          編寫簡單,如果需要訪問當前執行緒,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前執行緒。
    劣勢是:
    執行緒類已經繼承了Thread類,所以不能再繼承其他父類。
  4. 通過Executor 的工具類建立執行緒池來建立多執行緒(不建議使用,阿里明確不允許)

為什麼我們呼叫start()方法時會執行run()方法,為什麼我們不能直接呼叫run()方法?

new一個Thread,執行緒進入了新建狀態;呼叫start()方法,執行緒進入了就緒狀態,當分配到時間片後就可以開始運行了。 
start()會執行執行緒的相應準備工作,然後自動執行run()方法的內容。是真正的多執行緒工作。 
而直接執行run()方法,會把run方法當成一個mian執行緒下的普通方法去執行,並不會在某個執行緒中執行它,這並不是多執行緒工作。

執行緒的狀態(生命週期)

  1. 新建狀態:通過(建立執行緒的四種方式)new建立執行緒物件
  2. 就緒狀態:呼叫start()方法,進入就緒狀態
  3. 執行狀態:執行run()方法,進入執行狀態
  4. 阻塞狀態:執行sleep()方法,或者等待其他執行緒釋放資源的時候
  5. 死亡狀態:執行緒死亡原因一般有兩個:一是執行緒正常執行完畢;二是被強制終止,入通過呼叫stop()或destroy()方法

現在有T1、T2、T3三個執行緒,你怎樣保證T2在T1執行完後執行,T3在T2執行完後執行?

考察public final void join()方法:等待該執行緒終止

在前一個執行緒呼叫後一個執行緒的join()方法可以按順序執行。

JAVA記憶體模型(Java Memory Model:JMM)(原理圖待補充)

    Java通過8種原子操作完成工作記憶體主記憶體的互動:

        lock:作用於主記憶體,把變數標識為執行緒獨佔狀態。

        unlock:作用於主記憶體,解除獨佔狀態。

        read:作用主記憶體,把一個變數的值從主記憶體傳輸到執行緒的工作記憶體。

        load:作用於工作記憶體,把read操作傳過來的變數值放入工作記憶體的變數副本中。

        use:作用工作記憶體,把工作記憶體當中的一個變數值傳給執行引擎。

        assign:作用工作記憶體,把一個從執行引擎接收到的值賦值給工作記憶體的變數。

        store:作用於工作記憶體的變數,把工作記憶體的一個變數的值傳送到主記憶體中。

        write:作用於主記憶體的變數,把store操作傳來的變數的值放入主記憶體的變數中。

Java中的volatile關鍵是什麼作用?怎樣使用它?在Java中它跟synchronized方法有什麼不同?

執行緒安全是兩方面需要的 原子性(指的是多條操作)和可見性。

volatile只能保證可見性,synchronized是兩個均保證的。 
volatile輕量級,只能修飾變數;synchronized重量級,還可修飾方法。 
volatile不會造成執行緒的阻塞,而synchronized可能會造成執行緒的阻塞

 

鎖機制

  • 說說執行緒安全問題

    執行緒安全是多執行緒領域的問題,執行緒安全可以簡單理解為一個方法或者一個例項可以在多執行緒環境中使用而不會出現問題。
    在Java多執行緒程式設計當中,提供了多種實現Java執行緒安全的方式:

    • 最簡單的方式,使用Synchronization關鍵字:Java Synchronization介紹
    • 使用java.util.concurrent.atomic 包中的原子類,例如 AtomicInteger
    • 使用java.util.concurrent.locks 包中的鎖
    • 使用執行緒安全的集合ConcurrentHashMap
    • 使用volatile關鍵字,保證變數可見性(直接從記憶體讀,而不是從執行緒cache讀)
  • volatile 實現原理

    • 在JVM底層volatile是採用“記憶體屏障”來實現的。
    • 快取一致性協議(MESI協議)它確保每個快取中使用的共享變數的副本是一致的。其核心思想如下:當某個CPU在寫資料時,如果發現操作的變數是共享變數,則會通知其他CPU告知該變數的快取行是無效的,因此其他CPU在讀取該變數時,發現其無效會重新從主存中載入資料。
  • synchronize 實現原理

    同步程式碼塊是使用monitorenter和monitorexit指令實現的,同步方法(在這看不出來需要看JVM底層實現)依靠的是方法修飾符上的ACC_SYNCHRONIZED實現。

  • synchronized 與 lock 的區別

    一、synchronized和lock的用法區別
    (1)synchronized(隱式鎖):在需要同步的物件中加入此控制,synchronized可以加在方法上,也可以加在特定程式碼塊中,括號中表示需要鎖的物件。
    (2)lock(顯示鎖):需要顯示指定起始位置和終止位置。一般使用ReentrantLock類做為鎖,多個執行緒中必須要使用一個ReentrantLock類做為對 象才能保證鎖的生效。且在加鎖和解鎖處需要通過lock()和unlock()顯示指出。所以一般會在finally塊中寫unlock()以防死鎖。
    二、synchronized和lock效能區別
    synchronized是託管給JVM執行的,而lock是java寫的控制鎖的程式碼。在Java1.5中,synchronize是效能低效的。因為 這是一個重量級操作,需要呼叫操作介面,導致有可能加鎖消耗的系統時間比加鎖以外的操作還多。相比之下使用Java提供的Lock物件,效能更高一些。但 是到了Java1.6,發生了變化。synchronize在語義上很清晰,可以進行很多優化,有適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致 在Java1.6上synchronize的效能並不比Lock差。
    三、synchronized和lock機制區別
    (1)synchronized原始採用的是CPU悲觀鎖機制,即執行緒獲得的是獨佔鎖。獨佔鎖意味著其 他執行緒只能依靠阻塞來等待執行緒釋放鎖。
    (2)Lock用的是樂觀鎖方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。樂觀鎖實現的機制就 是CAS操作(Compare and Swap)。

  • CAS 樂觀鎖

    CAS是項樂觀鎖技術,當多個執行緒嘗試使用CAS同時更新同一個變數時,只有其中一個執行緒能更新變數的值,而其它執行緒都失敗,失敗的執行緒並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
    CAS 操作包含三個運算元 —— 記憶體位置(V)、預期原值(A)和新值(B)。如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”這其實和樂觀鎖的衝突檢查+資料更新的原理是一樣的。

  • ABA 問題

    CAS會導致“ABA問題”。
    CAS演算法實現一個重要前提需要取出記憶體中某時刻的資料,而在下時刻比較並替換,那麼在這個時間差類會導致資料的變化。
    比如說一個執行緒one從記憶體位置V中取出A,這時候另一個執行緒two也從記憶體中取出A,並且two進行了一些操作變成了B,然後two又將V位置的資料變成A,這時候執行緒one進行CAS操作發現記憶體中仍然是A,然後one操作成功。儘管執行緒one的CAS操作成功,但是不代表這個過程就是沒有問題的。
    部分樂觀鎖的實現是通過版本號(version)的方式來解決ABA問題,樂觀鎖每次在執行資料的修改操作時,都會帶上一個版本號,一旦版本號和資料的版本號一致就可以執行修改操作並對版本號執行+1操作,否則就執行失敗。因為每次操作的版本號都會隨之增加,所以不會出現ABA問題,因為版本號只會增加不會減少。

  • 樂觀鎖的業務場景及實現方式

    樂觀鎖(Optimistic Lock):
    每次獲取資料的時候,都不會擔心資料被修改,所以每次獲取資料的時候都不會進行加鎖,但是在更新資料的時候需要判斷該資料是否被別人修改過。如果資料被其他執行緒修改,則不進行資料更新,如果資料沒有被其他執行緒修改,則進行資料更新。由於資料沒有進行加鎖,期間該資料可以被其他執行緒進行讀寫操作。
    比較適合讀取操作比較頻繁的場景,如果出現大量的寫入操作,資料發生衝突的可能性就會增大,為了保證資料的一致性,應用層需要不斷的重新獲取資料,這樣會增加大量的查詢操作,降低了系統的吞吐量。

    <