學(xué)習(xí)JAVA中synchronized關(guān)鍵字

1.synchronized介紹

  • 關(guān)鍵字synchronized可以保證在同一時刻只有一個線程可以執(zhí)行某個方法或者某個代碼塊,同時synchronized可以保證一個線程的變化(共享數(shù)據(jù)的變化)被其他線程所看到的。

2.synchronized用法

2.1同步普通方法

  • 作用于當(dāng)前實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖,線程正在訪問該方法時,其他試圖訪問該對象該方法的線程會被阻塞。
public class SyncronizedTest implements Runnable {
    static int i = 0;
    public synchronized void increase() {
        i++;
    }
    @Override
    public void run() {
        for (int j = 0; j < 10000; j++) {
            increase();
        }
    }
    public static void main(String[] args)throws InterruptedException {
        SyncronizedTest test = new SyncronizedTest();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
20000
  • 開啟兩個線程t1和t2操作test的同一個共享資源i,對i進(jìn)行i++操作,該操作不具備原子性,首先需要讀取i的值,然后再給i+1。如果increase()不使用synchronized關(guān)鍵字,那么很有可能在t1讀取i值和寫i值的期間,t2也讀取i值,此時i值為舊值(即t1沒寫之前),然后t1對i加1后,t2也對i加1,最終i的值會比20000小,這就造成了線程不安全。
  • 因此在increase()前使用了synchronized關(guān)鍵字,當(dāng)t1對i進(jìn)行操作時,會拿到test的鎖,如果此時t2也要進(jìn)行操作,但是test沒有釋放鎖,一個對象只有一把鎖,那么test只能等待t1操作完,釋放鎖后才能進(jìn)行。這樣就保證了同一時刻只有一個線程可以執(zhí)行increase()方法。
  • 但是如果是兩個對象分別調(diào)用increase()方法,那么是允許的,比如下面代碼里面的,線程t1需要訪問test對象調(diào)用increase(),線程t2需要訪問test1對象調(diào)用increase(),t1與t2會進(jìn)入各自的對象鎖,所以線程是不安全的,最后i的值還是小于20000。
public class SyncronizedTest implements Runnable {
    static int i = 0;
    public synchronized void increase() {
        i++;
    }
    @Override
    public void run() {
        for (int j = 0; j < 10000; j++) {
            increase();
        }
    }
    public static void main(String[] args)throws InterruptedException {
        SyncronizedTest test = new SyncronizedTest();
        SyncronizedTest test1 = new SyncronizedTest();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test1);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
19851

2.2同步靜態(tài)方法

  • 作用于當(dāng)前類對象加鎖,進(jìn)入同步代碼之前要獲得當(dāng)前類對象的鎖。
  • 鑒于以上問題,可以將increase()方法設(shè)為static的,那么無論實(shí)例化多少對象,鎖對象為當(dāng)前類的class對象,這樣當(dāng)t1運(yùn)行時,t2線程如果也想運(yùn)行,需要獲取當(dāng)前class對象的鎖,此時鎖被占用,t2線程只能等待,這樣就保證了線程安全。
public class SyncronizedTest implements Runnable {
    static int i = 0;
    public static synchronized void increase() {
        i++;
    }
    @Override
    public void run() {
        for (int j = 0; j < 10000; j++) {
            increase();
        }
    }
    public static void main(String[] args)throws InterruptedException {
        SyncronizedTest test = new SyncronizedTest();
        SyncronizedTest test1 = new SyncronizedTest();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test1);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
20000

2.3同步方法塊

  • 指定加鎖對象,對給定對象加鎖,進(jìn)入同步代碼庫前要獲得給定對象的鎖。
  • 但是有時一個方法體比較大,需要同步的代碼卻很少,此時就可以使用同步方法塊。如下所示,每當(dāng)線程進(jìn)入到此代碼塊時,如果持有test對象的鎖,那么其他線程就必須等待,這樣就保證只有一個線程可以進(jìn)行i++操作。
public class SyncronizedTest implements Runnable {
    static SyncronizedTest test = new SyncronizedTest();
    static int i = 0;
    @Override
    public void run() {
        //其他一些操作......
        synchronized(test){
            for(int j=0;j<10000;j++){
                i++;
            }
        }
    }
    public static void main(String[] args)throws InterruptedException {
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
20000

3.synchronized原理

3.1堆內(nèi)存

  • 在JVM中,對象在內(nèi)存中的布局分為:對象頭、實(shí)例變量和填充數(shù)據(jù)。


    堆內(nèi)存.png
  • 實(shí)例變量:存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息,如果是數(shù)組的實(shí)例部分還包括數(shù)組的長度。
  • 填充數(shù)據(jù):由于虛擬機(jī)要求對象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對齊。
  • 對象頭:對象頭中有兩類信息,一是mark word,二是類型指針,如果是數(shù)組,還有記錄數(shù)組長度的數(shù)據(jù)。類型指針是指向該對象所屬類的指針,虛擬機(jī)通過這個指針來確定這個對象是哪個類;mark word用于存儲對象的hashcode、GC分代年齡、鎖狀態(tài)等信息。
  • 32位JVM的Mark Word默認(rèn)存儲結(jié)構(gòu)


    1.png
  • 由于對象頭的信息是與對象自身定義的數(shù)據(jù)沒有關(guān)系的額外存儲成本,因此考慮到JVM的空間效率,Mark Word 被設(shè)計(jì)成為一個非固定的數(shù)據(jù)結(jié)構(gòu),以便存儲更多有效的數(shù)據(jù),它會根據(jù)對象本身的狀態(tài)復(fù)用自己的存儲空間,如32位JVM下,除了上述列出的Mark Word默認(rèn)存儲結(jié)構(gòu)外,還有如下可能變化的結(jié)構(gòu):


    2.png

重量級鎖也就是synchronized的對象鎖,鎖標(biāo)識位為10,其中指針指向的是monitor對象起始地址。每個對象都存在著一個 monitor 與之關(guān)聯(lián),對象與其 monitor 之間的關(guān)系有存在多種實(shí)現(xiàn)方式,如monitor可以與對象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對象鎖時自動生成,但當(dāng)一個 monitor 被某個線程持有后,它便處于鎖定狀態(tài)。在Java虛擬機(jī)(HotSpot)中,monitor是由ObjectMonitor實(shí)現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)有:_count,_WaitSet,_EntryList等。

ObjectMonitor中有兩個隊(duì)列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當(dāng)多個線程同時訪問一段同步代碼時,首先會進(jìn)入 _EntryList 集合,當(dāng)線程獲取到對象的monitor 后進(jìn)入 _Owner 區(qū)域并把monitor中的owner變量設(shè)置為當(dāng)前線程同時monitor中的計(jì)數(shù)器count加1,若線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的monitor,owner變量恢復(fù)為null,count自減1,同時該線程進(jìn)入 WaitSe t集合中等待被喚醒。若當(dāng)前線程執(zhí)行完畢也將釋放monitor(鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor(鎖)。

monitor對象存在于每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因,同時也是notify/notifyAll/wait等方法存在于頂級對象Object中的原因。

3.2synchronized原理

同步代碼塊如下:

public class SyncBlock {
    public int i;
    public void syncTask(){
        //同步代碼庫
        synchronized (this){
        i++;
        }
    }
}

編譯上述代碼并使用javap反編譯后得到字節(jié)碼如下:

Compiled from "SyncBlock.java"
public class SyncBlock {
  public int i;

  public SyncBlock();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void syncTask();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: aload_0
       5: dup
       6: getfield      #2                  // Field i:I
       9: iconst_1
      10: iadd
      11: putfield      #2                  // Field i:I
      14: aload_1
      15: monitorexit
      16: goto          24
      19: astore_2
      20: aload_1
      21: monitorexit
      22: aload_2
      23: athrow
      24: return
    Exception table:
       from    to  target type
           4    16    19   any
          19    22    19   any
}
  • 從字節(jié)碼中可知同步語句塊的實(shí)現(xiàn)使用的是monitorenter 和 monitorexit 指令,monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結(jié)束位置,當(dāng)執(zhí)行monitorenter指令時,當(dāng)前線程將試圖獲取 objectref(即對象鎖) 所對應(yīng)的 monitor 的持有權(quán),當(dāng) objectref 的 monitor 的進(jìn)入計(jì)數(shù)器為 0,那線程可以成功取得 monitor,并將計(jì)數(shù)器值設(shè)置為 1,取鎖成功。如果當(dāng)前線程已經(jīng)擁有 objectref 的 monitor 的持有權(quán),那它可以重入這個 monitor,重入時計(jì)數(shù)器的值也會加 1。倘若其他線程已經(jīng)擁有 objectref 的 monitor 的所有權(quán),那當(dāng)前線程將被阻塞,直到正在執(zhí)行線程執(zhí)行完畢,即monitorexit指令被執(zhí)行,執(zhí)行線程將釋放 monitor(鎖)并設(shè)置計(jì)數(shù)器值為0 ,其他線程將有機(jī)會持有 monitor 。值得注意的是編譯器將會確保無論方法通過何種方式完成,方法中調(diào)用過的每條 monitorenter 指令都有執(zhí)行其對應(yīng) monitorexit 指令,而無論這個方法是正常結(jié)束還是異常結(jié)束。為了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執(zhí)行,編譯器會自動產(chǎn)生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執(zhí)行 monitorexit 指令。從字節(jié)碼中也可以看出多了一個monitorexit指令,它就是異常結(jié)束時被執(zhí)行的釋放monitor 的指令。

4.synchronized原子性,可見性和有序性保障

4.1synchronized原子性

原子性是指一個操作是不可中斷的,要么全部執(zhí)行,要不就都不執(zhí)行。

  • 線程是CPU調(diào)度的基本單位,CPU會根據(jù)不同的調(diào)度算法進(jìn)行線程調(diào)度,當(dāng)一個線程獲得時間片之后開始執(zhí)行,在時間片耗盡后,就會失去CPU使用權(quán),所以在多線程情況下,由于時間片在線程間輪換,就會發(fā)生原子性問題。
  • 但是synchronized修飾的代碼,通過monitorenter 和monitorexit 指令,保證代碼在同一時間只能被一個線程訪問,在鎖未釋放之前,無法被其他線程訪問。因此,在java中可以使用4.1synchronized來保證方法和代碼塊內(nèi)操作的原子性。

4.2synchronized可見性

可見性是指當(dāng)多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

  • synchronized為了保證可見性,對一個變量解鎖前,必須先把此變量同步回主存中,這樣解鎖后,后續(xù)線程就可以訪問到被修改后的值。

4.2synchronized有序性

有序性是指程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。

  • 編譯器和處理器遵守as-if-serial語義,保證單線程中指令排序是有一定的限制的,可以認(rèn)為單線程程序是按照順序執(zhí)行的,所以由synchronized修飾的代碼,同一時間只能被同一線程訪問,也就是單線程執(zhí)行,所以可以保證有序性。

5.synchronized的可重入性

定義:若一個程序或子程序可以“在任意時刻被中斷然后操作系統(tǒng)調(diào)度執(zhí)行另外一段代碼,這段代碼又調(diào)用了該子程序不會出錯”,則稱其為可重入(reentrant或re-entrant)的。即當(dāng)該子程序正在運(yùn)行時,執(zhí)行線程可以再次進(jìn)入并執(zhí)行它,仍然獲得符合設(shè)計(jì)時預(yù)期的結(jié)果。與多線程并發(fā)執(zhí)行的線程安全不同,可重入強(qiáng)調(diào)對單個線程執(zhí)行時重新進(jìn)入同一個子程序仍然是安全的。

  • synchronized擁有強(qiáng)制原子性的內(nèi)部鎖機(jī)制,是一個可重入鎖。因此,在一個線程使用synchronized方法時調(diào)用該對象另一個synchronized方法,即一個線程得到一個對象鎖后再次請求該對象鎖,是永遠(yuǎn)可以拿到鎖的。
  • synchronized可重入原因:
    每個鎖關(guān)聯(lián)一個monitor,當(dāng)計(jì)數(shù)器為0時表示該鎖沒有被任何線程持有,那么任何線程都都可能獲得該鎖而調(diào)用相應(yīng)方法。當(dāng)一個線程請求成功后,JVM會記下持有鎖的線程,并將計(jì)數(shù)器計(jì)為1。此時其他線程請求該鎖,則必須等待。而該持有鎖的線程如果再次請求這個鎖,就可以再次拿到這個鎖,同時計(jì)數(shù)器會遞增。當(dāng)線程退出一個synchronized方法/塊時,計(jì)數(shù)器會遞減,如果計(jì)數(shù)器為0則釋放該鎖。

6.synchronized和volatile的區(qū)別

  • 粒度不同:volatile針對變量 ,synchronized鎖對象和類
  • synchronized阻塞,volatile線程不阻塞
  • synchronized保證三大特性,volatile不保證原子性
  • synchronized編譯器優(yōu)化,volatile不優(yōu)化

7.synchronized與Lock的區(qū)別

  • synchronized是java內(nèi)置關(guān)鍵字,在jvm層面,Lock是個java類;
  • synchronized無法判斷是否獲取鎖的狀態(tài),Lock可以判斷是否獲取到鎖;
  • synchronized會自動釋放鎖(a 線程執(zhí)行完同步代碼會釋放鎖 ;b 線程執(zhí)行過程中發(fā)生異常會釋放鎖),Lock需在finally中手工釋放鎖(unlock()方法釋放鎖),否則容易造成線程死鎖;
  • 用synchronized關(guān)鍵字的兩個線程1和線程2,如果當(dāng)前線程1獲得鎖,線程2線程等待。如果線程1阻塞,線程2則會一直等待下去,而Lock鎖就不一定會等待下去,如果嘗試獲取不到鎖,線程可以不用一直等待就結(jié)束了;
  • synchronized的鎖可重入、不可中斷、非公平,而Lock鎖可重入、可判斷、可公平(兩者皆可)
  • Lock鎖適合大量同步的代碼的同步問題,synchronized鎖適合代碼少量的同步問題。

引用
https://blog.csdn.net/qq_41247433/article/details/79433831
https://blog.csdn.net/zfy163520/article/details/89138218
https://blog.csdn.net/u010647035/article/details/82320571
https://blog.csdn.net/qq_33173608/article/details/88202474
https://www.cnblogs.com/iyyy/p/7993788.html
https://www.cnblogs.com/cielosun/p/6684775.html
以上僅為筆記,如有錯誤,接受指正。

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

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

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