當咱們在談論synchronized的時候,咱們在談論什麼?

synchronized是作什麼用的?

synchronized是Java中實現鎖的一種方式,咱們能夠經過synchronized來給一個方法,一個屬性,一個對象等資源進行加鎖。html

咱們爲何須要加鎖呢?

可能你會說,是由於當某個資源被多個線程訪問時,咱們須要同步協調線程訪問的順序,在這種狀況下,咱們要對該資源加鎖。java

好比,在火車票放票期間,禁止售票員訪問票源,這個本質上就是將資源(火車票源)加鎖,協調了售票員和管理員的操做順序。程序員

若是給外行人解釋,這麼說已經足夠了。但對於一個有態度的技術人來講,這種描述就太淺顯了。這個問題,咱們還得從源頭上提及。安全

咱們使用一段代碼來表達上面的例子:多線程

//火車票程序
public class TrainTicket {
  int beiJing = 0, shangHai = 0;
  
  //放票
  public void writer() {
    beiJing = 1;
    shangHai = 2;
  }

  //查票
  public void reader() {
    int r1 = beiJing;
    int r2 = shangHai;
  }
}
複製代碼
//T-1 放票線程
recordering.writer();
複製代碼
//T-2 查票線程
recordering.reader();
複製代碼

咱們按「順序」執行T1和T2,結果會是什麼呢? 咱們指望的結果r1=1, r2=2 可是結果極可能是r1=0, r2=0 也多是r1=1, r2=0 也多是...oracle

爲何和咱們預期的結果不同呢,是哪裏出了問題?app

這就是咱們今天要重點說的一個概念——重排序優化

重排序(Reordering)是編譯器(Compiler)爲了優化執行效率而作的一種策略。在單線程中,重排序要保證不影響程序的語義,所以對於沒有依賴關係的語句,均可能被重排序。 好比spa

int a=0;
int b=1;
複製代碼

第一行語句和第二行語句並不構成依賴,因此編譯器能夠任意調換順序。線程

重點來了,那麼在多線程中環境中,涉及到重排序時,就會遇到線程安全的問題。 所以,Java編譯器並不會保證線程安全,線程是否安全由程序員確保的。

這不是甩鍋嘛!!!

麼辦法,這鍋就是程序員的!

好吧,讓咱們再回到最初的問題: 怎麼更好地背鍋?

哦,不對。

爲何,咱們須要對共享資源加鎖?

敲黑板,劃重點

加鎖是爲了消除程序因重排序而產生的線程安全問題,最終保證語義的一致性和數據的一致性!

說到這,好像說的比較清楚了,可是還有一個根本性的問題

當咱們用了synchronized(鎖),怎麼就能作到線程安全呢?

happen-before原則

在Java的內存模型(JMM)中定義了一系列的happen-before原則,具體這個原則如何描述,筆者也很差把握,若是執意要下定義的話,我認爲: happen-before是Java提供的一系列的確保局部有序的規則。 再具體一點就是,若是A操做happens-before於B操做,那麼也就意味着A的操做結果對B是可見的。

能夠回到咱們火車票的例子理解一下,若是出票操做happens-before於查票操做,那麼出票的結果對查票來講必定是可見的,也就是說出票結果必定會被正確查到。

下面是具體的每一條規則

  • Each action in a thread happens before every action in that thread that comes later in the program's order.
  • An unlock on a monitor happens before every subsequent lock on that same monitor.
  • A write to a volatile field happens before every subsequent read of that same volatile.
  • A call to start() on a thread happens before any actions in the started thread.
  • All actions in a thread happen before any other thread successfully returns from a join() on that thread.

這些規則的中文翻譯網上有不少,我之因此貼英文,主要是考慮到反正這種條文沒有人會去記,反卻是貼英文官方文檔更合適一些,也能幫助到想查官方文檔的同窗。

針對第二條(關於鎖)的規則擴展一下。 同一個鎖的unlock操做在lock以前,也就是說 一個鎖處於被鎖定狀態,那麼必須先執行unlock操做後面才能進行lock操做。

正式由於有了這條規則,咱們就能夠經過加鎖的方式實現線程安全,將以上代碼改造一下

//火車票程序
public class TrainTicket {
  int beiJing = 0, shangHai = 0;
  
  //放票
  public synchronized void writer() {
    beiJing = 1;
    shangHai = 2;
  }

  //查票
  public synchronized void reader() {
    int r1 = beiJing;
    int r2 = shangHai;
  }
}
複製代碼

這樣,將兩個方法都加上鎖,這樣就實現了線程的安全。

編譯器作了什麼?

synchronized代碼塊編譯以後會生成一個monitorenter指令和一個或多個monitorexit指令,大體以下:

monitorenter
/*---------*/
     code
/*--------*/
monitorexit
複製代碼

對於monitorenter和monitorexit,咱們能夠理解爲每一個鎖對象擁有一個鎖的計數器和一個指向持有該鎖的線程的指針。

當執行monitorenter指令時,若是目標鎖對象的計數器爲0,那麼說明它沒有被別的線程持有,在這個狀況下,Java虛擬機會將該鎖對象的持有線程設置爲當前線程,而且將計數器加1.

在目標鎖對象的計數器不等於0的狀況下,若是其它線程訪問,則須要等待,直到持有鎖的線程釋放該鎖。(聯想一下happen-before中的第二條規則)

寫在最後

有幾個問題是可擴展的

一、重排序有三個維度上的,分別是編譯器,內存和處理器。本文中只提到了編譯器的重排序,而沒有提到內存的重排序。

二、從內存的維度上,是如何禁止重排序的?

三、happen-before原則中提到了volatile類型變量,這個類型的變量有什麼特殊之處,咱們能用它解決什麼呢?

下次有時間再細說

本文原創,轉載請註明出處

引用參考(reference):

www.cs.umd.edu/~pugh/java/… docs.oracle.com/javase/spec…

相關文章
相關標籤/搜索