JAVA多線程/併發——問題與答案(2)

線程安全

1,什麼是線程安全?

線程安全:多個線程同時訪問公共資源數據,運行同一段代碼,並且每次執行的結果都與預期結果一致,既爲線程安全的。如果執行的結果存在不確定性,既爲線程不安全的。

2,servlet是線程安全嗎?

servlet不是線程安全的,因爲每個servlet在Tomcat是單例的,當多個HTTP請求同時請求同一個servlet時,多個請求對應的線程將併發調用Servlet的service()方法,此時,如果在Servlet中定義了實例變量或靜態變量,那麼可能會發生線程安全問題。

詳情參考:java基礎篇(5)——Servlet詳解

3,同步有幾種實現方法?

    1,同步方法(synchronized關鍵字修飾的方法);

    2,同步代碼塊(synchronized關鍵字修飾的語句塊);

    3,使用ReentrantLock可重入鎖實現線程同步;

    4,線程排隊執行

     總結就是加鎖和隊列

5,volatile有什麼用?能否用一句話說明下volatile的應用場景?

     volatile的作用有兩點,內存可見性防止指令重排序

    內存可見性:根據JMM模型,每個線程內部的工作內存中,保存有公共變量的副本,而volatile的作用就是使工作內存中的變量即時刷新到主內存中。

    防止指令重排序:根據happens-before原則,對volatile變量的寫操作 happens-before於對該變量的讀。

    volatile的使用場景:

            1,狀態標誌;2,獨立觀察;3,開銷較低的“讀-寫鎖”策略

6,請說明下java的內存模型及其工作流程。

Java的內存模型JMM(Java Memory Model)JMM主要是爲了規定了線程和內存之間的一些關係。根據JMM的設計,系統存在一個主內存(Main Memory),Java中所有實例變量都儲存在主存中,對於所有線程都是共享的。每條線程都有自己的工作內存(Working Memory),工作內存由緩存和堆棧兩部分組成,緩存中保存的是主存中變量的副本,緩存可能並不總和主存同步,也就是緩存中變量的修改可能沒有立刻寫到主存中;堆棧中保存的是線程的局部變量,線程之間無法相互直接訪問堆棧中的變量。 


從上圖來看,線程A與線程B之間如要通信的話,必須要經歷下面2個步驟:

  1. 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。
  2. 然後,線程B到主內存中去讀取線程A之前已更新過的共享變量。
7,爲什麼代碼會重排序?

重排序分編譯器重排序和處理器重排序,代碼在編譯時編譯器會對代碼進行優化重排序。處理器在執行指令時,根據指令並行技術,如果兩條指令不存在依賴性,處理器可以改變語句對應指令的執行順序;而且由於處理器使用緩存讀寫,這使得加載和存儲操作看上去可能是在亂序執行。

在執行程序時爲了提高性能,編譯器和處理器常常會對指令做重排序。重排序分三種類型


  1. 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
  2. 指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 內存系統的重排序。由於處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操作看上去可能是在亂序執行。

從java源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:

上述的1屬於編譯器重排序,2和3屬於處理器重排序。這些重排序都可能會導致多線程程序出現內存可見性問題。對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。對於處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定類型的內存屏障(memory barriers,intel稱之爲memory fence)指令,通過內存屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)。

併發容器和框架

1,如何讓一段程序併發的執行,並最終彙總結果?

使用CyclicBarrier 在多個關口處將多個線程執行結果彙總 ;CountDownLatch 在各線程執行完畢後向總線程彙報結果。

2,如何合理的配置java線程池?如CPU密集型的任務或IO密集型的任務,基本線程池應該配置多大?

CPU密集型任務可以少配置線程數,大概和機器的cpu核數相當,可以使得每個線程都在執行任務;IO密集型時,大部分線程都阻塞,故需要多配置線程數,2*cpu核數

3,用有界隊列好還是無界隊列好?任務非常多的時候,使用什麼阻塞隊列能獲取最好的吞吐量?

有界隊列和無界隊列的配置需區分業務場景,一般情況下配置有界隊列,在一些可能會有爆發性增長的情況下使用無界隊列。
任務非常多時,使用非阻塞隊列使用CAS操作替代鎖可以獲得好的吞吐量。

4,如何使用阻塞隊列實現一個生產者和消費者模型?請寫代碼。TODO


5,多讀少寫的場景應該使用哪個併發容器,爲什麼使用它?比如你做了一個搜索引擎,搜索引擎每次搜索前需要判斷搜索關鍵詞是否在黑名單裏,黑名單每天更新一次。
 答:1)CopyOnWriteArrayList這個容器適用於多讀少寫…
        2)讀寫並不是在同一個對象上。在寫時會大面積複製數組,所以寫的性能差,在寫完成後將讀的引用改爲執行寫的對象

Java中的鎖

1,如何實現樂觀鎖(CAS)?如何避免ABA問題?

        1.CAS原語有三個值,一個是內存值,一個是期望值,一個是寫入值。 在不加鎖的情況下寫入時,每次讀取內存值,然後跟預期值比對,如果比對失敗,反覆的讀和比對,直到成功。在CAS原語是一個原子操作,如果寫入時,內存值發生改變,則寫入值失敗。
        2.帶參數版本來避免aba問題,在讀取和替換的時候進行判定版本是否一致

2,什麼場景下可以使用volatile替換synchronized?
        只需要保證共享資源的可見性的時候可以使用volatile替代,synchronized保證可操作的原子性一致性和可見性。 volatile適用於新值不依賴於就值的情形

3,什麼是可重入鎖(ReentrantLock)?
ReentrantLock 相對於固有鎖synchronized,同樣是可重入的,在某些vm版本上提供了比固有鎖更高的性能,提供了更豐富的鎖特性,比如可中斷的鎖,可等待的鎖,平等鎖以及非塊結構的加鎖。從代碼上儘量用固有鎖,vm會對固有鎖做一定的優化,並且代碼可維護和穩定。只有在需要ReentrantLock的一些特性時,可以考慮用ReentrantLock實現。

4,ReentrantLock 和synchronized比較。來自《java併發編程實戰》
1.爲什麼JUC框架出現LOCK?
ReentrantLock並不是替代synchronized的方法,而是當內置鎖不適用時,作爲一種可選的高級功能。
2.那麼Synchronized有哪些缺點?
①. 只有一個condition與鎖相關聯,這個condition是什麼?就是synchronized對針對的對象鎖。
②. synchronized無法中斷一個正在等待獲得鎖的線程,也即多線程競爭一個鎖時,其餘未得到鎖的線程只能不停的嘗試獲得鎖,而不能中斷。這種情況對於大量的競爭線程會造成性能的下降等後果。
3.我們面對ReentrantLock和synchronized改如何選擇?
Synchronized相比Lock,爲許多開發人員所熟悉,並且簡潔緊湊,如果現有程序已經使用了內置鎖,那麼儘量保持代碼風格統一,儘量不引入Lock,避免兩種機制混用,容易令人困惑,也容易發生錯誤。
在Synchronized無法滿足需求的情況下,Lock可以作爲一種高級工具,這些功能包括“可定時的、可輪詢的與可中斷的鎖獲取操作,公平隊列,以及非塊結構的鎖”否則還是優先使用Synchronized。
最後,未來更可能提升Synchronized而不是Lock的性能,因爲Synchronized是JVM的內置屬性,他能執行一些優化,例如對線程封閉的鎖對象的鎖消除優化,通過增加鎖的粒度來消除內置鎖的同步,而如果基於類庫的鎖來實現這些功能,則可能性不大。

5,讀寫鎖可以用於什麼應用場景?(ReentrantReadWriteLock)

    區分讀和寫,處於讀操作時,可以允許多個線程同時獲得讀操作。但是同一時刻只能有一個線程可以獲得寫鎖。其它獲取寫鎖失敗的線程都會進入睡眠狀態,直到寫鎖釋放時被喚醒。 
注意:寫鎖會阻塞其它讀寫鎖。當有一個線程獲得寫鎖在寫時,讀鎖也不能被其它線程獲取;寫優先於讀,當有線程因爲等待寫鎖而進入睡眠時,則後續讀者也必須等待 
    適用於讀取數據的頻率遠遠大於寫數據的頻率的場合。 

6,什麼時候應該使用可重入鎖?

場景1:如果發現該操作已經在執行中則不再執行(有狀態執行)

a、用在定時任務時,如果任務執行時間可能超過下次計劃執行時間,確保該有狀態任務只有一個正在執行,忽略重複觸發。
b、用在界面交互時點擊執行較長時間請求操作時,防止多次點擊導致後臺重複執行(忽略重複觸發)。

以上兩種情況多用於進行非重要任務防止重複執行,(如:清除無用臨時文件,檢查某些資源的可用性,數據備份操作等)

場景2:如果發現該操作已經在執行,等待一個一個執行(同步執行,類似synchronized)

這種比較常見大家也都在用,主要是防止資源使用衝突,保證同一時間內只有一個操作可以使用該資源。但與synchronized的明顯區別是性能優勢(伴隨jvm的優化這個差距在減小)。同時Lock有更靈活的鎖定方式,公平鎖與不公平鎖,而synchronized永遠是公平的

這種情況主要用於對資源的爭搶(如:文件操作,同步消息發送,有狀態的操作等)

ReentrantLock默認情況下爲不公平鎖

不公平鎖與公平鎖的區別:

公平情況下,操作會排一個隊按順序執行,來保證執行順序。(會消耗更多的時間來排隊)
不公平情況下,是無序狀態允許插隊,jvm會自動計算如何處理更快速來調度插隊。(如果不關心順序,這個速度會更快)

場景3:如果發現該操作已經在執行,則嘗試等待一段時間,等待超時則不執行(嘗試等待執行)

這種其實屬於場景2的改進,等待獲得鎖的操作有一個時間的限制,如果超時則放棄執行。
用來防止由於資源處理不當長時間佔用導致死鎖情況(大家都在等待資源,導致線程隊列溢出)

場景4:如果發現該操作已經在執行,等待執行。這時可中斷正在進行的操作立刻釋放鎖繼續下一操作。

synchronized與Lock在默認情況下是不會響應中斷(interrupt)操作,會繼續執行完。lockInterruptibly()提供了可中斷鎖來解決此問題。(場景2的另一種改進,沒有超時,只能等待中斷或執行完畢)

這種情況主要用於取消某些操作對資源的佔用。如:(取消正在同步運行的操作,來防止不正常操作長時間佔用造成的阻塞)

併發工具

1,如何實現一個流控程序,用於控制請求的調用次數?























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