JAVA對象方法-wait

最簡單的東西,每每包含了最複雜的實現,由於須要爲上層的存在提供一個穩定的基礎,Object做爲java中全部對象的基類,其存在的價值不言而喻,其中wait和notify方法的實現多線程協做提供了保證。html

案例

public class WaitTestDemo {

    public static void main(String[] args) {
        Message msg = new Message("process it");
        Waiter waiter = new Waiter(msg);
        new Thread(waiter,"waiterThread").start();

        Waiter waiter1 = new Waiter(msg);
        new Thread(waiter1, "waiter1Thread").start();
        
        Notifier notifier = new Notifier(msg);
        new Thread(notifier, "notifierThread").start();
 
        System.out.println("All the threads are started");
    }

    public static class Message {
        private String msg;
        public Message(String str){
            this.msg=str;
        }
        public String getMsg() {
            return msg;
        }
        public void setMsg(String str) {
            this.msg=str;
        }
    }

    public static class Waiter implements Runnable{
        private Message msg;
        public Waiter(Message m){
            this.msg=m;
        }

        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            synchronized (msg) {
                try{
                    System.out.println(name+" waiting to get notified at time:"+System.currentTimeMillis());
                    msg.wait();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(name+" waiter thread got notified at time:"+System.currentTimeMillis());
                //process the message now
                System.out.println(name+" processed: "+msg.getMsg());
            }
        }
    }

    public static class Notifier implements Runnable {
        private Message msg;

        public Notifier(Message msg) {
            this.msg = msg;
        }

        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            System.out.println(name+" started");
            try {
                Thread.sleep(1000);
                synchronized (msg) {
                    msg.setMsg(name+" Notifier work done");
                    msg.notify();
                    msg.notify();
                    //msg.notifyAll();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Output:java

All the threads are started
waiterThread waiting to get notified at time:1572344152693
waiter1Thread waiting to get notified at time:1572344152693
notifierThread started
waiterThread waiter thread got notified at time:1572344153705
waiterThread processed: notifierThread Notifier work done
waiter1Thread waiter thread got notified at time:1572344153706
waiter1Thread processed: notifierThread Notifier work done

也可使用notifyAll,輸出爲:node

All the threads are started
waiterThread waiting to get notified at time:1572344222162
waiter1Thread waiting to get notified at time:1572344222162
notifierThread started
waiter1Thread waiter thread got notified at time:1572344223175
waiter1Thread processed: notifierThread Notifier work done
waiterThread waiter thread got notified at time:1572344223177
waiterThread processed: notifierThread Notifier work done

發現最後喚醒的順序顛倒了編程

執行完notify方法,並不會立馬喚醒等待線程,在notify方法後面加一段sleep代碼就能夠看到效果,若是線程執行完notify方法以後sleep 5s,在這段時間內,線程waiterThread1依舊持有monitor,線程waiterThread只能繼續等待;數組

爲何要使用synchronized?

在Java中,synchronized有兩種使用形式,同步方法和同步代碼塊。代碼以下:多線程

public class SynchronizedTest {

    public synchronized void doSth(){
        System.out.println("Hello World");
    }

    public void doSth1(){
        synchronized (SynchronizedTest.class){
            System.out.println("Hello World");
        }
    }
}

咱們先來使用Javap來反編譯以上代碼,結果以下(部分無用信息過濾掉了):併發

public synchronized void doSth();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return

  public void doSth1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #5                  // class com/hollis/SynchronizedTest
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #3                  // String Hello World
        10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: return

反編譯後,咱們能夠看到Java編譯器爲咱們生成的字節碼。在對於doSthdoSth1的處理上稍有不一樣。也就是說。JVM對於同步方法和同步代碼塊的處理方式不一樣。oracle

對於同步方法,JVM採用ACC_SYNCHRONIZED標記符來實現同步。 對於同步代碼塊。JVM採用monitorentermonitorexit兩個指令來實現同步。jvm

關於這部份內容,在JVM規範中也能夠找到相關的描述。ide

同步方法

方法級的同步是隱式的。同步方法的常量池中會有一個ACC_SYNCHRONIZED標誌。當某個線程要訪問某個方法的時候,會檢查是否有ACC_SYNCHRONIZED,若是有設置,則須要先得到監視器鎖,而後開始執行方法,方法執行以後再釋放監視器鎖。這時若是其餘線程來請求執行方法,會由於沒法得到監視器鎖而被阻斷住。值得注意的是,若是在方法執行過程當中,發生了異常,而且方法內部並無處理該異常,那麼在異常被拋到方法外面以前監視器鎖會被自動釋放。

同步代碼塊

同步代碼塊使用monitorentermonitorexit兩個指令實現。 The Java® Virtual Machine Specification 中有關於這兩個指令的介紹:

大體內容以下: 能夠把執行monitorenter指令理解爲加鎖,執行monitorexit理解爲釋放鎖。 每一個對象維護着一個記錄着被鎖次數的計數器。未被鎖定的對象的該計數器爲0,當一個線程得到鎖(執行monitorenter)後,該計數器自增變爲 1 ,當同一個線程再次得到該對象的鎖的時候,計數器再次自增。當同一個線程釋放鎖(執行monitorexit指令)的時候,計數器再自減。當計數器爲0的時候。鎖將被釋放,其餘線程即可以得到鎖。

歸總

同步方法經過ACC_SYNCHRONIZED關鍵字隱式的對方法進行加鎖。當線程要執行的方法被標註上ACC_SYNCHRONIZED時,須要先得到鎖才能執行該方法。

同步代碼塊經過monitorentermonitorexit執行來進行加鎖。當線程執行到monitorenter的時候要先得到所鎖,才能執行後面的方法。當線程執行到monitorexit的時候則要釋放鎖。

每一個對象自身維護這一個被加鎖次數的計數器,當計數器數字爲0時表示能夠被任意線程得到鎖。當計數器不爲0時,只有得到鎖的線程才能再次得到鎖。便可重入鎖。

底層原理

對象頭和內置鎖(ObjectMonitor)

每一個對象分爲三塊區域:對象頭、實例數據和對齊填充

  • 對象頭包含兩部分,第一部分是Mark Word,用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等等,這一部分佔一個字節。第二部分是Klass Pointer(類型指針),是對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例,這部分也佔一個字節。(若是對象是數組類型的,則須要3個字節來存儲對象頭,由於還須要一個字節存儲數組的長度)
  • 實例數據存放的是類屬性數據信息,包括父類的屬性信息,若是是數組的實例部分還包括數組的長度,這部份內存按4字節對齊
  • 填充數據是由於虛擬機要求對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是爲了字節對齊。

級鎖定、重量級鎖定、GC標記、可偏向)下對象的存儲內容以下表所示。
對象頭存儲結構

Synchronized一般被稱爲重量級鎖,可是1.6以後對其進行優化,新增了輕量級鎖和偏向鎖,這裏重點說下重量級鎖,隨後對Synchronized的優化簡單介紹下。

從對象頭的存儲內容能夠看出鎖的狀態都保存在對象頭中,Synchronized也不例外,當其從輕量級鎖膨脹爲重量級鎖時,鎖標識位爲10,其中指針指向的是monitor對象(也稱爲管程或監視器鎖)的起始地址。

關於Synchronized的實如今java對象頭裏較爲簡單,只是改變一下標識位,並將指針指向monitor對象的起始地址,其實現的重點是monitor對象。

在HotSpot虛擬機中,monitor採用ObjectMonitor實現。

內置鎖(ObjectMonitor)

一般所說的對象的內置鎖,是對象頭Mark Word中的重量級鎖指針指向的monitor對象,該對象是在HotSpot底層C++語言編寫的(openjdk裏面看),簡單看一下代碼:

//結構體以下
ObjectMonitor::ObjectMonitor() {  
  _header       = NULL;  
  _count       = 0;  
  _waiters      = 0,  
  _recursions   = 0;       //線程的重入次數
  _object       = NULL;  
  _owner        = NULL;    //標識擁有該monitor的線程
  _WaitSet      = NULL;    //等待線程組成的雙向循環鏈表,_WaitSet是第一個節點
  _WaitSetLock  = 0 ;  
  _Responsible  = NULL ;  
  _succ         = NULL ;  
  _cxq          = NULL ;    //多線程競爭鎖進入時的單向鏈表
  FreeNext      = NULL ;  
  _EntryList    = NULL ;    //_owner從該雙向循環鏈表中喚醒線程結點,_EntryList是第一個節點
  _SpinFreq     = 0 ;  
  _SpinClock    = 0 ;  
  OwnerIsThread = 0 ;  
}

ObjectMonitor隊列之間的關係轉換能夠用下圖表示:
img

既然提到了_waitSet和_EntryList(_cxq隊列後面會說),那就看一下底層的wait和notify方法

wait方法的實現過程:

//1.調用ObjectSynchronizer::wait方法
void ObjectSynchronizer::wait(Handle obj, jlong millis, TRAPS) {
  /*省略 */
  //2.得到Object的monitor對象(即內置鎖)
  ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj());
  DTRACE_MONITOR_WAIT_PROBE(monitor, obj(), THREAD, millis);
  //3.調用monitor的wait方法
  monitor->wait(millis, true, THREAD);
  /*省略*/
}
  //4.在wait方法中調用addWaiter方法
  inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) {
  /*省略*/
  if (_WaitSet == NULL) {
    //_WaitSet爲null,就初始化_waitSet
    _WaitSet = node;
    node->_prev = node;
    node->_next = node;
  } else {
    //不然就尾插
    ObjectWaiter* head = _WaitSet ;
    ObjectWaiter* tail = head->_prev;
    assert(tail->_next == head, "invariant check");
    tail->_next = node;
    head->_prev = node;
    node->_next = head;
    node->_prev = tail;
  }
}
  //5.而後在ObjectMonitor::exit釋放鎖,接着 thread_ParkEvent->park  也就是wait

總結:經過object得到內置鎖(objectMonitor),經過內置鎖將Thread封裝成OjectWaiter對象,而後addWaiter將它插入以_waitSet爲首結點的等待線程鏈表中去,最後釋放鎖。

notify方法的底層實現

//1.調用ObjectSynchronizer::notify方法
    void ObjectSynchronizer::notify(Handle obj, TRAPS) {
    /*省略*/
    //2.調用ObjectSynchronizer::inflate方法
    ObjectSynchronizer::inflate(THREAD, obj())->notify(THREAD);
}
    //3.經過inflate方法獲得ObjectMonitor對象
    ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
    /*省略*/
     if (mark->has_monitor()) {
          ObjectMonitor * inf = mark->monitor() ;
          assert (inf->header()->is_neutral(), "invariant");
          assert (inf->object() == object, "invariant") ;
          assert (ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is inva;lid");
          return inf 
      }
    /*省略*/ 
      }
    //4.調用ObjectMonitor的notify方法
    void ObjectMonitor::notify(TRAPS) {
    /*省略*/
    //5.調用DequeueWaiter方法移出_waiterSet第一個結點
    ObjectWaiter * iterator = DequeueWaiter() ;
    //6.後面省略是將上面DequeueWaiter尾插入_EntrySet的操做
    /**省略*/
  }

總結:經過object得到內置鎖(objectMonitor),調用內置鎖的notify方法,經過_waitset結點移出等待鏈表中的首結點,將它置於_EntrySet中去,等待獲取鎖。注意:notifyAll根據policy不一樣可能移入_EntryList或者_cxq隊列中,此處不詳談。

參考

JVM源碼分析之Object.wait/notify實現

[深刻理解多線程(四)—— Moniter的實現原理]

從jvm源碼看synchronized

併發編程之 wait notify 方法剖析

相關文章
相關標籤/搜索