多线程与并发编程实践
為什么80%的碼農都做不了架構師?>>> ??
一、多線程
進程一般作為資源的組織單位,是計算機程序的運行實例,表示正在執行的指令,有自己獨立的地址空間,包含程序內容和數據,進程間資源和狀態相互隔離。
線程是程序的執行流程,CPU調度執行的基本單位,有自己的程序計數器,寄存器,堆棧,幀,共享同一進程的地址空間,內存和其他資源。
當虛擬機中運行的所有線程都是守護線程時,虛擬機終止運行。
1、可見性
使用共享內存的方式進行多線程通信的話,可能造成可見性的相關問題,即一個線程所做的修改對于其他的線程不可見,導致其他線程仍然使用錯誤的值。
造成的原因:
(1)多線程的實際執行順序
(2)CPU采用的層次結構的多級緩存架構
在寫入時數據被先寫入緩存中,之后在某個特定的時間被寫回主存。不同的CPU可能采用不同的寫入策略,如寫穿透或者寫返回等。由于緩存的存在,在某些時間點上,緩存中的數據域主存中的數據可能是不一致的。
(3)CPU指令重排
2、Java內存模型
描述了程序中共享變量的關系以及在主存中寫入和讀取這些變量值得底層細節。
(1)程序順序
由程序內部代碼邏輯決定的執行順序,該順序具備一致性。
(2)同步順序
同步動作之間的先后順序。成功的加鎖動作必然在某個解鎖動作之后。常見的同步關系(即A發生在B前)如下:
A、在一個監視器對象上的解鎖動作與同對象上后續成功的加鎖動作保持同步
B、對一個聲明為volatile的變量的寫入動作與同一變量的后續讀取動作保持同步
C、啟動一個線程的動作與該線程執行的第一個動作保持同步
D、向線程中共享變量寫入默認值的動作與該線程執行的第一個動作保持同步
E、線程A運行時的最后一個動作與另外一個線程中任何可以檢測到線程A終止的動作保持同步
F、如果線程A中斷線程B,那么線程A的中斷動作與任何其他線程檢測到線程B處于被中斷的狀態的動作保持同步
(3)happens-before順序
如果一個動作按照happens-before順序發生在另外一個動作之前,那么前一個動作的執行結果再多線程程序中對后一個動作肯定是可見的。
A、如果A、B兩個動作在一個線程中執行,同時程序順序中A出現在B之前,則A在B之前發生
B、一個對象的構造方法的結束在該對象的finalize方法運行之前發生
C、如果動作A和動作B保持同步,則A在B之前發生
D、如果動作A在動作B之前發生,同時動作B在動作C之前發生,則A在C之前發生。(傳遞性)
開發者需要做的是利用Java平臺提供的支持來消除程序中的數據競爭,保證程序的正確性,不需要考慮CPU和編譯器可能進行的指令重排,因為Java虛擬機和編譯器會確保這些指令重排不會影響程序正確性。
3、volatile關鍵詞
可以保證有序性和可見性,不保證原子性。
有序性是:對一個聲明為volatile的變量的寫入動作與同一變量的后續讀取動作保持同步
可見性:在寫入volatile變量之后,CPU緩存中的內容會被寫回主存,在讀取volatile變量時,CPU緩存中的該變量副本被置為失效狀態,重新從主存中讀取。
主要用來確保對一個變量的修改被正確地傳播到其他線程中。
4、final關鍵詞
final在JMM中的語義是可見性。
該域的值只能被初始化一次,一旦在構造器中初始化完成并正確發布后,其他線程可見。
未正確發布的實例:
public?class?WrongUser?{private?final?String?name;public?WrongUser(String?name)?{UserHolder.user?=?this;this.name?=?name;} }public?class?UserHolder?{public?static?WrongUser?user?=?null; }如果域沒有被聲明為final,則構造方法完成之后,其他線程不一定看得到這個域被初始化之后的值,而有可能是默認值。對于final域,在代碼執行時,可以被保持在寄存器中,不用從主存中頻繁重新讀取。
5、原子操作
Java代碼中的一條語句,可能實際上對應的是多條字節碼指令,CPU在一條指令的執行過程中不會進行線程調度和上下文切換,但是在兩條指令的執行間隙,可能發生線程切換。
如果value++由一條CPU指令完成的話,就不存在多線程訪問的問題==》原子操作。
二、基本線程同步方式
1、synchronized關鍵詞
具有可見性、有序性、原子性,堪稱萬能。
會導致線程進入blocked狀態,等待獲取鎖。
可見性:當鎖被釋放時,對共享變量的修改會從CPU緩存直接寫回到主存中;當鎖被獲取時,CPU的緩存內存被置為失效狀態,從主存中重新讀取共享變量的值。
有序性:編譯器在處理synchronized代碼塊的時候,不會把其中包含的代碼移到synchronized之外,從而避免了由于代碼重排而造成的問題。
原子性:一個線程運行執行synchronized代碼塊之前,需要先獲取對應的監視器上的鎖對象,執行完之后自動釋放。保證同一時刻只有一個線程在操作同步塊里頭的變量。
2、Object類的wait/notify/notifyAll方法
線程的同步,通過synchronized來解決,而線程之間的協作,則需要等待-通知機制。
public?class?VolatileDemo?{private?volatile?boolean?done;public?void?setDone(boolean?done){this.done?=?done;}public?void?work()?{while?(!done)?{//執行任務}} }上面這種讓線程處于忙于等待的情況,需要占用CPU的時間,會對性能造成影響,可以使用等待-通知機制。
(1)wait方法
java中每個對象除了有與之關聯的監視器對象之外,還有一個與之關聯的包含線程的等待集合。成功調用wait方法的前提是當前線程獲取到監視器對象的鎖,如果沒有鎖則拋出java.lang.IllegalMonitorStateException異常,如果獲得到鎖之后,那么當前線程被添加到對象關聯的等待集合中,同時釋放持有的監視器對象上的鎖,進入等待狀態。
wait方法必須在synchronized中進行,限期等待在一定時間過后自動喚醒,無限期等待則需要其他線程喚醒。
(2)notify與notifyAll
notify由虛擬機實現來決定喚醒哪個線程,可能是隨機的,不按進入等待集合順序的;
notifyAll喚醒該對象等待集合中的所有線程,但會導致有些沒有必要的線程被喚醒之后,馬上又進入等待狀態,造成一定性能影響,但是可以保證程序的正確性。
被喚醒后,需要重新競爭獲取對象的鎖,然后執行后續方法。
(3)意外喚醒
處于某個對象關聯的等待集合中的線程可能被意外喚醒,這是由底層操作系統和虛擬機實現所產生的非正常行為,這種意外無法避免,需要開發人員注意處理。把wait方法置于循環之中。
synchronized(obj){//避免意外喚醒while(/*條件不滿足*/){obj.wait();} }三、使用Thread類
1、線程狀態
(1)NEW
新建尚未運行
(2)RUNNABLE
處于可運行狀態:正在運行或準備運行
(3)BLOCKED
等待獲取鎖時進入的狀態
(4)WAITING
通過wait方法進入的等待
(5)TIMED_WAITING
通過sleep或wait timeout方法進入的限期等待的狀態
(6)TERMINATED
線程終止狀態
2、線程中斷
線程之間的一種通信方式,一般情況下,中斷一個線程會再對應的Thread類的對象上設置一個標記,該標記用來記錄當前的中斷狀態,通過Thread類的isInterrupted方法可以查詢此標記來判斷是否有中斷請求發生。
當線程由于調用wait,join,sleep方法進入等待狀態時,如果收到中斷請求,線程會進入InterruptedException異常的處理邏輯。在該異常發生時,當前線程對應的Thread類的對象的中斷標記會被清空,相當于該中斷請求已經被異常處理邏輯處理了。如果當前異常處理代碼中不適合處理該異常又無法把它重新拋出來,則應該通過interrupt方法來重新中斷該線程,這樣就保存了當前線程曾經被中斷過的狀態信息,可以讓后續代碼來處理該中斷請求。
interrupted方法不但可以判斷當前線程是否被中斷,還可以清除線程內部的中斷標記。如果返回true則說明該線程曾經被中斷過,在該方法調用完成之后,中斷標記會被清空。
如果中斷發生時,線程處于對象關聯的等待集合中,則該線程會被移除集合,離開等待狀態(執行異常處理邏輯)。
如果中斷跟喚醒同時發生時,對于notifyAll方法至少有一個線程被喚醒,或者所有的線程被中斷,喚醒請求不會丟失,不受中斷影響;如果一個線程被選為喚醒對象,同時又被中斷,且虛擬機選擇讓線程中斷,則等待集合中另外一個對象必須被喚醒。
3、線程等待、睡眠和讓步
A調用B的join方法,即A等待線程B完成。
public?static?void?main(String[]?args){Thread?thread?=?new?Thread(){public?void?run(){try{Thread.sleep(5000);}?catch?(InterruptedException?e)?{e.printStackTrace();}}};thread.start();try{//主線程等待線程運行結束thread.join();}?catch?(InterruptedException?e)?{e.printStackTrace();}}sleep方法會導致線程進入TIMED_WAITING狀態,但是不會釋放所持有的鎖,因此不要把sleep放在synchronized方法中,否則會造成其他線程等待鎖的時間太長(debug是可用)。
如果當前線程因為某些原因無法繼續運行,可用使用yield方法來嘗試讓出所占用的CPU資源,讓其他線程獲取運行的機會,yield對操作系統上的調度器來說是一個信號,但調度器不一定會立即進行線程切換。調用yield方法可以使線程切換頻繁一些,可以暴露出多線程相關錯誤。
四、非阻塞方式
鎖機制給多線程帶來的性能影響主要來自于其帶來的線程blocked問題。
如果能夠把讀取、修改、寫入三步組成一個CPU的原子操作,則CPU在執行這三步時,不會發生線程切換,也不會造成數據的不一致問題。
通過CAS(Compare-and-Swap)操作可以實現不依賴鎖機制的非阻塞算法。一般將其放在一個無限循環當中調用,如果當前循環沒能完成修改操作,就不斷進行嘗試,總會在某個時機上完成修改操作。
java中的CAS操作包含在atomic包里頭:
1、支持以原子操作來進行更新的數據類型的java類
AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference
2、提供對數組類型的變量進行處理的java類
把數組聲明為volatile只能保證對引用變量本身的修改是對其他線程可見的,但是不涉及數組中所包含的元素。AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray類把volatile的語義擴展到了數組的元素訪問中
3、通過反射方式對任何對象中包含的volatile變量使用compareAndSet方法進行修改
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater,分別可以用來對聲明為volatile的int類型、long類型、對象引用類型的變量進行修改。提供了一種方式把compareAndSet方法功能擴展到任何java類中聲明的volatile域上。
public?class?TreeNode?{private?volatile?TreeNode?parent;private?static?final?AtomicReferenceFieldUpdater<TreeNode,?TreeNode>?parentUpdater=?AtomicReferenceFieldUpdater.newUpdater(TreeNode.class,?TreeNode.class,?"parent");public?boolean?compareAndSetParent(TreeNode?expect,?TreeNode?update)?{return?parentUpdater.compareAndSet(this,?expect,?update);} }五、高級使用工具
六、Java7新特性
七、ThreadLocal類
轉載于:https://my.oschina.net/scipio/blog/297650
總結
以上是生活随笔為你收集整理的多线程与并发编程实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【C++基础 09】避免对象的拷贝
- 下一篇: 转:word2vec 中的数学原理详解