簡單看看LockSupport和AQS

来源:https://www.cnblogs.com/wyq1995/archive/2020/02/01/12244538.html
-Advertisement-
Play Games

這次我們可以看看併發中鎖的原理,大概會說到AQS,ReentrantLock,ReentrantReadWriteLock以及JDK8中新增的StampedLock,這些都是在java併發中很重要的東西,慢慢看吧! 一.LockSupport工具類 LockSupport工具類是jdk中rt.jar ...


  這次我們可以看看併發中鎖的原理,大概會說到AQS,ReentrantLock,ReentrantReadWriteLock以及JDK8中新增的StampedLock,這些都是在java併發中很重要的東西,慢慢看吧!

 

一.LockSupport工具類

  LockSupport工具類是jdk中rt.jar裡面的,主要作用是掛起和喚醒線程,該類是創建鎖和創建其他同步類的基礎。還有我們要知道,LockSupport這個類是以Unsafe這個類為基礎,講過前面簡單的看了看Unsafe,是不是覺得還是比較熟悉的吧!

  我們先看看LockSupport的park(英文翻譯:停下,坐下)和unpark(英文翻譯:喚醒,啟動)方法,註意,這兩個方法和wait和notify功能很像,但是在這裡我更喜歡叫做授權

  簡單的看一個例子:

package com.example.demo.study;
import java.util.concurrent.locks.LockSupport;

public class Study0130 {

    public static void main(String[] args) {
        System.out.println("main begin");                
        LockSupport.park();       
        System.out.println("main end");
    }
}

 

  我們可以看到我們直接調用park方法的話,當前的線程就阻塞了,不能到後面去了,這裡我們可以說當前線程沒有被LockSupport類授權,沒有許可證,所以到這裡碰到park()這個路口就只能掛了;那麼怎麼樣才能使得當前線程被授權呢?我們就需要unpark()方法進行授權

package com.example.demo.study;
import java.util.concurrent.locks.LockSupport;

public class Study0130 {

    public static void main(String[] args) {
        //這裡就是給當前線程授權了,當前線程可以隨便跑,碰到park都不會掛
        LockSupport.unpark(Thread.currentThread());
        
        System.out.println("main begin");
        LockSupport.park();
        System.out.println("main end");

    }
}

 

  還記得以前的wait和notify的用法麽?一個線程A中調用了wait方法,那麼線程A就掛起了,如果線上程B中調用notify方法,那麼A線程就會被喚醒;這裡的park和unpark方法也可以實現這種,看以下代碼:

package com.example.demo.study;

import java.util.concurrent.locks.LockSupport;

public class Study0130 {

    public static void main(String[] args) {

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread1 start");
                //線程1會阻塞
                LockSupport.park();
                System.out.println("thread1 end");
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread2 start");
                //給線程1授權
                LockSupport.unpark(thread1);
                System.out.println("thread2 end");
            }
        });
        thread1.start();
        thread2.start();
    }
}

 

  我們打開LockSupport的park和unpark方法可以發現,就是調用的Unsafe實現的,可惜看不到源碼...

 

  假如我們調用park方法使得線程阻塞太久了也不是我們想看到的,我們還可以使用parkNanos設置阻塞時間,當時間到了,就會自動返回:

 

  最後說一下,還可以調用park方法的時候傳進去一個對象,比如LockSupport.park(this);這樣使用可以使用jstack pid命令查看堆棧信息的時候,可以看到是那個類被阻塞了!

   到此為止,應該就是LockSupport的常用方法了!

 

二.認識AQS

  AQS全稱是AbstractQueuedSynchronizer,叫做抽象同步隊列,用於實現各種同步組件,比如併發包中的鎖就是用這個實現的,把這個弄清楚了,那些鎖的機制就差不多懂了!

  那麼所謂的AQS到底是什麼呢?其實就是一個有順序的雙向鏈表(或者叫做FIFO雙向隊列,一樣的意思),在這個雙向鏈表中,每一個節點中都可以存放一個線程,節點的所有屬性如下圖所示,我們隨便說幾個;

  prev表示指向前一個節點,next指向後一個節點,thread表示當前節點存儲的一個線程,SHARED表示當前節點存儲的線程是由於獲取共用資源是被阻塞了才被丟到鏈表中的;EXCLUSIVE表示當前節點存儲的線程是由於獲取獨占資源阻塞才被丟到鏈表中來的;

  waitStatus表示當前節點存儲的線程的狀態,可能的狀態有以下幾種:(1)CANCELLED =  1;  表示線程被取消了  (2)SIGNAL    = -1; 表示線程需要喚醒 (3)CONDITION = -2;表示線程在鏈表中等待 (4)PROPAGATE = -3;表示線程釋放共用資源時需要通知其他節點;

  註意,這裡其實還有一個狀態,就是waitStatus為0,表示當前節點是初始狀態,所以可以知道當waitStatus大於0的時候是無效狀態,小於零才是有效狀態

  

  這個Node類是AQS的一個內部類,那麼怎麼通過AQS來訪問這個鏈表呢?下麵我們再來看看AQS有哪些屬性可以幫助我們訪問這個雙向鏈表;

//欄位
//指向鏈表的頭節點
private transient volatile Node head;
//指向鏈表的尾節點
private transient volatile Node tail;
//狀態信息,這個欄位在每個實現類中表達的意思都不一樣,比如在ReentrantLock中表示可重入的次數,
//在Semaphore中表示可用信號的個數等等用法
private volatile int state;

//獲取Unsafe對象,前面用過的,還記得說過為什麼可以使用getUnsafe的方式獲取對象,而我們自己的類中卻不能用這種方式
private static final Unsafe unsafe = Unsafe.getUnsafe();

//下麵的這幾個屬性就是獲取AQS類中的欄位的偏移量,在前幾篇的博客已經說過了這偏移量有什麼用
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;

//方法
//這幾個方法都是嘗試獲取鎖
public final void acquire(int arg) {}//獨占方式
protected boolean tryAcquire(int arg) {}
public final void acquireShared(int arg) {}//共用方式
public final void acquireInterruptibly(int arg){}//獨占方式
public final void acquireSharedInterruptibly(int arg){}//共用方式
//這幾個方法都是試圖釋放鎖 public final boolean release(int arg) {}//獨占方式 public final boolean releaseShared(int arg) {}//共用方式 protected boolean tryRelease(int arg) {} protected boolean tryReleaseShared(int arg) {}

 

  在AQS中對線程的同步主要的是操作state,對state的操作方式分為兩種,獨占方式和共用方式,至於兩種方式各自的獲取鎖和釋放鎖的方法在上面已經標識出來了!

  這裡稍微提一下什麼叫做鎖啊?在java多線程中可以把一個對象當做一個鎖,為什麼呢?我們可以簡單看看一個普通的java對象(不是數組)在java堆中有哪些組成部分:

 

   一個java對象是由對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)三部分組成,實例數據和對齊填充可以看做是一類,因為對齊填充就是起到填充空白的作用,因為java對象的位元組數必須是8的倍數(對象頭肯定是8的倍數,這裡其實就是填充實例數據成8的倍數即可),所以對齊填充可能有也可能沒有;

  對象頭一般有兩部分組成(數組的話還有一個部分,即數組長度),如下所示:

    第一部分:用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啟壓縮指針)中分別為32bit和64bit,官方稱它為“MarkWord”。

    第二部分:對象頭的另外一部分是klass類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。

  我們可以看作一個對象就是一個鎖,如果一個線程獲取了某個鎖,那麼在這個鎖對象的對象頭的markword中存了某個線程的編號,也就表示該線程持有了該鎖!

  上面說了這麼多,我們大概就知道了所謂的AQS就是如下圖所示這樣,維護了一個鏈表,每次只有頭部的這個節點中的線程是運行的,當頭部的線程由於某些原因阻塞了或中斷了,下一個線程才會嘗試獲取資源,重覆如此

  然後我們再來說說一個線程以獨占方式獲取資源或者是共用方式獲取資源;

 

三.獨占方式

  當一個線程要以獨占方式獲取該資源,說得直白一點就是實現一個獨占鎖,類似synchorized代碼塊一樣,對共用資源的操作都在這個代碼塊中,一個線程只有先獲取這個鎖才能進入到代碼塊中操作共用資源,其他線程嘗試獲取鎖的時候,和這個鎖中對象頭的線程編號比較如果不一樣,那就只能將這個線程放到鏈表中存起來,然後該線程掛起來,等條件滿足之後再喚醒,就是使用LockSupport的park和unpark方法實現的。

  就以ReentrantLock為例,一個線程獲取到了ReentrantLock的鎖之後,在AQS中就會首先使用CAS將state從0變為1,然後設置當前鎖為本線程所持有;如果當前線程繼續嘗試獲取鎖,那麼只會將state從1變為2,其他的沒啥變化,這也叫做可重入次數;當如果其他線程去嘗試獲取鎖的時候,那麼發現鎖對象的對象頭中不是自己線程編號,於是就丟進了阻塞隊列中掛起;

  1.當線程通過acquire(int arg)獲取獨占資源時: 

public final void acquire(int arg) {
     //1.tryAcquire方法沒有實現,這個方法主要是留給具體子類去實現,通過具體場景去用CAS修改state的值,修改成功返回true,否則false
     //2.如果修改state的值失敗,就會到第二個條件這裡,這裡會將當前線程封裝成一個Node.EXCLUSIVE類型的節點,然後存到鏈表尾端,最後在acquireQueued方法內部會調用
      LockSupport.park(this);方法阻塞線程
//3.調用selfInterrupt方法中斷當前的線程,為什麼要這樣呢?因為一個線程在阻塞隊列中等待,這時通過某種方式把它中斷了,不會立即看到效果的,
   //只會在這個線程獲取資源後再調用selfInterrupt方法將中斷補上
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } //中斷當前線程 static void selfInterrupt() { Thread.currentThread().interrupt(); }

 

  2.當線程通過release(int arg)釋放獨占資源時:

public final boolean release(int arg) {
    //tryRelease方法沒有實現,子類根據具體場景是實現,其實就是修改state的值
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
        //這個方法在下麵,裡面會調用LockSupport.unpark(s.thread)方法激活阻塞隊列中的一個節點的線程,而這個激活的線程會通過tryAcquire嘗試當前的state是否滿足自己的需要
     //滿足條件的話就運行,不滿足的話還是會掛起 unparkSuccessor(h); return true; } return false; }

  通過簡單的看了這獲取資源和釋放資源我們可以看到底層還是使用的Unsafe的park和unpark方法,還有就是tryAcquire()方法和tryRelease()方法需要在具體的子類自己實現,在其中就是對AQS中state的修改,子類還需要定義state這個狀態值的增減是什麼含義;

  例如ReentrantLock繼承自AQS的實現中,state為0表示鎖空閑,為1表示鎖被占用,在重寫tryAcquire()方法的時候,需要用CAS將state的值從0改為1,並且設置當前鎖的持有者就是當前線程;而重寫tryRelease()方法的時候,就需要用CAS將state的值從1改為0,然後設置當前鎖的持有者為null

 

四.共用方式

  知道了獨占方式之後,共用方式就簡單了,什麼叫做共用?同一時間可以有多個線程獲取資源,這就叫做共用!!!

  一個線程嘗試去獲取資源成功後,此時另外一個線程也可以直接用CAS去嘗試獲取資源,成功的話就修改,失敗的話就丟進鏈表中存起來;例如Semaphore信號量,當一個線程通過acquire()方法獲取信號量的時候,信號量滿足條件就通過CAS去獲取,不滿足就將線程丟到鏈表裡面;

  共用方式和前面的獨占方式其實很像,我們也來簡單的看一看:

  1.當線程通過acquireShared(int arg)獲取共用資源時:

 public final void acquireShared(int arg) {
    //tryAcquireShared方法也是沒有實現,留給具體子類會根據實際情況實現,會設置state的值,設置成功就直接返回
    //設置失敗的話就進入到doAcquireShared方法中,這個方法里會將當前線程封裝為Node.SHARED類型的節點,然後放到阻塞隊列的最後面
   //使用LockSupport.park(this)方法掛起自己
if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }

 

  2.當線程通過releaseShared(int arg)釋放共用資源時:

public final boolean releaseShared(int arg) {
    //tryReleaseShared方法由子類實現,修改state的值,嘗試釋放資源
    //釋放資源成功的話,然後使用LockSupport.unpark(thread)去喚醒阻塞隊列中的一個線程
    //激活的線程會使用tryReleaseShared查看當前state的值是否符合自己的需要,滿足則激活,向下運行,否則還是被放在AQS阻塞隊列中掛起
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

  例如讀寫鎖ReentrantReadWriteLock就是繼承自AQS的實現,由於state是int類型的,32位,高16位表示獲取讀鎖的次數,所以讀鎖的tryAcquireShared方法實現中,首先檢查寫鎖是否被其他線程持有,是則返回false,否則就用CAS將state的高16位+1;在讀鎖的tryReleaseShared的實現中,內部使用CAS將state的高16位減一,成功的話就返回true,失敗的話返回false

 


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

-Advertisement-
Play Games
更多相關文章
  • 本文將介紹以下內容:Windows下安裝scala運行環境,安裝編譯工具並簡單配置,實現著名的“Hello,World"。 一,Windows下安裝scala運行環境 1.配置jdk,因為scala的運行需要依靠jvm虛擬機,所以在使用scala時需要有java環境 2.官網下載scala包,點擊這 ...
  • 2020年,給自己定一個新目標————開始寫技術博客,將之前所學的內容重新複習並整理成一系列的文章,一來可以讓自己對這些基礎知識更加熟悉,二來方便於以後的複習查閱。 以前自己都是以筆記的形式將知識點記錄在有道雲筆記中,這樣可能造成由於時間緊或者懶,只是記錄了筆記,沒有去深刻的理解。所以乘著這次全面復 ...
  • 前言 最近因為公司需要,需要瞭解下java探針,在網上找資料,發現資料還是有很多的,但是例子太少,有的直接把公司代碼粘貼出來,太複雜了,有的又特別簡單不是我想要的例子, 我想要這樣的一個例子: jvm在運行,我想動態修改一個類,jvm在不用重啟的情況下, 自動載入新的類定義. 動態修改類定義,聽著感 ...
  • web應用常見的資源存方式 在WEB-INF下新建lib,存放要使用的jar包 在WEB-INF下新建jsp文件夾,存放jsp文件。首頁除外,首頁就放到web下。 在web下,或WEB-INF下,新建css、js、image文件夾,存放相關文件。 WEB-INF目錄是web應用的安全目錄,裡面的資源 ...
  • Spring官方支持的服務的渲染模板中,並不包含jsp。而是Thymeleaf和Freemarker等,而Thymeleaf與SpringMVC的視圖技術,及SpringBoot的自動化配置集成非常完美,幾乎沒有任何成本,你只用關註Thymeleaf的語法即可。 Thymeleaf的特點 動靜結合: ...
  • 繼承關係用不好女人也可以是男人,複合關係用不好「狗中有人,人中有狗」的事情也會有。 ...
  • 第一種註冊方法 from flask import Flask app = Flask(__name__) @app.route("/hello") # 第一種註冊方法 def hello(): return "hello python!!!" if __name__ == "__main__": ...
  • 我們在進行APP開發的時候都會遇到一個文件:AndroidManifest.xml。從剛開始進行Android開發,到現在已經過去了幾個月,還是對這個文件一知半解,只知道它是配置用的。但是這文件里的東西具體有什麼用,該怎麼用一直都沒有理解。藉著做項目的機會,仔細研究一下這個文件。 研究Android ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...