溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

JAVA中如何實(shí)現(xiàn)AQS

發(fā)布時(shí)間:2021-10-21 11:06:05 來(lái)源:億速云 閱讀:173 作者:小新 欄目:編程語(yǔ)言

小編給大家分享一下JAVA中如何實(shí)現(xiàn)AQS,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

AbstractQueuedSynchronizer是JUC的核心框架,其設(shè)計(jì)非常精妙。 使用了Java的模板方法模式。 首先試圖還原一下其使用場(chǎng)景:
對(duì)于排他鎖,在同一時(shí)刻,N個(gè)線(xiàn)程只有1個(gè)線(xiàn)程能獲取到鎖;其他沒(méi)有獲取到鎖的線(xiàn)程被掛起放置在隊(duì)列中,待獲取鎖的線(xiàn)程釋放鎖后,再喚醒隊(duì)列中的線(xiàn)程。

線(xiàn)程的掛起是獲取鎖失敗時(shí)調(diào)用Unsafe.park()方法;線(xiàn)程的喚醒是由其他線(xiàn)程釋放鎖時(shí)調(diào)用Unsafe.unpark()實(shí)現(xiàn)。
由于獲取鎖,執(zhí)行鎖內(nèi)代碼邏輯,釋放鎖整個(gè)流程可能只需要耗費(fèi)幾毫秒,所以很難對(duì)鎖的爭(zhēng)用有一個(gè)直觀的感受。下面以3個(gè)線(xiàn)程來(lái)簡(jiǎn)單模擬一下排他鎖的機(jī)制。

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.LockSupport;

public class AQSDemo {

    private static final Unsafe unsafe = getUnsafe();
    private static final long stateOffset;
    private static Unsafe getUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe)field.get(null);

        } catch (Exception e) {
        }
        return null;
    }

    static{
        try{
            stateOffset = unsafe.objectFieldOffset
                    (AQSDemo.class.getDeclaredField("state"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int state;

    private List<Thread> threads = new ArrayList<>();

    public void lock(){

        if(!unsafe.compareAndSwapInt(state,stateOffset,0,1)){
           // 有問(wèn)題,非線(xiàn)程安全;只作演示使用
            threads.add(Thread.currentThread());
            LockSupport.park();
            Thread.interrupted();
        }
    }

    public void unlock(){
        state = 0;
        if(!threads.isEmpty()){
            Thread first = threads.remove(0);
            LockSupport.unpark(first);
        }
    }

    static class MyThread extends Thread{

        private AQSDemo lock;

        public MyThread(AQSDemo lock){
            this.lock = lock;
        }
        public void run(){
            try{
                lock.lock();
                System.out.println("run ");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        AQSDemo lock = new AQSDemo();
        MyThread a1 = new MyThread(lock);
        MyThread a2 = new MyThread(lock);
        MyThread a3 = new MyThread(lock);
        a1.start();
        a2.start();
        a3.start();
    }

}

上面的代碼,使用park和unpark簡(jiǎn)單模擬了排他鎖的工作原理。使用ArrayList屏蔽了鏈表多線(xiàn)程環(huán)境下鏈表的構(gòu)造細(xì)節(jié), 該代碼實(shí)際上在多線(xiàn)程環(huán)境中使用是有問(wèn)題的,發(fā)現(xiàn)了么?

通過(guò)上面的代碼,能理解到多線(xiàn)程環(huán)境下,鏈表為什么能比ArrayList好使。

理解AQS, 其核心在于理解statehead, tail三個(gè)變量。換句話(huà)說(shuō),理解AQS, 只需理解狀態(tài)鏈表實(shí)現(xiàn)的隊(duì)列 這兩樣?xùn)|西。其使用方式就是,如果更新?tīng)顟B(tài)不成功,就把線(xiàn)程掛起,丟到隊(duì)列中;其他線(xiàn)程使用完畢后,從隊(duì)列中喚醒一個(gè)線(xiàn)程執(zhí)行。 如果排隊(duì)的線(xiàn)程數(shù)量過(guò)多,那么該誰(shuí)首先獲得鎖就有講究,不能暗箱操作,所以有公平和非公平兩種策略。

越來(lái)越能理解 “編程功底,細(xì)節(jié)是魔鬼”,理解了上面的使用方式,只相當(dāng)于理解了需求。那么實(shí)現(xiàn)上有那些細(xì)節(jié)呢? 我們通過(guò)問(wèn)答的方式來(lái)闡明。

問(wèn)題1: state變量為什么要用volatile關(guān)鍵詞修飾?

volatile是synchronized的輕量版本,在特定的場(chǎng)景下具備鎖的特點(diǎn)變量更新的值不依賴(lài)于當(dāng)前值, 比如setState()方法。 當(dāng)volatile的場(chǎng)景不滿(mǎn)足時(shí),使用Unsafe.compareAndSwap即可。

問(wèn)題2: 鏈表是如何保證多線(xiàn)程環(huán)境下的鏈?zhǔn)浇Y(jié)構(gòu)?

首先我們看鏈表是一個(gè)雙向鏈表,我們看鏈表呈現(xiàn)的幾個(gè)狀態(tài):

1. 空鏈表
    (未初始化)
head  -- null
tail  -- null

    or
   (初始化后)
head  -- Empty Node
tail  -- Empty Node

2. 只有一個(gè)元素的鏈表

head  -- Empty Node <->  Thread Node  -- tail

也就是說(shuō),當(dāng)鏈表的不為空時(shí), 鏈表中填充者一個(gè)占位節(jié)點(diǎn)。

學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu),把插入刪除兩個(gè)操作弄明白,基本就明白這個(gè)數(shù)據(jù)結(jié)構(gòu)了。我們先看插入操作enq():

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

首先一個(gè)無(wú)限循環(huán)。 假如這個(gè)鏈表沒(méi)有初始化,那么這個(gè)鏈表會(huì)通過(guò)循環(huán)的結(jié)構(gòu)插入2個(gè)節(jié)點(diǎn)。 由于多線(xiàn)程環(huán)境下, compareAndSet會(huì)存在失敗,所以通過(guò)循環(huán)保證了失敗重試。 為了保證同步,要么依賴(lài)鎖,要么通過(guò)CPU的cas。 這里是實(shí)現(xiàn)同步器,只能依賴(lài)cas。 這種編程結(jié)構(gòu),看AtomicInteger,會(huì)特別熟悉。

接下來(lái)看鏈表的刪除操作。當(dāng)線(xiàn)程釋放鎖調(diào)用release()方法時(shí),AQS會(huì)按線(xiàn)程進(jìn)入隊(duì)列的順序喚醒地一個(gè)符合條件的線(xiàn)程,這就是FIFO的體現(xiàn)。代碼如下:

  public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

這里unparkSuccessor()里面的waitStatus我們先忽略。這樣的話(huà),線(xiàn)程會(huì)從阻塞的后面繼續(xù)執(zhí)行,從parkAndCheckInterrupt()方法中出來(lái)。

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

由于喚醒的順序是FIFO, 所以通常p==head條件是滿(mǎn)足的。如果獲取到鎖,就把當(dāng)前節(jié)點(diǎn)作為鏈表的head節(jié)點(diǎn):setHead(node), 原h(huán)ead節(jié)點(diǎn)從鏈表中斷開(kāi),讓GC回收p.next=null。 也就是說(shuō),鏈表的刪除是從頭開(kāi)始刪除,以實(shí)現(xiàn)FIFO的目標(biāo)。

到這里,AQS的鏈表操作就弄清楚了。

以上是“JAVA中如何實(shí)現(xiàn)AQS”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI