併發編程的實現原理-synchronized-筆記

多線程的緩存數據須要放到chm,修改緩存數據,多線程計數統計數據都須要使用鎖java

synchronized的使用編程

  • 在多線程併發編程中synchronized一直是元老級角色,不少人都會稱呼它爲重量級鎖。
  • 可是,隨着Java SE 1.6對synchronized進行了各類優化以後,
  • 有些狀況下它就並不那麼重了,
    • Java SE 1.6中爲了減小得到鎖和釋放鎖帶來的性能消耗而引入的偏向鎖輕量級鎖
    • 以及鎖的存儲結構和升級過程。
  • 咱們仍然沿用前面使用的案例,而後經過synchronized關鍵字來修飾在inc的方法上。
  • 再看看執行結果:
    • public class Demo{
              private static int count=0;
              public static void inc(){
                  synchronized (Demo.class) {
                      try {
                          Thread.sleep(1);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      count++;
                  }
              }
              public static void main(String[] args) throws InterruptedException {
                  for(int i=0;i<1000;i++){
                      new Thread(()->Demo.inc()).start();
                  }
                  Thread.sleep(3000);
                  System.out.println("運行結果"+count);
              }
          }

synchronized的三種應用方式緩存

  • synchronized有三種方式來加鎖,分別是
    • 修飾實例方法,做用於當前實例加鎖,
      • 進入同步代碼前要得到當前實例的鎖
    • 靜態方法,做用於當前類對象加鎖,
      • 進入同步代碼前要得到當前類對象的鎖
    • 修飾代碼塊,指定加鎖對象,對給定對象加鎖,
      • 進入同步代碼庫前要得到給定對象的鎖。

synchronized括號後面的對象多線程

  • synchronized括號裏面的對象是一把鎖,
  • 在java中任意一個對象均可以成爲鎖,
    • 簡單來講,咱們把object比喻是一個key,
      • 擁有這個key的線程才能執行這個方法,
      • 拿到這個key之後在執行方法過程當中,這個key是隨身攜帶的,而且只有一把。
    • 若是後續的線程想訪問當前方法,
      • 由於沒有key因此不能訪問只能在門口等着,
      • 等以前的線程把key放回去。
    • 因此,synchronized鎖定的對象必須是同一個,
      • 若是是不一樣對象,就意味着是不一樣的房間的鑰匙,
      • 對於訪問者來講是沒有任何影響的

synchronized的字節碼指令併發

  • 經過javap -v 來查看對應代碼的字節碼指令,
    • 對於同步塊的實現使用了monitorentermonitorexit指令,
      • 前面咱們在講JMM的時候,提到過這兩個指令,他們隱式的執行了LockUnLock操做,用於提供原子性保證。
    • monitorenter指令插入到同步代碼塊開始的位置、
    • monitorexit指令插入到同步代碼塊結束位置,
    • jvm須要保證每一個monitorenter都有一個monitorexit對應。
  • 這兩個指令,本質上都是對一個對象的監視器(monitor)進行獲取,
  • 這個過程是排他的,
    • 也就是說同一時刻只能有一個線程獲取到由synchronized所保護對象的監視器線程
  • 執行到monitorenter指令時,
    • 會嘗試獲取對象所對應的monitor全部權,也就是嘗試獲取對象的鎖;
    • 而執行monitorexit,就是釋放monitor的全部權

synchronized的鎖的原理jvm

  • jdk1.6之後對synchronized鎖進行了優化,包含偏向鎖、輕量級鎖、重量級鎖;
  • 在瞭解synchronized鎖以前,咱們須要瞭解兩個重要的概念,一個是對象頭、另外一個是monitor

Java對象頭ide

  • 在Hotspot虛擬機中,對象在內存中的佈局分爲三塊區域:對象頭實例數據對齊填充
  • Java對象頭是實現synchronized的鎖對象的基礎,
    • 通常而言,synchronized使用的鎖對象是存儲在Java對象頭裏。
    • 它是輕量級鎖和偏向鎖的關鍵
    • 鎖的全部信息,都記錄在java的對象頭中

Mark Word用於存儲對象自身的運行時數據工具

  • 如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等等。
  • Java對象頭通常佔有兩個機器碼(在32位虛擬機中,1個機器碼等於4字節,也就是32bit


Monitoroop

  • 什麼是Monitor?
    • 咱們能夠把它理解爲一個同步工具,也能夠描述爲一種同步機制。
    • 全部的Java對象是天生的Monitor,每一個object的對象裏 markOop->monitor() 裏能夠保存ObjectMonitor的對象。
    • 從源碼層面分析一下monitor對象
      • Ø oop.hpp下的oopDesc類是JVM對象的頂級基類,
        • 因此每一個object對象都包含markOop
      • Ø markOop.hpp**中** markOopDesc繼承自oopDesc,
        • 並擴展了本身的monitor方法,
        • 這個方法返回一個ObjectMonitor指針對象
      • Ø objectMonitor.hpp,在hotspot虛擬機中,
        • 採用ObjectMonitor類來實現monitor,

synchronized的鎖升級和獲取過程佈局

  • 瞭解了對象頭以及monitor之後,接下來去分析synchronized的鎖的實現,就會很是簡單了。
  • 前面講過synchronized的鎖是進行過優化的,引入了偏向鎖、輕量級鎖;
  • 鎖的級別從低到高逐步升級, 無鎖->偏向鎖->輕量級鎖->重量級鎖.
  • 而且鎖只能升級不能降級

自旋鎖(CAS)

  • 自旋鎖就是讓不知足條件的線程等待一段時間,而不是當即掛起。
    • 看持有鎖的線程是否可以很快釋放鎖。
  • 怎麼自旋呢?
    • 其實就是一段沒有任何意義的循環。
    • 雖然它經過佔用處理器的時間來避免線程切換帶來的開銷,
    • 可是若是持有鎖的線程不能在很快釋放鎖,
      • 那麼自旋的線程就會浪費處理器的資源,
      • 由於它不會作任何有意義的工做。
  • 因此,自旋等待的時間或者次數是有一個限度的
    • 若是自旋超過了定義的時間仍然沒有獲取到鎖,則該線程應該被掛起

偏向鎖

  • 大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,
  • 爲了讓線程得到鎖的代價更低而引入了偏向鎖。
  • 當一個線程訪問同步塊並獲取鎖時,
    • 會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,
    • 之後該線程在進入和退出同步塊時不須要進行CAS操做來加鎖和解鎖,
    • 只需簡單地測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖。
    • 若是測試成功,表示線程已經得到了鎖。
    • 若是測試失敗,則須要再測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖):
      • 若是沒有設置,則使用CAS競爭鎖;
      • 若是設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程

輕量級鎖

  • 引入輕量級鎖的主要目的是在沒有多線程競爭的前提下,
  • 減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗。
  • 當關閉偏向鎖功能或者多個線程競爭偏向鎖致使偏向鎖升級爲輕量級鎖,則會嘗試獲取輕量級鎖
  • 輕量鎖與偏向鎖不一樣的是: 
    1. 輕量級鎖每次退出同步塊都須要釋放鎖,而偏向鎖是在競爭發生時才釋放鎖 
    2. 每次進入退出同步塊都須要CAS更新對象頭 
    3. 爭奪輕量級鎖失敗時,自旋嘗試搶佔鎖

重量級鎖

  • 當競爭線程嘗試佔用輕量級鎖失敗屢次以後
    • 輕量級鎖就會膨脹爲重量級鎖
    • 重量級線程指針指向競爭線程,競爭線程也會阻塞,
    • 等待輕量級線程釋放鎖後喚醒他。
  • 重量級鎖經過對象內部的監視器(monitor)實現,
    • 其中monitor的本質是依賴於底層操做系統的Mutex Lock實現,
    • 操做系統實現線程之間的切換須要從用戶態到內核態的切換,切換成本很是高。
  • 前面咱們在講Java對象頭的時候,講到了monitor這個對象,
    • 在hotspot虛擬機中,經過ObjectMonitor類來實現monitor。
    • 他的鎖的獲取過程的體現會簡單不少
  • 與輕量級鎖區別區別是:
    • 競爭失敗後,線程阻塞,
    • 釋放鎖後,喚醒阻塞的線程,
    • 不使用自旋鎖,不會那麼消耗CPU,
    • 因此重量級鎖適合用在同步塊執行時間長的狀況下。

wait和notify

  • wait和notify是用來讓線程進入等待狀態以及使得線程喚醒的兩個操做
public class ThreadWait extends Thread{
        private Object lock;
        public ThreadWait(Object lock) {
            this.lock = lock;
        }
        @Override
        public void run() {
            synchronized (lock){
                System.out.println("開始執行 thread wait");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("執行結束 thread wait");
            }
        }
    }

    public class ThreadNotify extends Thread{
        private Object lock;
        public ThreadNotify(Object lock) {
            this.lock = lock;
        }
        @Override
        public void run() {
            synchronized (lock){
                System.out.println("開始執行 thread notify");
                lock.notify();
                System.out.println("執行結束 thread notify");
            }
        }
    }

wait和notify的原理

  • 調用wait方法,首先會獲取監視器鎖,
  • 得到成功之後,會讓當前線程進入等待狀態進入等待隊列而且釋放鎖;
  • 而後當其餘線程調用notify或者notifyall之後,會選擇從等待隊列中喚醒任意一個線程,
  • 而執行完notify方法之後,並不會立馬喚醒線程,
    • 緣由是當前的線程仍然持有這把鎖,處於等待狀態的線程沒法得到鎖。
  • 必需要等到當前的線程執行完按monitorexit指令之後,也就是鎖被釋放之後,處於等待隊列中的線程就能夠開始競爭鎖了

wait和notify爲何須要在synchronized裏面

  • wait方法的語義有兩個,
    • 一個是釋放當前的對象鎖、
      • 執行結果:
    • 另外一個是使得當前線程進入阻塞隊列,
    • 而這些操做都和監視器是相關的,
    • 因此wait必需要得到一個監視器鎖
  • 而對於notify來講也是同樣,它是喚醒一個線程,
  • 既然要去喚醒,首先得知道它在哪裏?
  • 因此就必需要找到這個對象獲取到這個對象的鎖,
  • 而後到這個對象的等待隊列中去喚醒一個線程。
相關文章
相關標籤/搜索