面試官:為什麼 wait/notify 必須與 synchronized 一起使用??

来源:https://www.cnblogs.com/javastack/archive/2023/01/03/17021997.html
-Advertisement-
Play Games

來源:blog.csdn.net/randompeople/article/details/114917087 為什麼 java wait/notify 必須與 synchronized 一起使用 這個問題就是書本上沒怎麼講解,就是告訴我們這樣處理,但沒有解釋為什麼這麼處理?我也是基於這樣的困惑去了 ...


來源:blog.csdn.net/randompeople/article/details/114917087

為什麼 java wait/notify 必須與 synchronized 一起使用

這個問題就是書本上沒怎麼講解,就是告訴我們這樣處理,但沒有解釋為什麼這麼處理?我也是基於這樣的困惑去瞭解原因。

synchronized是什麼

Java中提供了兩種實現同步的基礎語義:synchronized方法和synchronized塊, 看個demo:

public class SyncTest {

   \\ 1、synchronized方法

 public synchronized void syncMethod(){
        System.out.println("hello method");
    }

    \\ 2、synchronized塊

    public void syncBlock(){
        synchronized (this){
            System.out.println("hello block");
        }
    }
}

具體還要區分:

  • 修飾實例方法,作用於當前實例加鎖,進入同步代碼前要獲得當前實例的鎖。不同實例對象的訪問,是不會形成鎖的。
  • 修飾靜態方法,作用於當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖
  • 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。

它具有的特性:

  • 原子性
  • 可見性
  • 有序性
  • 可重入性

synchronized如何實現鎖

這樣看來synchronized實現的鎖是基於class對象來實現的,我們來看看如何實現的,它其實是跟class對象的對象頭一起起作用的,對象在記憶體中的佈局分為三塊區域:對象頭、實例數據和對齊填充。

其中對象頭中有一個Mark Word,這裡主要存儲對象的hashCode、鎖信息或分代年齡或GC標誌等信息,把可能的情況列出來大概如下:

其中synchronized就與鎖標誌位一起作用實現鎖。主要分析一下重量級鎖也就是通常說synchronized的對象鎖,鎖標識位為10,其中指針指向的是monitor對象(也稱為管程或監視器鎖)的起始地址。

每個對象都存在著一個 monitor 與之關聯,對象與其 monitor 之間的關係有存在多種實現方式,如monitor可以與對象一起創建銷毀或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有後,它便處於鎖定狀態。

在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的,其主要數據結構如下(位於HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的):

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //記錄個數
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

上面有2個欄位很重要:

  • _WaitSet隊列處於wait狀態的線程,會被加入到_WaitSet。
  • _EntryList隊列處於等待鎖block狀態的線程,會被加入到該列表。
  • _owner_owner指向持有ObjectMonitor對象的線程

我們來模擬一下進入鎖的流程:

1、當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合

2、當線程獲取到對象的monitor 後進入 _Owner 區域,並把monitor中的owner變數設置為當前線程同時monitor中的計數器count加1

3、若線程調用 wait() 方法,將釋放當前持有的monitor,owner變數恢復為null,count自減1,同時該線程進入 WaitSet集合中等待被喚醒。

4、若當前線程執行完畢也將釋放monitor(鎖)並複位變數的值,以便其他線程進入獲取monitor(鎖)

wait/notify

這兩個是Java對象都有的屬性,表示這個對象的一個等待和通知機制。

推薦一個開源免費的 Spring Boot 最全教程:

https://github.com/javastacks/spring-boot-best-practice

不用synchronized 會怎麼樣

參考其他博客,我們來看看不使用synchronized會怎麼樣,假設有2個線程,分別做2件事情,T1線程代碼邏輯:

while(!條件滿足) // line 1
{
    obj.wait(); // line 2
}
doSomething();

T2線程的代碼邏輯:

更改條件為滿足; // line 1
obj.notify(); // line 2

多線程環境下沒有synchronized,沒有鎖的情況下可能會出現如下執行順序情況:

  • T1 line1 滿足while 條件
  • T2 line1 執行
  • T2 line2 執行,notify發出去了
  • T1 line2 執行,wait再執行

這樣的執行順序導致了notify通知發出去了,但沒有用,已經wait是在之後執行,所以有人說沒有保證原子性,就是line1 和line2 是一起執行結束,這個也被稱作lost wake up問題。解決方法就是可以利用synchronized來加鎖,於是有人就寫了這樣的代碼:

synchronized(lock)
{
    while(!條件滿足)
    {
        obj.wait();
    }
    doSomething();
}
synchronized(lock)
{
   更改條件為滿足;
   obj.notify();
}

這樣靠鎖來做達到目的。但這代碼會造成死鎖,因為先T1 wait(),再T2 notify();而問題在於T1持有lock後block住了,T2一直無法獲得lock,從而永無可能notify()並將T1的block狀態解除,就與T1形成了死鎖。

所以JVM在實現wait()方法時,一定需要先隱式的釋放lock,再block,並且被notify()後從wait()方法返回前,隱式的重新獲得了lock後才能繼續user code的執行。要做到這點,就需要提供lock引用給obj.wait()方法,否則obj.wait()不知道該隱形釋放哪個lock,於是調整之後的結果如下:

synchronized(lock)
{
    while(!條件滿足)
    {
        obj.wait(lock);
        // obj.wait(lock)偽實現
        //   [1] unlock(lock)
        //   [2] block住自己,等待notify()
        //   [3] 已被notify(),重新lock(lock)
        //   [4] obj.wait(lock)方法成功返回
    }
    doSomething();
}

[最終形態] 把lock和obj合一

其它線程API如PThread提供wait()函數的簽名是類似cond_wait(obj, lock)的,因為同一個lock可以管多個obj條件隊列。而Java內置的鎖與條件隊列的關係是1:1,所以就直接把obj當成lock來用了。因此此處就不需要額外提供lock,而直接使用obj即可,代碼也更簡潔:

synchronized(obj)
{
    while(!條件滿足)
    {
        obj.wait();
    }
    doSomething();
}
synchronized(lock)
{
   更改條件為滿足;
   obj.notify();
}

lost wake up

wait/notify 如果不跟synchronized結合就會造成lost wake up,難以喚醒wait的線程,所以單獨使用會有問題。

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 大多數開發者可能都用過 Postman,根據其官網的介紹:Postman 是一個用於構建和使用 API 的 API 平臺,簡化了 API 生命周期的每個步驟,提供更便捷的團隊協作,因此可以更快地創建更好的 API。這裡的 API,除了我們常用的 HTTP API 之外,還包括 Websocket(B ...
  • 一、前言 redis在我們企業級開發中是很常見的,但是單個redis不能保證我們的穩定使用,所以我們要建立一個集群。 redis有兩種高可用的方案: High availability with Redis Sentinel(哨兵) Scaling with Redis Cluster(分片集群) ...
  • 有問必答 最近有好多讀者私信我,為什麼選擇GoFrame做電商項目的開發? 原因很簡單: 因為我司是用GoFrame做電商業務開發的,而且我司同事基本都是PHP轉Go的。GoFrame可以說是非常適合PHPer轉Gopher的開發框架。 在入職我司之前,我有使用Gin和go-micro框架,目前也正 ...
  • JavaSE:基礎語法 註釋 Java中的註釋有三種: 單行註釋:只能註釋當前行,以//開始,直到行結束 ​ //輸出HelloWorld! 多行註釋:註釋一段文字,以/ * 開始以 * / 結束! ​ /* 這是我們Java程式的主入口, main方法也是程式的主線程。 */ 文檔註釋:用於生產A ...
  • JZ78 把二叉樹列印成多行 題目 給定一個節點數為 n 二叉樹,要求從上到下按層列印二叉樹的 val 值,同一層結點從左至右輸出,每一層輸出一行, 將輸出的結果存放到一個二維數組中返回。 例如:給定的二叉樹是{1,2,3,#,#,4,5} [ [1], [2,3], [4,5] ] 方法 非遞歸層 ...
  • 1. 協議的作用 TCP/IP 中消息傳輸基於流的方式,沒有邊界 協議的目的就是劃定消息的邊界,制定通信雙方要共同遵守的通信規則 2. Redis 協議 如果我們要向 Redis 伺服器發送一條 set name Nyima 的指令,需要遵守如下協議 // 該指令一共有3部分,每條指令之後都要添加回 ...
  • JVM是運行在操作系統之上的,它與硬體沒有直接的交互。先說一下JVM的記憶體區域,當函數開始運行時,JVM拿到自己的記憶體將自己的記憶體區域進行了分割,分為五塊區域:線程共用的有堆、方法區,線程私有的有java棧、本地方法棧、程式計數器。 ...
  • 公司直招 急聘崗位 ♦telegram:@xiaobai04 @HRdajisi♦釘釘:馬新宇 零九四五五五壹零壹六吧♦Skype: live:.cid.d850fdc83f05e44a♦郵箱:[email protected] ♦技術部:薪資面議 薪資範圍25-100k 績效:MA ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...