J.U.C之AQS

基于jdk8

越是核心的東西越是要反復(fù)看,本文篇幅較長,希望各位細細品讀掖蛤,來回多讀幾遍理解下。

AQS簡介

??java的內(nèi)置鎖一直都是備受爭議的井厌,在JDK6之前蚓庭,synchronized這個重量級鎖其性能一直都是較為低下,雖然在JDK6后旗笔,進行大量的鎖優(yōu)化策略,但是與Lock相比synchronized還是存在一些缺陷的:雖然synchronized提供了便捷性的隱式獲取鎖釋放鎖機制(基于JVM機制)彪置,但是它卻缺少了獲取鎖與釋放鎖的可操作性拄踪,可中斷蝇恶、超時獲取鎖,且它為獨占式在高并發(fā)場景下性能大打折扣惶桐。

??在介紹Lock之前撮弧,我們需要先熟悉一個非常重要的組件,掌握了該組件JUC包下面很多問題都不在是問題了姚糊。該組件就是AQS贿衍。

??AQS:AbstractQueuedSynchronizer,即隊列同步器救恨。它是構(gòu)建鎖或者其他同步組件的基礎(chǔ)框架(如ReentrantLock贸辈、ReentrantReadWriteLock、Semaphore等)肠槽,JUC并發(fā)包的作者(Doug Lea)期望它能夠成為實現(xiàn)大部分同步需求的基礎(chǔ)擎淤。它是JUC并發(fā)包中的核心基礎(chǔ)組件。

??AQS解決了子類實現(xiàn)同步器時涉及當?shù)拇罅考毠?jié)問題秸仙,例如獲取同步狀態(tài)嘴拢、FIFO同步隊列〖偶停基于AQS來構(gòu)建同步器可以帶來很多好處席吴。它不僅能夠極大地減少實現(xiàn)工作,而且也不必處理在多個位置上發(fā)生的競爭問題捞蛋。

??在基于AQS構(gòu)建的同步器中孝冒,只能在一個時刻發(fā)生阻塞,從而降低上下文切換的開銷拟杉,提高了吞吐量庄涡。同時在設(shè)計AQS時充分考慮了可伸縮行,因此J.U.C中所有基于AQS構(gòu)建的同步器均可以獲得這個優(yōu)勢捣域。

??AQS的主要使用方式是繼承啼染,子類通過繼承同步器并實現(xiàn)它的抽象方法來管理同步狀態(tài)宴合。

??AQS使用一個int類型的成員變量state來表示同步狀態(tài),當state>0時表示已經(jīng)獲取了鎖迹鹅,當state = 0時表示釋放了鎖卦洽。它提供了三個方法(getState()、setState(int newState)斜棚、compareAndSetState(int expect,int update))來對同步狀態(tài)state進行操作阀蒂,當然AQS可以確保對state的操作是安全的。

??AQS通過內(nèi)置的FIFO同步隊列來完成資源獲取線程的排隊工作弟蚀,如果當前線程獲取同步狀態(tài)失斣橄肌(鎖)時,AQS則會將當前線程以及等待狀態(tài)等信息構(gòu)造成一個節(jié)點(Node)并將其加入同步隊列义钉,同時會阻塞當前線程昧绣,當同步狀態(tài)釋放時,則會把節(jié)點中的線程喚醒捶闸,使其再次嘗試獲取同步狀態(tài)夜畴。

AQS主要提供了如下一些方法:

  • getState():返回同步狀態(tài)的當前值;
  • setState(int newState):設(shè)置當前同步狀態(tài)删壮;
  • compareAndSetState(int expect, int update):使用CAS設(shè)置當前狀態(tài)贪绘,該方法能夠保證狀態(tài)設(shè)置的原子性;
  • tryAcquire(int arg):獨占式獲取同步狀態(tài)央碟,獲取同步狀態(tài)成功后税灌,其他線程需要等待該線程釋放同步狀態(tài)才能獲取同步狀態(tài)
  • tryRelease(int arg):獨占式釋放同步狀態(tài);
  • tryAcquireShared(int arg):共享式獲取同步狀態(tài)亿虽,返回值大于等于0則表示獲取成功菱涤,否則獲取失敗经柴;
  • tryReleaseShared(int arg):共享式釋放同步狀態(tài)狸窘;
  • isHeldExclusively():當前同步器是否在獨占式模式下被線程占用,一般該方法表示是否被當前線程所獨占坯认;
  • acquire(int arg):獨占式獲取同步狀態(tài)翻擒,如果當前線程獲取同步狀態(tài)成功,則由該方法返回牛哺,否則陋气,將會進入同步隊列等待,該方法將會調(diào)用可重寫的tryAcquire(int arg)方法引润;
  • acquireInterruptibly(int arg):與acquire(int arg)相同巩趁,但是該方法響應(yīng)中斷,當前線程為獲取到同步狀態(tài)而進入到同步隊列中,如果當前線程被中斷议慰,則該方法會拋出InterruptedException異常并返回蠢古;
  • tryAcquireNanos(int arg,long nanos):超時獲取同步狀態(tài),如果當前線程在nanos時間內(nèi)沒有獲取到同步狀態(tài)别凹,那么將會返回false草讶,已經(jīng)獲取則返回true;
  • acquireShared(int arg):共享式獲取同步狀態(tài)炉菲,如果當前線程未獲取到同步狀態(tài)堕战,將會進入同步隊列等待,與獨占式的主要區(qū)別是在同一時刻可以有多個線程獲取到同步狀態(tài)拍霜;
  • acquireSharedInterruptibly(int arg):共享式獲取同步狀態(tài)嘱丢,響應(yīng)中斷;
  • tryAcquireSharedNanos(int arg, long nanosTimeout):共享式獲取同步狀態(tài)祠饺,增加超時限制越驻;
  • release(int arg):獨占式釋放同步狀態(tài),該方法會在釋放同步狀態(tài)之后吠裆,將同步隊列中第一個節(jié)點包含的線程喚醒伐谈;
  • releaseShared(int arg):共享式釋放同步狀態(tài);

CLH同步隊列

??CLH同步隊列是一個FIFO雙向隊列试疙,AQS依賴它來完成同步狀態(tài)的管理,當前線程如果獲取同步狀態(tài)失敗時抠蚣,AQS則會將當前線程已經(jīng)等待狀態(tài)等信息構(gòu)造成一個節(jié)點(Node)并將其加入到CLH同步隊列祝旷,同時會阻塞當前線程,當同步狀態(tài)釋放時嘶窄,會把首節(jié)點喚醒(公平鎖)怀跛,使其再次嘗試獲取同步狀態(tài)。
??在CLH同步隊列中柄冲,一個節(jié)點表示一個線程吻谋,它保存著線程的引用(thread)、狀態(tài)(waitStatus)现横、前驅(qū)節(jié)點(prev)漓拾、后繼節(jié)點(next),其定義如下:

static final class Node {
    /** 共享 */
    static final Node SHARED = new Node();
    /** 獨占 */
    static final Node EXCLUSIVE = null;
    /**
     * 因為超時或者中斷戒祠,節(jié)點會被設(shè)置為取消狀態(tài)骇两,被取消的節(jié)點時不會參與到競爭中的,他會一直保持取消狀態(tài)不會轉(zhuǎn)變?yōu)槠渌麪顟B(tài)姜盈;
     */
    static final int CANCELLED =  1;
    /**
     * 后繼節(jié)點的線程處于等待狀態(tài)低千,而當前節(jié)點的線程如果釋放了同步狀態(tài)或者被取消,將會通知后繼節(jié)點馏颂,使后繼節(jié)點的線程得以運行
     */
    static final int SIGNAL    = -1;
    /**
     * 節(jié)點在等待隊列中示血,節(jié)點線程等待在Condition上棋傍,當其他線程對Condition調(diào)用了signal()后,該節(jié)點將會從等待隊列中轉(zhuǎn)移到同步隊列中难审,加入到同步狀態(tài)的獲取中
     */
    static final int CONDITION = -2;
    /**
     * 表示下一次共享式同步狀態(tài)獲取將會無條件地傳播下去
     */
    static final int PROPAGATE = -3;
    /** 等待狀態(tài) */
    volatile int waitStatus;
    /** 前驅(qū)節(jié)點 */
    volatile Node prev;
    /** 后繼節(jié)點 */
    volatile Node next;
    /** 獲取同步狀態(tài)的線程 */
    volatile Thread thread;
    Node nextWaiter;
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    Node() {
    }
    Node(Thread thread, Node mode) {
        this.nextWaiter = mode;
        this.thread = thread;
    }
    Node(Thread thread, int waitStatus) {
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

CLH同步隊列結(jié)構(gòu)圖如下:

clh.png

入列
??學(xué)了數(shù)據(jù)結(jié)構(gòu)的我們舍沙,CLH隊列入列是再簡單不過了,無非就是tail指向新節(jié)點剔宪、新節(jié)點的prev指向當前最后的節(jié)點拂铡,當前最后一個節(jié)點的next指向當前節(jié)點。代碼我們可以看看addWaiter(Node node)方法:

private Node addWaiter(Node mode) {
        //新建Node
        Node node = new Node(Thread.currentThread(), mode);
        //快速嘗試添加尾節(jié)點
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            //CAS設(shè)置尾節(jié)點
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //多次嘗試
        enq(node);
        return node;
    }

??addWaiter(Node node)先通過快速嘗試設(shè)置尾節(jié)點葱绒,如果失敗感帅,則調(diào)用enq(Node node)方法設(shè)置尾節(jié)點

private Node enq(final Node node) {
        //多次嘗試,直到成功為止
        for (;;) {
            Node t = tail;
            //tail不存在地淀,設(shè)置為首節(jié)點
            if (t == null) {
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //設(shè)置為尾節(jié)點
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

??在上面代碼中失球,兩個方法都是通過一個CAS方法compareAndSetTail(Node expect, Node update)來設(shè)置尾節(jié)點,該方法可以確保節(jié)點是線程安全添加的帮毁。在enq(Node node)方法中实苞,AQS通過“死循環(huán)”的方式來保證節(jié)點可以正確添加,只有成功添加后烈疚,當前線程才會從該方法返回黔牵,否則會一直執(zhí)行下去。
過程圖如下:

image.png

出列
??CLH同步隊列遵循FIFO爷肝,首節(jié)點的線程釋放同步狀態(tài)后猾浦,將會喚醒它的后繼節(jié)點(next),而后繼節(jié)點將會在獲取同步狀態(tài)成功時將自己設(shè)置為首節(jié)點灯抛,這個過程非常簡單金赦,head執(zhí)行該節(jié)點并斷開原首節(jié)點的next和當前節(jié)點的prev即可,注意在這個過程是不需要使用CAS來保證的对嚼,因為只有一個線程能夠成功獲取到同步狀態(tài)夹抗。

過程圖如下:

image.png

同步狀態(tài)的獲取與釋放

??在前面提到過,AQS是構(gòu)建Java同步組件的基礎(chǔ)纵竖,我們期待它能夠成為實現(xiàn)大部分同步需求的基礎(chǔ)漠烧。AQS的設(shè)計模式采用的模板方法模式,子類通過繼承的方式磨确,實現(xiàn)它的抽象方法來管理同步狀態(tài)沽甥,對于子類而言它并沒有太多的活要做,AQS提供了大量的模板方法來實現(xiàn)同步乏奥,主要是分為三類:獨占式獲取和釋放同步狀態(tài)摆舟、共享式獲取和釋放同步狀態(tài)、查詢同步隊列中的等待線程情況。自定義子類使用AQS提供的模板方法就可以實現(xiàn)自己的同步語義恨诱。

獨占式:同一時刻僅有一個線程持有同步狀態(tài)媳瞪。
獨占式同步狀態(tài)獲取:
??acquire(int arg)方法為AQS提供的模板方法照宝,該方法為獨占式獲取同步狀態(tài)蛇受,但是該方法對中斷不敏感,也就是說由于線程獲取同步狀態(tài)失敗加入到CLH同步隊列中厕鹃,后續(xù)對線程進行中斷操作時兢仰,線程不會從同步隊列中移除。代碼如下:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

各個方法定義如下:

  • tryAcquire:去嘗試獲取鎖剂碴,獲取成功則設(shè)置鎖狀態(tài)并返回true把将,否則返回false。該方法自定義同步組件自己實現(xiàn)忆矛,該方法必須要保證線程安全的獲取同步狀態(tài)察蹲。
  • addWaiter:如果tryAcquire返回FALSE(獲取同步狀態(tài)失敗)催训,則調(diào)用該方法將當前線程加入到CLH同步隊列尾部洽议。
  • acquireQueued:當前線程會根據(jù)公平性原則來進行阻塞等待(自旋),直到獲取鎖為止;并且返回當前線程在等待過程中有沒有中斷過漫拭。
  • selfInterrupt:產(chǎn)生一個中斷亚兄。

acquireQueued方法為一個自旋的過程,也就是說當前線程(Node)進入同步隊列后嫂侍,就會進入一個自旋的過程儿捧,每個節(jié)點都會自省地觀察,當條件滿足挑宠,獲取到同步狀態(tài)后,就可以從這個自旋過程中退出颓影,否則會一直執(zhí)行下去各淀。

   final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            //中斷標志
            boolean interrupted = false;
            /*
             * 自旋過程,其實就是一個死循環(huán)而已
             */
            for (;;) {
                //當前線程的前驅(qū)節(jié)點
                final Node p = node.predecessor();
                //當前線程的前驅(qū)節(jié)點是頭結(jié)點诡挂,且同步狀態(tài)成功
                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);
        }
    }

??從上面代碼中可以看到,當前線程會一直嘗試獲取同步狀態(tài)璃俗,當然前提是只有其前驅(qū)節(jié)點為頭結(jié)點才能夠嘗試獲取同步狀態(tài)奴璃,理由:

  • 保持FIFO同步隊列原則。
  • 頭節(jié)點釋放同步狀態(tài)后城豁,將會喚醒其后繼節(jié)點苟穆,后繼節(jié)點被喚醒后需要檢查自己是否為頭節(jié)點。

acquire(int arg)方法流程圖如下:

image.png

獨占式獲取響應(yīng)中斷
??AQS提供了acquire(int arg)方法以供獨占式獲取同步狀態(tài),但是該方法對中斷不響應(yīng)雳旅,對線程進行中斷操作后跟磨,該線程會依然位于CLH同步隊列中等待著獲取同步狀態(tài)。為了響應(yīng)中斷攒盈,AQS提供了acquireInterruptibly(int arg)方法抵拘,該方法在等待獲取同步狀態(tài)時,如果當前線程被中斷了型豁,會立刻響應(yīng)中斷拋出異常InterruptedException僵蛛。

   public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

??首先校驗該線程是否已經(jīng)中斷了,如果是則拋出InterruptedException迎变,否則執(zhí)行tryAcquire(int arg)方法獲取同步狀態(tài)充尉,如果獲取成功,則直接返回氏豌,否則執(zhí)行doAcquireInterruptibly(int arg)喉酌。doAcquireInterruptibly(int arg)定義如下:

private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

??doAcquireInterruptibly(int arg)方法與acquire(int arg)方法僅有兩個差別。

  1. 方法聲明拋出InterruptedException異常泵喘。
  2. 在中斷方法處不再是使用interrupted標志泪电,而是直接拋出InterruptedException異常。

獨占式超時獲取
??AQS除了提供上面兩個方法外纪铺,還提供了一個增強版的方法:tryAcquireNanos(int arg,long nanos)相速。該方法為acquireInterruptibly方法的進一步增強,它除了響應(yīng)中斷外鲜锚,還有超時控制突诬。即如果當前線程沒有在指定時間內(nèi)獲取同步狀態(tài),則會返回false芜繁,否則返回true旺隙。如下:

   public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

tryAcquireNanos(int arg, long nanosTimeout)方法超時獲取最終是在doAcquireNanos(int arg, long nanosTimeout)中實現(xiàn)的,如下:

  private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        //nanosTimeout <= 0
        if (nanosTimeout <= 0L)
            return false;
        //超時時間
        final long deadline = System.nanoTime() + nanosTimeout;
        //新增Node節(jié)點
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            //自旋
            for (;;) {
                final Node p = node.predecessor();
                //獲取同步狀態(tài)成功
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                /*
                 * 獲取失敗骏令,做超時蔬捷、中斷判斷
                 */
                //重新計算需要休眠的時間
                nanosTimeout = deadline - System.nanoTime();
                //已經(jīng)超時,返回false
                if (nanosTimeout <= 0L)
                    return false;
                //如果沒有超時榔袋,則等待nanosTimeout納秒
                //注:該線程會直接從LockSupport.parkNanos中返回周拐,
                //LockSupport為JUC提供的一個阻塞和喚醒的工具類,后面做詳細介紹
                if (shouldParkAfterFailedAcquire(p, node) &&
                        nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                //線程是否已經(jīng)中斷了
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

??針對超時控制凰兑,程序首先記錄喚醒時間deadline 妥粟,deadline = System.nanoTime() + nanosTimeout(時間間隔)。如果獲取同步狀態(tài)失敗吏够,則需要計算出需要休眠的時間間隔nanosTimeout(= deadline - System.nanoTime())勾给,如果nanosTimeout <= 0 表示已經(jīng)超時了滩报,返回false,如果大于spinForTimeoutThreshold(1000L)則需要休眠nanosTimeout 锦秒,如果nanosTimeout <= spinForTimeoutThreshold 露泊,就不需要休眠了,直接進入快速自旋的過程旅择。原因在于 spinForTimeoutThreshold 已經(jīng)非常小了惭笑,非常短的時間等待無法做到十分精確,如果這時再次進行超時等待生真,相反會讓nanosTimeout 的超時從整體上面表現(xiàn)得不是那么精確沉噩,所以在超時非常短的場景中,AQS會進行無條件的快速自旋柱蟀。

image.png

獨占式同步狀態(tài)釋放
??當線程獲取同步狀態(tài)后川蒙,執(zhí)行完相應(yīng)邏輯后就需要釋放同步狀態(tài)。AQS提供了release(int arg)方法釋放同步狀態(tài):

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

??該方法同樣是先調(diào)用自定義同步器自定義的tryRelease(int arg)方法來釋放同步狀態(tài)长已,釋放成功后畜眨,會調(diào)用unparkSuccessor(Node node)方法喚醒后繼節(jié)點(如何喚醒LZ后面介紹)。

這里稍微總結(jié)下:

在AQS中維護著一個FIFO的同步隊列术瓮,當線程獲取同步狀態(tài)失敗后康聂,則會加入到這個CLH同步隊列的對尾并一直保持著自旋。在CLH同步隊列中的線程在自旋時會判斷其前驅(qū)節(jié)點是否為首節(jié)點胞四,如果為首節(jié)點則不斷嘗試獲取同步狀態(tài)恬汁,獲取成功則退出CLH同步隊列。當線程執(zhí)行完邏輯后辜伟,會釋放同步狀態(tài)氓侧,釋放后會喚醒其后繼節(jié)點。
共享式
共享式與獨占式的最主要區(qū)別在于同一時刻獨占式只能有一個線程獲取同步狀態(tài)导狡,而共享式在同一時刻可以有多個線程獲取同步狀態(tài)约巷。例如讀操作可以有多個線程同時進行,而寫操作同一時刻只能有一個線程進行寫操作旱捧,其他操作都會被阻塞载庭。

共享式同步狀態(tài)獲取
??AQS提供acquireShared(int arg)方法共享式獲取同步狀態(tài):

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            //獲取失敗,自旋獲取同步狀態(tài)
            doAcquireShared(arg);
    }

??從上面程序可以看出廊佩,方法首先是調(diào)用tryAcquireShared(int arg)方法嘗試獲取同步狀態(tài),如果獲取失敗則調(diào)用doAcquireShared(int arg)自旋方式獲取同步狀態(tài)靖榕,共享式獲取同步狀態(tài)的標志是返回 >= 0 的值表示獲取成功标锄。自選式獲取同步狀態(tài)如下:

    private void doAcquireShared(int arg) {
        /共享式節(jié)點
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //前驅(qū)節(jié)點
                final Node p = node.predecessor();
                //如果其前驅(qū)節(jié)點,獲取同步狀態(tài)
                if (p == head) {
                    //嘗試獲取同步
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

??tryAcquireShared(int arg)方法嘗試獲取同步狀態(tài)茁计,返回值為int料皇,當其 >= 0 時谓松,表示能夠獲取到同步狀態(tài),這個時候就可以從自旋過程中退出践剂。
??acquireShared(int arg)方法不響應(yīng)中斷鬼譬,與獨占式相似,AQS也提供了響應(yīng)中斷逊脯、超時的方法优质,分別是:acquireSharedInterruptibly(int arg)、tryAcquireSharedNanos(int arg,long nanos)军洼,這里就不做解釋了巩螃。

共享式同步狀態(tài)釋放
??獲取同步狀態(tài)后,需要調(diào)用release(int arg)方法釋放同步狀態(tài)匕争,方法如下:

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

因為可能會存在多個線程同時進行釋放同步狀態(tài)資源避乏,所以需要確保同步狀態(tài)安全地成功釋放,一般都是通過CAS和循環(huán)來完成的甘桑。

阻塞和喚醒線程

??在線程獲取同步狀態(tài)時如果獲取失敗拍皮,則加入CLH同步隊列,通過通過自旋的方式不斷獲取同步狀態(tài)跑杭,但是在自旋的過程中則需要判斷當前線程是否需要阻塞铆帽,其主要方法在acquireQueued():

if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;

??通過這段代碼我們可以看到,在獲取同步狀態(tài)失敗后艘蹋,線程并不是立馬進行阻塞锄贼,需要檢查該線程的狀態(tài),檢查狀態(tài)的方法為 shouldParkAfterFailedAcquire(Node pred, Node node) 方法女阀,該方法主要靠前驅(qū)節(jié)點判斷當前線程是否應(yīng)該被阻塞宅荤,代碼如下:

   private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //前驅(qū)節(jié)點
        int ws = pred.waitStatus;
        //狀態(tài)為signal,表示當前線程處于等待狀態(tài)浸策,直接放回true
        if (ws == Node.SIGNAL)
            return true;
        //前驅(qū)節(jié)點狀態(tài) > 0 冯键,則為Cancelled,表明該節(jié)點已經(jīng)超時或者被中斷了,需要從同步隊列中取消
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        }
        //前驅(qū)節(jié)點狀態(tài)為Condition庸汗、propagate
        else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

這段代碼主要檢查當前線程是否需要被阻塞惫确,具體規(guī)則如下:

  1. 如果當前線程的前驅(qū)節(jié)點狀態(tài)為SINNAL,則表明當前線程需要被阻塞蚯舱,調(diào)用unpark()方法喚醒改化,直接返回true,當前線程阻塞
  2. 如果當前線程的前驅(qū)節(jié)點狀態(tài)為CANCELLED(ws > 0)枉昏,則表明該線程的前驅(qū)節(jié)點已經(jīng)等待超時或者被中斷了陈肛,則需要從CLH隊列中將該前驅(qū)節(jié)點刪除掉,直到回溯到前驅(qū)節(jié)點狀態(tài) <= 0 兄裂,返回false
  3. 如果前驅(qū)節(jié)點非SINNAL句旱,非CANCELLED阳藻,則通過CAS的方式將其前驅(qū)節(jié)點設(shè)置為SINNAL,返回false

如果 shouldParkAfterFailedAcquire(Node pred, Node node) 方法返回true谈撒,則調(diào)用parkAndCheckInterrupt()方法阻塞當前線程:

   private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

parkAndCheckInterrupt() 方法主要是把當前線程掛起腥泥,從而阻塞住線程的調(diào)用棧,同時返回當前線程的中斷狀態(tài)啃匿。其內(nèi)部則是調(diào)用LockSupport工具類的park()方法來阻塞該方法蛔外。

當線程釋放同步狀態(tài)后,則需要喚醒該線程的后繼節(jié)點:

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                //喚醒后繼節(jié)點
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

調(diào)用unparkSuccessor(Node node)喚醒后繼節(jié)點:

   private void unparkSuccessor(Node node) {
        //當前節(jié)點狀態(tài)
        int ws = node.waitStatus;
        //當前狀態(tài) < 0 則設(shè)置為 0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        //當前節(jié)點的后繼節(jié)點
        Node s = node.next;
        //后繼節(jié)點為null或者其狀態(tài) > 0 (超時或者被中斷了)
        if (s == null || s.waitStatus > 0) {
            s = null;
            //從tail節(jié)點來找可用節(jié)點
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //喚醒后繼節(jié)點
        if (s != null)
            LockSupport.unpark(s.thread);
    }

可能會存在當前線程的后繼節(jié)點為null立宜,超時冒萄、被中斷的情況,如果遇到這種情況了橙数,則需要跳過該節(jié)點尊流,但是為何是從tail尾節(jié)點開始,而不是從node.next開始呢灯帮?原因在于node.next仍然可能會存在null或者取消了崖技,所以采用tail回溯辦法找第一個可用的線程。最后調(diào)用LockSupport的unpark(Thread thread)方法喚醒該線程钟哥。

LockSupport
從上面我可以看到迎献,當需要阻塞或者喚醒一個線程的時候,AQS都是使用LockSupport這個工具類來完成的腻贰。

LockSupport是用來創(chuàng)建鎖和其他同步類的基本線程阻塞原語

每個使用LockSupport的線程都會與一個許可關(guān)聯(lián)吁恍,如果該許可可用,并且可在進程中使用播演,則調(diào)用park()將會立即返回冀瓦,否則可能阻塞。如果許可尚不可用写烤,則可以調(diào)用 unpark 使其可用翼闽。但是注意許可不可重入,也就是說只能調(diào)用一次park()方法洲炊,否則會一直阻塞感局。

LockSupport定義了一系列以park開頭的方法來阻塞當前線程,unpark(Thread thread)方法來喚醒一個被阻塞的線程暂衡。如下:


image.png

park(Object blocker)方法的blocker參數(shù)询微,主要是用來標識當前線程在等待的對象,該對象主要用于問題排查和系統(tǒng)監(jiān)控狂巢。

park方法和unpark(Thread thread)都是成對出現(xiàn)的拓提,同時unpark必須要在park執(zhí)行之后執(zhí)行,當然并不是說沒有不調(diào)用unpark線程就會一直阻塞隧膘,park有一個方法代态,它帶了時間戳(parkNanos(long nanos):為了線程調(diào)度禁用當前線程,最多等待指定的等待時間疹吃,除非許可可用)蹦疑。

park()方法的源碼如下:

    public static void park() {
        UNSAFE.park(false, 0L);
    }

unpark(Thread thread)方法源碼如下:

   public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

從上面可以看出,其內(nèi)部的實現(xiàn)都是通過UNSAFE(sun.misc.Unsafe UNSAFE)來實現(xiàn)的萨驶,其定義如下:

public native void park(boolean var1, long var2);
public native void unpark(Object var1);

兩個都是native本地方法歉摧。Unsafe 是一個比較危險的類,主要是用于執(zhí)行低級別腔呜、不安全的方法集合叁温。盡管這個類和所有的方法都是公開的(public),但是這個類的使用仍然受限核畴,你無法在自己的java程序中直接使用該類膝但,因為只有授信的代碼才能獲得該類的實例锋八。

轉(zhuǎn)自:https://mp.weixin.qq.com/s/-swOI_4_cxP5BBSD9wd0lA

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末泉粉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子萨咕,更是在濱河造成了極大的恐慌丑孩,老刑警劉巖冀宴,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異温学,居然都是意外死亡略贮,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進店門仗岖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逃延,“玉大人,你說我怎么就攤上這事箩帚≌嬗眩” “怎么了?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵紧帕,是天一觀的道長盔然。 經(jīng)常有香客問我,道長是嗜,這世上最難降的妖魔是什么愈案? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮鹅搪,結(jié)果婚禮上站绪,老公的妹妹穿的比我還像新娘。我一直安慰自己丽柿,他們只是感情好恢准,可當我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布魂挂。 她就那樣靜靜地躺著,像睡著了一般馁筐。 火紅的嫁衣襯著肌膚如雪涂召。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天敏沉,我揣著相機與錄音果正,去河邊找鬼。 笑死盟迟,一個胖子當著我的面吹牛秋泳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播攒菠,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼迫皱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了要尔?” 一聲冷哼從身側(cè)響起舍杜,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎赵辕,沒想到半個月后既绩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡还惠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年饲握,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蚕键。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡救欧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出锣光,到底是詐尸還是另有隱情笆怠,我是刑警寧澤,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布誊爹,位于F島的核電站蹬刷,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏频丘。R本人自食惡果不足惜办成,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望搂漠。 院中可真熱鬧迂卢,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至拍摇,卻和暖如春亮钦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背充活。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蜡娶,地道東北人混卵。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像窖张,于是被迫代替她去往敵國和親幕随。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,658評論 2 350

推薦閱讀更多精彩內(nèi)容