[筆記]Java多線程基礎(chǔ)——內(nèi)存

因?yàn)樘幚砥髦黝l在硬件發(fā)展上的瓶頸,摩爾定律基本失效,現(xiàn)在真正起作用的是并行處理的Amdahl定律,畢竟,現(xiàn)在計(jì)算機(jī)的瓶頸在于存儲和通信,而不是運(yùn)算本身,并行運(yùn)算可以更充分地發(fā)揮運(yùn)算的能力,也是提升計(jì)算機(jī)性能。

一致性的問題

并行運(yùn)算通過讓多個(gè)處理器并行來提升性能,但是多處理器并行就會有內(nèi)存上的一致性問題。

  1. 硬件上的緩存一致性
    運(yùn)算不是處理器自己的事情,處理器必須與內(nèi)存交互(讀參數(shù),寫結(jié)果),但是處理器和內(nèi)存在速度上相差幾個(gè)數(shù)量級,硬件上的一個(gè)解決辦法是在處理器和內(nèi)存之間插一組高速緩存(Cache),每個(gè)處理器都有一塊兒Cache,Cache的速度相對更接近運(yùn)算器,這能提高運(yùn)算速度,但是,多個(gè)處理器的Cache同時(shí)與內(nèi)存交互時(shí),就有可能發(fā)生沖突,這就是緩存一致性的問題。
  2. 指令上的亂序執(zhí)行和指令重排序
    為了充分利用處理器的運(yùn)算能力,一段代碼中的各個(gè)語句可能被分配到不同的處理器共同處理,最后再把處理結(jié)果重組,這樣處理速度更快,但是各語句執(zhí)行的先后順序就不一定是代碼的先后順序了,這就是亂序執(zhí)行。
    JVM在編譯時(shí),也會對生成的指令做類似的操作,叫做指令重排序,指令重排序能通過多處理器并行來提升性能,但是語句的執(zhí)行順序會被重排,這可能會破壞代碼中的邏輯順序。
  3. JVM的主內(nèi)存和工作內(nèi)存
    Java不像C/C++,不會讓程序直接操作硬件和OS上的內(nèi)存,而是提供了JVM的虛擬機(jī)內(nèi)存。JVM內(nèi)存分為主內(nèi)存和線程上的工作內(nèi)存。
    主內(nèi)存
    對應(yīng)物理內(nèi)存,存放對象、靜態(tài)對象、常量等。(可以理解為堆,但其實(shí)不是一個(gè)維度)
    工作內(nèi)存
    對應(yīng)Cache甚至處理器的寄存器,每個(gè)線程都有自己的工作內(nèi)存,存放線程私有的方法參數(shù)和局部變量。(可以理解為虛擬機(jī)棧的棧幀中的局部變量表,但其實(shí)不是一個(gè)維度)
    線程不能直接操作主內(nèi)存的數(shù)據(jù),只能在工作內(nèi)存中操作目標(biāo)對象的拷貝副本(一般只拷貝對象的reference和線程要使用的字段),最后再把拷貝副本寫回主線程;線程之間也不能直接傳遞對象,要通過主內(nèi)存中轉(zhuǎn)。

JVM的主內(nèi)存和工作內(nèi)存需要頻繁交互,為了確保數(shù)據(jù)一致性,定義了8種操作(并沒有開放給用戶),確保以主內(nèi)存為準(zhǔn):

  1. lock,一個(gè)線程獨(dú)占,其他線程不可用
    同一個(gè)線程可以lock多次,但是就需要unlock同樣次
    會清空本地內(nèi)存數(shù)據(jù),重新從主內(nèi)存load或assign(確保lock后的數(shù)據(jù)與主內(nèi)存一致)
  2. unlock,線程不再占用,其他線程可以使用
    必須是lock過的對象,而且是本線程lock過的對象
    前面必須先write+store(unlock前必須寫回主內(nèi)存)
  3. read,從主內(nèi)存讀出
    后面必須load
  4. load,復(fù)制到工作內(nèi)存
    必須先read
  5. use,在工作內(nèi)存中使用該對象
    必須是load或assign過的(工作內(nèi)存不能增加對象,對象必須來自主內(nèi)存)
  6. assign,在工作內(nèi)存中為該對象賦值
    后面必須做write(修改過的對象必須寫回主內(nèi)存)
  7. write,從工作內(nèi)存讀出
    前面必須assign過(沒修改過的對象不需要寫回主內(nèi)存)
    后面必須store
  8. store,寫到主內(nèi)存
    前面必須先write

原子性、可見性、有序性

實(shí)現(xiàn)并發(fā)操作的一致性,核心就是原子性、可見性和有序性。

  • 原子性
    原子性就是操作不可被分割(避免被分到不同處理器并行)。
    基本類型的讀寫是原子性的。
    對象和代碼段可以用synchronized塊實(shí)現(xiàn)原子性,synchronized反映在字節(jié)碼上,就是monitorenter和monitorexit這對內(nèi)存屏障,屏蔽指令重排,從而實(shí)現(xiàn)原子性。
  • 可見性
    可見性針對的是變量值的修改的可見性,一個(gè)線程做了修改,其他線程能立即得知。
    Java以主內(nèi)存為核心實(shí)現(xiàn)可見性,變量修改后寫回到主內(nèi)存,變量讀取前從主內(nèi)存刷新。
    volatile、synchronized和final都能實(shí)現(xiàn)可見性。
    volatile能把修改的值立即寫回到主內(nèi)存,每次使用前還會立即從主內(nèi)存刷新(因此,每次使用的值都可能變化,所以叫volatile易變的)。
    synchronized是在unlock之前,把變量同步寫回主內(nèi)存,因?yàn)閟ynchronized每次只允許一個(gè)線程lock,所以只要先在unlock時(shí)同步數(shù)據(jù),在后面lock時(shí)就能確??梢娦?。
    final只要完成初始化就不能修改,也就不需要通知修改,所以能滿足可見性,但是如果還沒有完成初始化時(shí),其他線程就能看見final字段,就打破了可見性(也就是發(fā)生了this逃逸)。
  • 有序性
    有序性指的是按照程序代碼規(guī)定的順序執(zhí)行指令,它針對的是指令重排序。
    在線程內(nèi)的操作,天然是有序的。
    對于線程間的有序性,可以用volatile和synchronized實(shí)現(xiàn)。
    volatile禁止指令重排序。
    synchronized同一時(shí)刻只允許一個(gè)線程lock,變成線程內(nèi)的操作。

volatile、synchronized和ReentrantLock同步機(jī)制

前面提到,JVM中有volatile和synchronized兩個(gè)同步機(jī)制,其中volatile是輕量級的,synchronized是重量級的,JVM還提供了ReentrantLock重入鎖。

  • volatile
    volatile的核心就是修改立即通知+使用前立即刷新,反應(yīng)在8種內(nèi)存操作上,有3個(gè)原則:
    1.use和load綁定,use之前必須read+load。
    2.assign和store綁定,assign之后必須write+store。
    3.同一線程對兩個(gè)volatile變量測操作中,要use/assign的那個(gè)變量,需要先read/write。
    在性能和安全上,volatile有這么幾個(gè)特點(diǎn):
    1.volatile是JVM中最輕量級的同步機(jī)制
    volatile的讀操作幾乎等同于普通變量,volatile的寫操作相對較慢(需要內(nèi)存屏障避免亂序執(zhí)行),
    2.volatile滿足可見性和有序性,但不滿足原子性,所以不是并發(fā)安全的。
    比如,讓多個(gè)線程對一個(gè)volatile數(shù)值做自增運(yùn)算,雖然代碼上看起來每次取到的數(shù)據(jù)都是最新的,但是非原子性意味著代碼上的一行會被解析成多行字節(jié)碼甚至更多的機(jī)器碼,在字節(jié)碼或者機(jī)器碼中,獲取數(shù)據(jù)時(shí)是最新的,到操作數(shù)據(jù)時(shí),其他線程可能又做了修改,當(dāng)前數(shù)據(jù)就不是最新的了。
    在不需要考慮原子性的場景下,volatile性能占優(yōu),比如在單一線程中,或者僅用一個(gè)volatile變量作為約束條件:
volatile boolean flag=false;
public void shutdown(){
      flag=true;
}
public void dowork(){
      while(!flag){...} //僅利用volatile的可見性
}
  • synchronized
    synchronized是線程級別的處理,相對只是讀寫數(shù)據(jù)的volatile來說,絕對是重量級的操作,因?yàn)閟ynchronized的核心是阻塞同步。
    編譯
    synchronized編譯后,會成對地生成monitorenter和monitorexit內(nèi)存屏障,這兩個(gè)字節(jié)碼需要對reference加鎖和解鎖,需要對象參數(shù)的reference,如果沒有在代碼中明確指定對象,就會根據(jù)所在方法是類方法還是實(shí)例方法,去獲取所在的類或者實(shí)例對象的reference。
    阻塞
    線程在進(jìn)入monitorenter時(shí),如果當(dāng)前線程沒有這個(gè)鎖,而且鎖計(jì)數(shù)器不為0,說明有其他線程正在工作,當(dāng)前線程必須阻塞等待,直至其他線程釋放鎖后,才能被喚醒。可見,阻塞會浪費(fèi)大量的資源。
    系統(tǒng)消耗
    線程的阻塞和喚醒,需要操作系統(tǒng)來處理,這會從用戶態(tài)轉(zhuǎn)換為核心態(tài),這種轉(zhuǎn)換要消耗大量的處理器資源(往往超過用戶代碼的消耗)。
    串行執(zhí)行
    synchronized下,一個(gè)變量同一時(shí)刻只能有一個(gè)線程進(jìn)行l(wèi)ock,因此,持有同一個(gè)lock的兩個(gè)synchronized塊,只能串行地進(jìn)入(如果在同一個(gè)線程里就是天然串行,如果在不同線程里,就必須等lock被釋放,事實(shí)上也是串行)。這會影響程序的處理速度。
    所以synchronized是非常重量級的操作,我們應(yīng)該僅在必要的時(shí)候使用該操作,虛擬機(jī)自己也會做一些自旋等待,盡量避免線程阻塞和核心態(tài)切換。
  • ReentrantLock
    ReentrantLock是java.util.concurrent包使用的同步機(jī)制,ReentrantLock和synchronized一樣,是支持線程重入的互斥鎖,不同的是ReentrantLock是API層面的,synchronized是原生語法層面的。
    另外,ReentrantLock有三個(gè)高級功能。
    1.等待可中斷,synchronized中的線程只能阻塞。
    2.時(shí)間順序公平(公平鎖),多個(gè)線程按申請鎖的時(shí)間順序排隊(duì),synchronized中的線程做不到這一點(diǎn)。
    3.綁定多個(gè)條件,一個(gè)ReentrantLock對象可以同時(shí)綁定多個(gè)condition對象,synchronize要增加關(guān)聯(lián)條件,只能增加鎖。

先行發(fā)生

在Java中解決有序性的問題,并不是全都需要同步器,有一些是天然就能確保先后關(guān)系的規(guī)律:(先行發(fā)生僅指操作上的先后順序,不是時(shí)間上的先后順序)
1.程序次序,同一線程內(nèi),是按代碼邏輯順序。
2.管程鎖定,同一個(gè)鎖對象,時(shí)間上一定是先unlock,然后才能lock
3.volatile,一個(gè)volatile變量,時(shí)間上一定是先寫,然后才能讀
4.線程啟動,Thread一定是先start
5.線程終止,Thread一定是最后執(zhí)行終止(Thread.join())
6.線程中斷,一定是先執(zhí)行了Thread.interrupt(),然后線程的代碼才能檢測到中斷事件
7.對象終結(jié),對象的初始化一定在finalize()之前
8.傳遞性,操作的先后順序是可傳遞的,A在B前,B在C前,則A一定在C前。
先行發(fā)生與時(shí)間先后沒有關(guān)系,因?yàn)榫€程在時(shí)間上先發(fā)生的操作,與并發(fā)執(zhí)行時(shí)的順序并沒有太大關(guān)系,操作上的先發(fā)生不代表時(shí)間上的先發(fā)生,時(shí)間上的先發(fā)生也不代表操作上是先發(fā)生的。

對雙鎖檢測單例模式的解讀

我們看一個(gè)雙鎖檢索的DCL單例

public class Singleton{
    private volatile static Singleton instance; //避免指令重排序
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance == null){ //volatile確??梢娦裕易x操作性能較高
             //用synchronized確保原子性、可見性和有序性
             synchronized(Singleton.class){ //這是一個(gè)靜態(tài)類方法,所以要用class類做全局同步鎖
                 if(instance == null){ //volatile在使用時(shí)刷新,再檢查一次
                     instance=new Singleton();
                 }
             }
        }
        return instance;
    }
}

引用

《深入理解Java虛擬機(jī)》
synchronized(this)與synchronized(class)
Java集合及concurrent并發(fā)包總結(jié)(轉(zhuǎn))
深入理解java虛擬機(jī) 精華總結(jié)(面試)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容