簡單看看LockSupport和AQS

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

 

一.LockSupport工具類數組

  LockSupport工具類是jdk中rt.jar裏面的,主要做用是掛起和喚醒線程,該類是建立鎖和建立其餘同步類的基礎。還有咱們要知道,LockSupport這個類是以Unsafe這個類爲基礎,講過前面簡單的看了看Unsafe,是否是以爲仍是比較熟悉的吧!多線程

  咱們先看看LockSupport的park(英文翻譯:停下,坐下)和unpark(英文翻譯:喚醒,啓動)方法,注意,這兩個方法和wait和notify功能很像,可是在這裏我更喜歡叫作受權併發

  簡單的看一個例子:ide

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方法也能夠實現這種,看如下代碼:ui

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實現的,惋惜看不到源碼...this

 

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

 

  最後說一下,還能夠調用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

相關文章
相關標籤/搜索