1. 程式人生 > ><實戰java高並發>自記筆記

<實戰java高並發>自記筆記

32位系統 同步器 from 實例方法 一次 編碼 ted 發生 call

1.java的jmm關鍵技術:多線程的原子性、可見性、有序性。
原子性:是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程幹擾。
對於32位系統的long型(64位)數據就不是原子性的。
可見性:是指當一個線程修改了某一個共享變量的值,其他線程是否能夠立即知道這個修改。
在一個線程中觀察另外一個線程的變量,它們的值是否觀測到、何時能觀測到是沒有保證的。因為有指令重排。
有序性:在程序並發時,程序的執行順序可能會出現亂序。有效性問題的原因是因為程序在執行時,可能會進行指令重排,重排後的指令與原指令的順序未必一致。
指令重排可以保證串行語義一致,但是沒有義務保證多線程間的語義也一致。對於一個線程來說,它看到的指令執行順序一定是一致的(否則的話我們的應用根本無法正常工作)
指令重排是因為性能考慮。盡量少的中斷流水線。

Java Memory Model (JAVA 內存模型)描述線程之間如何通過內存(memory)來進行交互。 具體說來, JVM中存在一個主存區(Main Memory或Java Heap Memory),對於所有線程進行共享,而每個線程又有自己的工作內存(Working Memory),工作內存中保存的是主存中某些變量的拷貝,線程對所有變量的操作並非發生在主存區,而是發生在工作內存中,而線程之間是不能直接相互訪問,變量在程序中的傳遞,是依賴主存來完成的。

2.
Thread線程的start()方法會創建一個線程並讓這個線程執行run()方法。

3.happens-before關系

什麽是happens-before關系? 這個關系其實就是一個保證而已,那麽保證什麽呢?它保證一條語句對內存的寫操作對另一條語句是可見的。換句話說,如果寫操作A和讀操作B存在happens-before這種關系,那麽寫操作在結束以後都操作才能開始。
下面是Java內存模型中的八條可保證happen—before的規則,它們無需任何同步器協助就已經存在,可以在編碼中直接使用。
1、程序次序規則:在一個單獨的線程中,按照程序代碼的執行流順序,(時間上)先執行的操作happen—before(時間上)後執行的操作。
2、管理鎖定規則:一個unlock操作happen—before後面(時間上的先後順序,下同)對同一個鎖的lock操作。
3、volatile變量規則:對一個volatile變量的寫操作happen—before後面對該變量的讀操作。
4、線程啟動規則:Thread對象的start()方法happen—before此線程的每一個動作。
5、線程終止規則:線程的所有操作都happen—before對此線程的終止檢測,可以通過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。
6、線程中斷規則:對線程interrupt()方法的調用happen—before發生於被中斷線程的代碼檢測到中斷時事件的發生。
7、對象終結規則:一個對象的初始化完成(構造函數執行結束)happen—before它的finalize()方法的開始。
8、傳遞性:如果操作A happen—before操作B,操作B happen—before操作C,那麽可以得出A happen—before操作C。
這裏的八個規則除了第三個以外都容易理解。所以專門講一下volatile變量規則。
1、 對volatile變量執行寫操作時,會在寫操作後加入一條store屏障指令
2、 對volatile變量執行讀操作時,會在讀操作前加入一條load屏障指令。
通俗得講,volatile變量在每次被線程訪問時,都強迫從主內存中讀該變量的值,而當該變量發生變化時,又會強迫將最新的值刷新到主內存。這樣任何時刻,不同的線程總是能夠看到該變量的最新值。
如果你不能夠理解,我們可以采取一種極端的思維方式:如果有兩個線程的話,對於一個普通變量,在java內存模型中它是有三個拷貝的,一個在主內存,另外兩個在線程的工作內存裏。如果刷新不及時,那麽就可能導致兩個工作內存中的變量值不一致。但是對於volatile變量,你完全可以假定其只有一份而且唯一一份拷貝在主內存中發生,所以當兩個線程想對volatile變量進行更改或者讀取的時候,總是得等其中一個線程完成以後才行。

Java語言中有一個“先行發生”(happen—before)的規則,它是Java內存模型中定義的兩項操作之間的偏序關系,如果操作A先行發生於操作B,其意思就是說,在發生操作B之前,操作A產生的影響都能被操作B觀察到,“影響”包括修改了內存中共享變量的值、發送了消息、調用了方法等,它與時間上的先後發生基本沒有太大關系。這個原則特別重要,它是判斷數據是否存在競爭、線程是否安全的主要依據。

JMM關於synchronized的兩條規定:

  1. 線程解鎖前,必須把共享變量的最新值刷新到主內存中
  2. 線程加鎖時,講清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值。
    這樣,線程解鎖前對共享變量的修改在下次加鎖時對其他線程可見。

  3. Thread的stop方法會立即釋放該線程所持有的鎖,而這些鎖恰恰是用來維持對象一致性的。如果此時寫線程寫入數據正寫到一半,並強行終止,那麽對象就會被破壞,同時由於所已經被釋放,另外一個等待該所的線程就會讀到這個不一致的對象。
    太過於暴力,強行把執行到一半的線程終止,可能會引起一些數據不一致的問題。單線程不會出現該問題。

5.Java中Wait、Sleep和Yield方法的區別
sleep()和yield()方法是定義在Thread類中,而wait()方法是定義在Object類中的, 這也是面試中常問的一個問題。
wait()和sleep()的關鍵的區別在於,wait()是用於線程間通信的,而sleep()是用於短時間暫停當前線程。更加明顯的一個區別在於,當一個線程調用wait()方法的時候,會釋放它鎖持有的對象的管程和鎖,但是調用sleep()方法的時候,不會釋放他所持有的管程。
下面列出Java中wait和sleep方法的區別:
wait只能在同步(synchronize)環境中被調用,而sleep不需要。詳見Why to wait and notify needs to call from synchronized method
進入wait狀態的線程能夠被notify和notifyAll線程喚醒,但是進入sleeping狀態的線程不能被notify方法喚醒。
wait通常有條件地執行,線程會一直處於wait狀態,直到某個條件變為真。但是sleep僅僅讓你的線程進入睡眠狀態。方法在進入wait狀態的時候會釋放對象的鎖,但是sleep方法不會。
wait方法是針對一個被同步代碼塊加鎖的對象,而sleep是針對一個線程。更詳細的講解可以參考《Java核心技術卷1》,裏面介紹了如何使用wait和notify方法。
Thread.sleep()方法是一個靜態方法,作用在當前線程上.wait方法是一個實例方法,並且只能在其他線程調用本實例的notify()方法時被喚醒。
使用sleep方法時,被暫停的線程在被喚醒之後會立即進入就緒態(Runnable state),但是使用wait方法的時候,被暫停的線程會首先獲得鎖(譯者註:阻塞態),然後再進入就緒態。所以,如果需要暫定你的線程一段特定的時間就使用sleep()方法,如果你想要實現線程間通信就使用wait()方法。

yield()方法上來,與wait()和sleep()方法有一些區別,它僅僅釋放線程所占有的CPU資源,從而讓其他線程有機會運行,但是並不能保證某個特定的線程能夠獲得CPU資源。誰能獲得CPU完全取決於調度器,在有些情況下調用yield方法的線程甚至會再次得到CPU資源。所以,依賴於yield方法是不可靠的,它只能盡力而為。

yield和sleep的區別
yield和sleep的主要是,yield方法會臨時暫停當前正在執行的線程,來讓有同樣優先級的正在等待的線程有機會執行。如果沒有正在等待的線程,或者所有正在等待的線程的優先級都比較低,那麽該線程會繼續運行。執行了yield方法的線程什麽時候會繼續運行由線程調度器來決定,不同的廠商可能有不同的行為。yield方法不保證當前的線程會暫停或者停止,但是可以保證當前線程在調用yield方法時會放棄CPU。

Java中sleep方法的幾個註意點:
Thread.sleep()方法用來暫停線程的執行,將CPU放給線程調度器。
Thread.sleep()方法是一個靜態方法,它暫停的是當前執行的線程。
Java有兩種sleep方法,一個只有一個毫秒參數,另一個有毫秒和納秒兩個參數。
與wait方法不同,sleep方法不會釋放鎖
如果其他的線程中斷了一個休眠的線程,sleep方法會拋出Interrupted Exception。
休眠的線程在喚醒之後不保證能獲取到CPU,它會先進入就緒態,與其他線程競爭CPU。
有一個易錯的地方,當調用t.sleep()的時候,會暫停線程t。這是不對的,因為Thread.sleep是一個靜態方法,它會使當前線程而不是線程t進入休眠狀態。
這就是java中的sleep方法。我們已經看到了java中sleep、wait以及yield方法的區別。
總之,記住sleep和yield作用於當前線程。

6.Thread.join詳解
為什麽要用join()方法
在很多情況下,主線程生成並起動了子線程,如果子線程裏要進行大量的耗時的運算,主線程往往將於子線程之前結束,但是如果主線程處理完其他的事務後,需要用到子線程的處理結果,也就是主線程需要等待子線程執行完成之後再結束,這個時候就要用到join()方法了
join方法的作用
JDK中對join方法解釋為:“等待該線程終止”,換句話說就是:”當前線程等待子線程的終止“。也就是在子線程調用了join()方法後面的代碼,只有等到子線程結束了當前線程才能執行。
主要作用是同步,它可以使得線程之間的並行執行變為串行執行。在A線程中調用了B線程的join()方法時,表示只有當B線程執行完畢時,A線程才能繼續執行。
join()方法中如果傳入參數,則表示這樣的意思:如果A線程中掉用B線程的join(10),則表示A線程會等待B線程執行10毫秒,10毫秒過後,A、B線程並行執行。需要註意的是,jdk規定,join(0)的意思不是A線程等待B線程0秒,而是A線程等待B線程無限時間,直到B線程執行完畢,即join(0)等價於join()。
join方法的原理就是調用相應線程的wait方法進行等待操作的,例如A線程中調用了B線程的join方法,則相當於在A線程中調用了B線程的wait方法,當B線程執行完(或者到達等待時間),B線程會自動調用自身的notifyAll方法喚醒A線程,從而達到同步的目的。

6.線程中斷
嚴格地講,線程中斷不會使線程立即退出,而是給線程發送一個通知,告知目標線程,有人希望你退出。至於目標線程接到通過後何時退出,完全由目標線程自行決定。如果立即退出,又會出現stop問題。
interrupt 中斷線程。目標線程中斷,設置中斷標識位。
isInterrupted 是否被中斷。目標對象檢查中斷標識位。
static interrupted 判斷是否被中斷,並清除當前線程中斷狀態標識。
Thread.sleep()方法由於中斷而拋出異常,此時,它會清楚中斷標識。如果不加處理,那麽在下一次循環開始時,就無法捕獲到這個中斷。所以在異常處理中,需要再次設置中斷標識。

7.等待wait和通知notify
這兩個方法都是在object上的,所以任何對象都可以調用這兩個方法。
當在一個對象實例上調用wait()方法後,當前線程就會在這個對象上等待。比如線程A中,調用了obj.wait()方法,那麽線程A就會停止繼續執行,而轉為等待狀態。等待何時結束?線程A會一直等到其他線程調用了obj.notify方法為止。這時,obj對象就成為了多個線程之前的有效通信手段。
wait和notify的工作原理:如果一個線程調用了obj.wait,那麽它就會進入obj對象的等待隊列。這個等待隊列中,可能會有多個線程,因為系統運行多個線程同時等待某一個對象。當obj.notify被調用時,它就會等待隊列中隨機選擇一個,將其喚醒。這個選擇是不公平的,不是先等待的就先被喚醒,而是隨機的選擇。notifyall方法是喚醒所有等待的線程。
wait和notify必須在synchronzied中,必須先獲取到目標對象的一個監視器。

8.掛起suspend和繼續執行resume
這兩個方法都是不推薦的,都是廢棄方法。
suspend不推薦原因:該方法在導致線程暫停的同時,並不會釋放任何鎖資源。此刻其他任何線程想要訪問被它暫用的鎖時,都會被牽連,導致無法正常繼續運行。直到對應的線程上進行了resume操作,被掛起的線程才能夠繼續,從而其他所有阻塞在相關鎖上的線程也可以繼續執行。但是如果resume操作意外地在suspend前就執行了,那麽被掛起的線程可能很難有機會繼續被執行。更嚴重的是,它所占用的鎖都不會釋放,可能導致整個系統無法使用,而且對於被掛起的線程,從它的狀態上看,還是runnable,影響對狀態的判斷。
suspend方法導致線程進入類似死鎖的狀態。

9.JIT編譯器,英文寫作Just-In-Time Compiler,中文意思是即時編譯器

<實戰java高並發>自記筆記