13161216443

您所在位置: 首頁> 學習課程> java培訓 | Java并發編程 :synchronized原理

java培訓 | Java并發編程 :synchronized原理

發布百知教育 來源:學習課程 2019-11-14

并發編程中用到最多的關鍵字毫無疑問是synchronized。這篇文章就來探究下synchronized

synchronized如何使用?

synchronized是實現同步加鎖的原理?

synchronized解決了并發編程的哪些問題?


1.  synchronized使用

1.1 線程安全問題

并發編程中,當多個線程同時訪問同一個資源的時候,就會存在線程安全問題。

由于每個線程執行的過程是不可控的,所以很可能導致最終的結果與實際期望的結果相違背或者直接導致程序出錯。

舉例:

public classVolatileTest {
   public int inc = 0;

   public void increase() {
      inc++;
   }

   public static void main(String[] args) {
      final VolatileTest test = newVolatileTest();
      for (int i = 0; i < 10; i++) {
          new Thread() {
             public void run() {
                 for (int j = 0; j < 1000;j++)
                    test.increase();
             };
          }.start();
      }

      while (Thread.activeCount() > 1)
          // 保證前面的線程都執行完
          Thread.yield();
      System.out.println(test.inc);
   }
}

目的:test.inc = 10000

結果:多次執行得到的結果都小于10000

分析:線程安全問題。


當某個時間test.inc=2,有多個線程同時讀取到test.inc=2,并且同時執行加1操作,這些線程的此次操作都執行之后test.inc=3。也就是說執行了多個加1操作,卻只將結果增加了1,所以導致最終結果始終小于10000。

基本上所有的并發模式在解決線程安全問題時,都采用“序列化訪問臨界資源”的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱作同步互斥訪問。
通常來說,是在訪問臨界資源的代碼前面加上一個鎖,當訪問完臨界資源后釋放鎖,讓其他線程繼續訪問。
在Java中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock。

Java中用synchronized標記同步塊。

  • 同步塊在Java中是同步在某個對象上(監視器對象)。

  • 所有同步在一個對象上的同步塊在同一時間只能被一個線程進入并執行操作。

  • 所有其他等待進入該同步塊的線程將被阻塞,直到執行該同步塊中的線程退出。

1.2 synchronized用法

  • 普通同步方法,鎖是當前實例對象

  • 靜態同步方法,鎖是當前類的class對象

  • 同步方法塊,鎖是括號里面的對象

舉例:

public class MyClass{
   int count;
 
   // 1.實例方法
   public synchronized void add(int value){
       count += value;
   }
 
   // 2.實例方法中的同步塊 (等價于1)
   public void add(int value){
       synchronized(this){
           count += value;
       }
   }
 
   // 3.靜態方法
   public static synchronized void add(intvalue){
        count += value;
   }
 
   // 4.靜態方法中的同步塊 (等價于3)
   public static void add(int value){
       synchronized(MyClass.class){
           count += value;
       }
   }
}

2. 原理探究

如下代碼,

利用javap工具查看生成的class文件信息來分析Synchronize的實現。

代碼:

public class synchronized Test {
   // 同步代碼塊
   public void doSth1(){
      synchronized (synchronizedTest.class){
          System.out.println("HelloWorld");
      }
   }
   // 同步方法
   public synchronized void doSth2(){
       System.out.println("HelloWorld");
   }
}

使用javap對class文件進行反編譯后結果:

javap命令:D:\install\java\jdk8\bin\javap.exe -v .\synchronizedTest.class


java培訓


同步代碼塊


java培訓

從反編譯后的結果中可以看到:對于同步方法,JVM采用ACC_synchronized標記符來實現同步。對于同步代碼塊。JVM采用monitorenter、monitorexit兩個指令來實現同步。

同步代碼塊

JVM采用monitorenter、monitorexit兩個指令來實現同步。
查詢JVM規范The Java? Virtual Machine Specification[1]中關于monitorenter和monitorexit的介紹:

java培訓



大致內容如下:

  1. 可以把執行monitorenter指令理解為加鎖,執行monitorexit理解為釋放鎖。

  2. 每個對象維護著一個記錄著被鎖次數的計數器。

  3. 未被鎖定的對象的該計數器為0,當一個線程獲得鎖(執行monitorenter)后,該計數器自增變為1,當同一個線程再次獲得該對象的鎖的時候,計數器再次自增。當同一個線程釋放鎖(執行monitorexit指令)的時候,計數器再自減。

  4. 當計數器為0的時候。鎖將被釋放,其他線程便可以獲得鎖。

同步方法

JVM采用ACC_synchronized標記符來實現同步。
查詢JVM規范The Java? Virtual Machine Specification[2]中關于方法級同步的介紹:


java培訓班


大致內容如下:

  1. 方法級的同步是隱式的。同步方法的常量池中會有一個ACC_synchronized標志。

  2. 當某個線程要訪問某個方法的時候,會檢查是否有ACC_synchronized,如果有設置,則需要先獲得監視器鎖(monitor),然后開始執行方法,方法執行之后再釋放監視器鎖。這時如果其他線程來請求執行方法,會因為無法獲得監視器鎖而被阻斷住。

  3. 值得注意的是,如果在方法執行過程中,發生了異常,并且方法內部并沒有處理該異常,那么在異常被拋到方法外面之前監視器鎖會被自動釋放。

3. Monitor

無論是同步方法還是同步代碼塊都是基于監視器Monitor實現的。

Monitor是什么?

所有的Java對象是天生的Monitor,每一個Java對象都有成為Monitor的潛質,因為在Java的設計中,每一個Java對象自打娘胎里出來就帶了一把看不見的鎖,它叫做內部鎖或者Monitor鎖。

每個對象都存在著一個Monitor與之關聯,對象與其Monitor之間的關系有存在多種實現方式,如Monitor可以與對象一起創建銷毀。

Moniter如何實現線程的同步?

在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的(位于HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的)。

ObjectMonitor中有幾個關鍵屬性:

_owner:指向持有ObjectMonitor對象的線程
_WaitSet:存放處于wait狀態的線程隊列
_EntryList:存放處于等待鎖block狀態的線程隊列
_recursions:鎖的重入次數
_count:用來記錄該線程獲取鎖的次數
  • 線程T等待對象鎖:_EntryList中加入T。

  • 線程T獲取對象鎖:_EntryList移除T,_owner置為T,計數器_count加1。

  • 線程T中鎖對象調用wait():_owner置為null,計數器_count減1,_WaitSet中加入T等待被喚醒。

  • 持有對象鎖的線程T執行完畢:復位變量的值,以便其他線程進入獲取monitor。


4. 解決三大問題

保證原子性

在并發編程中的原子性:一段代碼,或者一個變量的操作,在一個線程沒有執行完之前,不能被其他線程執行。

synchronized修飾的代碼在同一時間只能被一個線程訪問,在鎖未釋放之前,無法被其他線程訪問到。

即使在執行過程中,CPU時間片用完,線程放棄了CPU,但并沒有進行解鎖。而由于synchronized的鎖是可重入的,下一個時間片還是只能被他自己獲取到,還是會由同一個線程繼續執行代碼,直到所有代碼執行完。從而保證synchronized修飾的代碼塊在同一時間只能被一個線程訪問。

保證有序性

如果在本線程內觀察,所有操作都是天然有序的。
——《深入理解Java虛擬機》

單線程重排序要遵守as-if-serial語義,不管怎么重排序,單線程程序的執行結果都不能被改變。因為不會改變執行結果,所以無須關心這種重排的干擾,可以認為單線程程序是按照順序執行的。

synchronized修飾的代碼,同一時間只能被同一線程訪問。那么也就是單線程執行的。所以,可以保證其有序性。

保證可見性

加鎖的含義不僅僅局限于互斥行為,還包括可見性。
——《Java并發編程實戰》

JMM關于synchronized的兩條語義規定保證了可見性:

  • 線程解鎖前,必須把共享變量的最新值刷新到主內存中。

  • 線程加鎖前,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值。

5. 總結

多并發編程中通過同步互斥訪問臨界資源來解決線程安全問題,Java中常用synchronized標記同步塊達到加鎖的目的。

synchronized用法有兩種,修飾方法和修飾同步代碼塊。

synchronized的實現原理:每一個Java對象都會關聯一個Monitor,通過Monitor對線程的操作實現synchronized對象鎖。

并發編程中synchronized可以保證原子性、可見性、有序性。

java培訓班:http://www.akpsimsu.com/java2019

注釋:本文內容來自 java進階架構師


上一篇:參加IT培訓,大家一定要注意這五點!

下一篇:應屆生去公司找個Java程序員的職位需要什么技能?

相關推薦

www.akpsimsu.com

有位老師想和您聊一聊

關閉

立即申請