主頁 > 軟體設計 > 并發編程之java鎖

并發編程之java鎖

2020-09-11 06:46:50 軟體設計

一,鎖的分類

1.執行緒是否要鎖住同步資源

鎖住 悲觀鎖
不鎖住 樂觀鎖

2.鎖住同步資源失敗 執行緒是否要阻塞

阻塞
不阻塞 自旋鎖,適應性自旋鎖

3.多個執行緒競爭同步資源的流程細節有沒有區別

不鎖住資源,多個執行緒只有一個能修改資源成功,其它執行緒會重試 無鎖
同一個執行緒執行同步資源時自動獲取資源 偏向鎖
多個執行緒競爭同步資源時,沒有獲取資源的執行緒自旋等待鎖釋放 輕量級鎖
多個執行緒競爭同步資源時,沒有獲取資源的執行緒阻塞等待喚醒 重量級鎖

4.多個執行緒競爭鎖時是否要排隊

排隊 公平鎖
先嘗試插隊,插隊失敗在排隊 非公平鎖

5.一個執行緒的多個流程能不能獲取同一把鎖

可重入鎖
不能 非可重入鎖

6.多個執行緒能不能共享一把鎖

共享
不能 排他鎖


二,悲觀鎖與樂觀鎖

悲觀鎖與樂觀鎖時一種廣義的概念,體現的是看待執行緒同步的不同角度,

1.悲觀鎖

悲觀鎖認為自己在使用資料的時候一定有別的執行緒來修改資料,在獲取資料的時候會先加鎖,確保資料不會被別的執行緒修改,
鎖實作:synchronized 介面Lock的實作類
適用場景:寫操作多,先加鎖可以保證寫操作時資料正確,

2.樂觀鎖

樂觀鎖認為自己在使用資料時不會有別的執行緒修改資料,所以不會添加鎖,只是在更新資料的時候去判斷之前有沒有別的執行緒更新了這個資料,
鎖實作:CAS演算法,例如AtomicInteger類的原子自增時通過CAS自旋實作,
適用場景:讀操作較多,不加鎖的特點能夠使其讀操作的性能大幅度提升,
樂觀鎖的執行流程:
執行緒A獲取到資料以后直接操作,操作完資料以后準備更新同步資源,更新之前會先判斷記憶體中同步資源是否被更新:
1.如果沒有被更新,更新記憶體中同步資源的值,
2.如果同步資源被其他執行緒更新,根據實作方法執行不同的操做(報錯or重試),

3.CAS演算法

全名:Compare And Swap(比較并交換)
無鎖演算法:基于硬體原語實作,在不使用鎖(沒有執行緒被阻塞)的情況下實作多執行緒之間的變數同步,
jdk中的實作:java.util.concurrent包中的原子類就是通過CAS來實作了樂觀鎖,
演算法涉及到的三個運算元:

需要讀寫的記憶體值V
進行比較的值A
要寫入的新值的B

在這里插入圖片描述

4.CAS存在的問題

1.ABA問題
執行緒1準備用CAS將變數的值由A替換為B,在此之前,執行緒2將變數的值由A替換為C,又由C替換為A,然后執行緒1執行CAS時發現變數的值仍然為A,所以CAS成功,但實際上這時的現場已經和最初不同了,盡管CAS成功,但可能存在潛藏的問題,
舉例:一個小偷,把別人家的錢偷了之后又還了回來,還是原來的錢嗎,你老婆出軌之后又回來,還是原來的老婆嗎?ABA問題也一樣,如果不好好解決就會帶來大量的問題,最常見的就是資金問題,也就是別人如果挪用了你的錢,在你發現之前又還了回來,但是別人卻已經觸犯了法律,
但是jdk已經解決了這個問題,
想追下原始碼來著,但是一追發現直接到c了,

2.回圈時間長開銷大
3.只能保證一個共享變數的原子操作

5.Unsafe

AtomicInteger

public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

從這里可見原子類的方法呼叫了unsafe類的方法
unsafe

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

再往底層的話,實際加上就到了C,
最終的實作就是 cmpxchg = cas修改變數值

lock cmpxchg 指令

硬體:
lock指令在執行后面指令的時候鎖定一個北橋信號(不采用鎖總線的方式)

三,自旋鎖

是指當一個執行緒在獲取鎖的時候,如果鎖已經被其他執行緒獲取,那么該執行緒將回圈等待,然后不斷的判斷鎖是否能夠被成功獲取,自旋直到獲取到鎖才會退出回圈,

自旋存在的意義與使用場景

阻塞與喚醒執行緒需要作業系統切換CPU狀態,需要消耗一定時間,
同步代碼塊邏輯簡單,執行時間很短,
自適應自旋鎖假定不同執行緒持有同一個鎖物件的時間基本相當,競爭程度趨于穩定,因此,可以根據上一次自旋的時間與結果調整下一次自旋的時間,
JDK>=1.7自旋鎖的引數被取消,虛擬機不再支持由用戶配置自旋鎖,自旋鎖總是會執行,自旋鎖次數也是由虛擬機自動調整,
在這里插入圖片描述

四,鎖升級

1.鎖升級流程

jdk6的時候對鎖進行了很多優化,其中就有了鎖的升級程序,

1.偏向鎖:只有一個執行緒進入臨界區,適用于只有一個執行緒訪問同步塊的場景
2.輕量級鎖:多執行緒為競爭或者競爭不激烈,適用于追求回應時間,同步塊執行速度非常快,
3.重量級鎖:多執行緒競爭,適用于追求吞吐量,同步塊執行速度較長,

2.JVM物件加鎖的原理

物件的記憶體結構?
物件頭:比如hash碼,物件所屬的年代,物件鎖,鎖狀態標志,偏向鎖(執行緒)ID,偏向時間,陣列長度(陣列物件)等,
實體資料:創建物件時,物件中的成員變數,方法等,
對齊填充:就為了湊夠8的倍數,
在這里插入圖片描述
在這里插入圖片描述
利用一個插件,對比物件上鎖前后的差異:

    <dependencies>
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>
    </dependencies>
    public static void main(String[] args) {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        System.out.println("===============================================");
        synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }

在這里插入圖片描述

3.實體物件是怎樣存盤的?

物件實體存盤在堆空間,物件的元資料存在元空間,物件參考存在堆疊空間,

4.鎖消除

/**
 * @author yhd
 * @createtime 2020/9/8 20:40
 */
public class Demo2 {
    public static void main(String[] args) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("a").append("b");
        System.out.println(ClassLayout.parseInstance(buffer).toPrintable());
    }
}

在這里插入圖片描述
我們都知道StringBuffer是執行緒安全的,因為他的關鍵方法都加了synchronized,但是,從列印結果可以看出,鎖被消除了,因為buffer這個參考只會在main方法中使用,不可能被其他執行緒參考(因為是區域變數,堆疊私有),所以buffer是不可能共享的資源,JVM會自動消除StringBuffer物件內部的鎖,

5.鎖粗化

/**
 * @author yhd
 * @createtime 2020/9/8 20:48
 */
public class Demo3 {
    public static void main(String[] args) {
        int i=0;
        StringBuffer buffer = new StringBuffer();
        while (i<100){
            buffer.append(i);
            i++;
        }
        System.out.println(buffer.toString());
        System.out.println(ClassLayout.parseInstance(buffer).toPrintable());
    }
}

JVM會檢測到這樣一連串的操作都對同一個物件加鎖(while 回圈內 100 次執行 append,沒有鎖粗化的就要進行 100 次加鎖/解鎖),此時 JVM 就會將加鎖的范圍粗化到這一連串的操作的外部(比如 while 虛幻體外),使得這一連串操作只需要加一次鎖即可,

6.synchronized和volatile

關于synchronized和volatile的具體講解請看我的另一篇博客

五,AQS

研究了AQS一天,終于找到了他的入口,接下來看我的想法:

1.多執行緒操做共享資料問題

/**
 * @author yhd
 * @createtime 2020/9/8 8:11
 */
public class Demo1 {
    public static int m=0;

    public static void main(String[] args)throws Exception {
        Thread []threads=new Thread[100];
        for (int i = 0; i < threads.length; i++) {
            threads[i]=new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    m++;
                }
            });
        }
        for (Thread t :threads) t.start();
        for (Thread t :threads) t.join();//執行緒順序結束
        System.out.println(m);
    }
}

毫無疑問,這段代碼是存在執行緒安全問題的,只要了解一點并發編程,都是可以看出來的,那么我們可以怎么來解決呢?

使用synchronized來解決

/**
 * @author yhd
 * @createtime 2020/9/8 8:32
 */
public class Demo2 {

    public static int m=0;

    public static void main(String[] args)throws Exception {
        Thread []threads=new Thread[100];
        for (int i = 0; i < threads.length; i++) {
            threads[i]=new Thread(()->{
                synchronized (Demo2.class) {
                    for (int j = 0; j < 100; j++) {
                        m++;
                    }
                }
            });
        }
        for (Thread t :threads) t.start();
        for (Thread t :threads) t.join();//執行緒順序結束
        System.out.println(m);
    }

這樣解決了執行緒安全的問題,但是同時也存在一個問題,synchronized是作業系統層面的方法,也就是需要jvm和操做系統之間進行一個切換(用戶態和內核態的切換),這樣實際上是影響效率的,另一種解決辦法:

使用ReentrantLock來解決

/**
 * @author yhd
 * @createtime 2020/9/8 8:41
 */
public class Demo3 {

    public static int m=0;
    public static Lock lock=new ReentrantLock();
    public static void main(String[] args)throws Exception {
        Thread []threads=new Thread[100];
        for (int i = 0; i < threads.length; i++) {
            threads[i]=new Thread(()->{

                try {
                    lock.lock();
                    for (int j = 0; j < 100; j++) {
                            m++;
                    }
                } finally {
                    lock.unlock();
                }
            });
        }
        for (Thread t :threads) t.start();
        for (Thread t :threads) t.join();//執行緒順序結束
        System.out.println(m);
    }
}

那么這種方式的底層是怎么做的呢?接下來追原始碼,

    public ReentrantLock() {
        sync = new NonfairSync();
    }

這個sync又是啥呢?接著追

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

它實際上是ReentrantLock的一個內部類繼承了Sync,而他里面的方法實際上也是呼叫了Sync的方法,這樣目標就明確了,我們可以看一下Sync
這個Sync的原始碼:

abstract static class Sync extends AbstractQueuedSynchronizer

由此可知,實際上ReentrantLock是利用AbstractQueuedSynchronizer也就是AQS來實作的,

2.AbstractQueuedSynchronizer概述

此處參考的文章鏈接
這個類的內部有一個內部類Node

static final class Node {
	static final Node SHARED = new Node();
	volatile Node prev;//前驅指標
	volatile Node next;//后繼指標
	volatile Thread thread;//當前執行緒
	private transient volatile Node head;//頭節點
	private transient volatile Node tail;//尾節點
	private volatile int state;//鎖狀態,加鎖成功則為1,重入+1 解鎖則為0
	.....
}

看到這里就想到了LinkedHashMap,實際上也就是類似于維持了一個雙向的鏈表,每個節點都是一個執行緒,
在這里插入圖片描述
它維護了一個volatile int state(代表共享資源)和一個FIFO執行緒等待佇列(多執行緒爭用資源被阻塞時會進入此佇列),這里volatile是核心關鍵詞,具體volatile的語意,在此不述,state的訪問方式有三種:

    getState()
    setState()
    compareAndSetState()

AQS定義兩種資源共享方式:Exclusive(獨占,只有一個執行緒能執行,如ReentrantLock)和Share(共享,多個執行緒可同時執行,如Semaphore/CountDownLatch),
  不同的自定義同步器爭用共享資源的方式也不同,自定義同步器在實作時只需要實作共享資源state的獲取與釋放方式即可,至于具體執行緒等待佇列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實作好了,自定義同步器實作時主要實作以下幾種方法:

`isHeldExclusively()`:該執行緒是否正在獨占資源,只有用到condition才需要去實作它,
`tryAcquire(int)`:獨占方式,嘗試獲取資源,成功則回傳true,失敗則回傳false,
`tryRelease(int)`:獨占方式,嘗試釋放資源,成功則回傳true,失敗則回傳false,
`tryAcquireShared(int)`:共享方式,嘗試獲取資源,負數表示失敗;0表示成功,但沒有剩余可用資源;正數表示成功,且有剩余資源,
`tryReleaseShared(int)`:共享方式,嘗試釋放資源,如果釋放后允許喚醒后續等待結點回傳true,否則回傳false,

ReentrantLock為例,state初始化為0,表示未鎖定狀態,A執行緒lock()時,會呼叫tryAcquire()獨占該鎖并將state+1,此后,其他執行緒再tryAcquire()時就會失敗,直到A執行緒unlock()到state=0(即釋放鎖)為止,其它執行緒才有機會獲取該鎖,當然,釋放鎖之前,A執行緒自己是可以重復獲取此鎖的(state會累加),這就是可重入的概念,但要注意,獲取多少次就要釋放多么次,這樣才能保證state是能回到零態的,

再以CountDownLatch以例,任務分為N個子執行緒去執行,state也初始化為N(注意N要與執行緒個數一致),這N個子執行緒是并行執行的,每個子執行緒執行完后countDown()一次,state會CAS減1,等到所有子執行緒都執行完后(即state=0),會unpark()主呼叫執行緒,然后主呼叫執行緒就會從await()函式回傳,繼續后余動作,

一般來說,自定義同步器要么是獨占方法,要么是共享方式,他們也只需實作tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一種即可,但AQS也支持自定義同步器同時實作獨占和共享兩種方式,如ReentrantReadWriteLock

3.原始碼解讀與執行流程分析

3.1,獨占鎖

acquire(int)==
此方法是獨占模式下執行緒獲取共享資源的頂層入口,如果獲取到資源,執行緒直接回傳,否則進入等待佇列,直到獲取到資源為止,且整個程序忽略中斷的影響,這也正是lock()的語意,當然不僅僅只限于lock(),獲取到資源后,執行緒就可以去執行其臨界區代碼了,

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

函式流程如下:

tryAcquire()嘗試直接去獲取資源,如果成功則直接回傳;
addWaiter()將該執行緒加入等待佇列的尾部,并標記為獨占模式;
acquireQueued()使執行緒在等待佇列中獲取資源,一直獲取到資源后才回傳,如果在整個等待程序中被中斷過,則回傳true,否則回傳false,如果執行緒在等待程序中被中斷過,它是不回應的,只是獲取資源后才再進行自我中斷selfInterrupt(),將中斷補上,
tryAcquire(int)
此方法嘗試去獲取獨占資源,如果獲取成功,則直接回傳true,否則直接回傳false,這也正是tryLock()的語意,還是那句話,當然不僅僅只限于tryLock(),

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

一開始還傻乎乎的想為啥直接拋例外了,后來才反應過來,這不是給自定義的方法么?AQS這里只定義了一個介面,具體資源的獲取交由自定義同步器去實作了,
addWaiter(Node)
此方法用于將當前執行緒加入到等待佇列的隊尾,并回傳當前執行緒所在的結點,

    private Node addWaiter(Node mode) {
     //以給定模式構造結點,mode有兩種:EXCLUSIVE(獨占)和SHARED(共享)
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

Node結點是對每一個訪問同步代碼的執行緒的封裝,其包含了需要同步的執行緒本身以及執行緒的狀態,如是否被阻塞,是否等待喚醒,是否已經被取消等,變數waitStatus則表示當前被封裝成Node結點的等待狀態,共有4種取值CANCELLED、SIGNAL、CONDITION、PROPAGATE,

CANCELLED:值為1,在同步佇列中等待的執行緒等待超時或被中斷,需要從同步佇列中取消該Node的結點,其結點的waitStatus為CANCELLED,即結束狀態,進入該狀態后的結點將不會再變化,

SIGNAL:值為-1,被標識為該等待喚醒狀態的后繼結點,當其前繼結點的執行緒釋放了同步鎖或被取消,將會通知該后繼結點的執行緒執行,說白了,就是處于喚醒狀態,只要前繼結點釋放鎖,就會通知標識為SIGNAL狀態的后繼結點的執行緒執行,

CONDITION:值為-2,與Condition相關,該標識的結點處于等待佇列中,結點的執行緒等待在Condition上,當其他執行緒呼叫了Condition的signal()方法后,CONDITION狀態的結點將從等待佇列轉移到同步佇列中,等待獲取同步鎖,

PROPAGATE:值為-3,與共享模式相關,在共享模式中,該狀態標識結點的執行緒處于可運行狀態,

0狀態:值為0,代表初始化狀態,

AQS在判斷狀態時,通過用waitStatus>0表示取消狀態,而waitStatus<0表示有效狀態,

== enq(Node)==
此方法用于將node加入隊尾,

    private Node enq(final Node node) {
        for (;;) {//自旋
            Node t = tail;
            if (t == null) { // Must initialize
            // 佇列為空,創建一個空的標志結點作為head結點,并將tail也指向它
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {//正常放入佇列尾部
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

cas自旋volatile變數
acquireQueued(Node, int)
通過tryAcquire()和addWaiter(),該執行緒獲取資源失敗,已經被放入等待佇列尾部了,聰明的你立刻應該能想到該執行緒下一步該干什么了吧:進入等待狀態休息,直到其他執行緒徹底釋放資源后喚醒自己,自己再拿到資源,然后就可以去干自己想干的事了,沒錯,就是這樣!是不是跟醫院排隊拿號有點相似~~acquireQueued()就是干這件事:在等待佇列中排隊拿號(中間沒其它事干可以休息),直到拿到號后再回傳,這個函式非常關鍵,還是上原始碼吧:

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;//標記是否已經拿到鎖
        try {
            boolean interrupted = false;//標記等待程序中是否被中斷過
            for (;;) {
                final Node p = node.predecessor();//拿到前驅節點
                //如果前驅是head,即該結點已成老二,那么便有資格去嘗試獲取資源(可能是老大釋放完資源喚醒自己的,當然也可能被interrupt了),
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    //拿到資源后,將head指向該結點,所以head所指的標桿結點,就是當前獲取到資源的那個結點或null,
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //如果自己可以休息了,就進入waiting狀態,直到被unpark()
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;//如果等待程序中被中斷過,哪怕只有那么一次,就將interrupted標記為true
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire(Node, Node)
此方法主要用于檢查狀態,看看自己是否真的可以去休息了,

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//拿到前驅的狀態
        if (ws == Node.SIGNAL)
            //如果已經告訴前驅拿完號后通知自己一下,那就可以安心休息了
            return true;
        if (ws > 0) {
           /*
           * 如果前驅放棄了,那就一直往前找,直到找到最近一個正常等待的狀態,并排在它的后邊,
           * 注意:那些放棄的結點,由于被自己“加塞”到它們前邊,它們相當于形成一個無參考鏈,稍后就會被保安大叔趕走了(GC回收)!
          */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //如果前驅正常,那就把前驅的狀態設定成SIGNAL,告訴它拿完號后通知自己一下,有可能失敗,人家說不定剛剛釋放完呢!
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

整個流程中,如果前驅結點的狀態不是SIGNAL,那么自己就不能安心去休息,需要去找個安心的休息點,同時可以再嘗試下看有沒有機會輪到自己拿號,
parkAndCheckInterrupt()
如果執行緒找好安全休息點后,那就可以安心去休息了,此方法就是讓執行緒去休息,真正進入等待狀態,

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

Thread.interrupted()會清除當前執行緒的中斷標記位,
整個獲取鎖的流程:
1.如果嘗試獲取鎖成功,直接回傳,
2.沒成功,先加入到等待佇列尾部,標記為獨占模式,
3.嘗試這獲取一次鎖后,如果還是獲取不到就去休息,有機會時(輪到自己,會被unpark())會去嘗試獲取資源,獲取到資源后才回傳,如果在整個等待程序中被中斷過,則回傳true,否則回傳false,
4.如果執行緒在等待程序中被中斷過,它是不回應的,只是獲取資源后才再進行自我中斷selfInterrupt(),將中斷補上,
這也就是ReentrantLock.lock()的流程
在這里插入圖片描述
release(int)
此方法是獨占模式下執行緒釋放共享資源的頂層入口,它會釋放指定量的資源,如果徹底釋放了(即state=0),它會喚醒等待佇列里的其他執行緒來獲取資源,

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
                //喚醒等待佇列里的下一個執行緒
            return true;
        }
        return false;
    }

它是根據tryRelease()的回傳值來判斷該執行緒是否已經完成釋放掉資源了!所以自定義同步器在設計tryRelease()的時候要明確這一點!!
tryRelease(int)
此方法嘗試去釋放指定量的資源,

    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

還是需要AQS的實作類自己去寫,
unparkSuccessor(Node)
此方法用于喚醒等待佇列中下一個執行緒,

    private void unparkSuccessor(Node node) {
        //這里,node一般為當前執行緒所在的結點,
        int ws = node.waitStatus;
        if (ws < 0)
        	//置零當前執行緒所在的結點狀態,允許失敗,
            compareAndSetWaitStatus(node, ws, 0);

        //找到下一個需要喚醒的結點s
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {//如果為慷訓已取消
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)//從這里可以看出,<=0的結點,都是還有效的結點,
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);//喚醒
    }

用unpark()喚醒等待佇列中最前邊的那個未放棄執行緒,
釋放鎖的流程
1.釋放指定鎖的資源并回傳結果,
2.如果成功釋放,就喚醒等待佇列中最前邊的那個未放棄執行緒,
3.如果沒成功,回傳false,

3.2,共享鎖

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

此方法用于將當前執行緒加入等待佇列尾部休息,直到其他執行緒釋放資源喚醒自己,自己成功拿到相應量的資源后才回傳,

    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {//成功
                        setHeadAndPropagate(node, r);//將head指向自己,還有剩余資源可以再喚醒之后的執行緒
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

跟獨占模式比,這里只有執行緒是head.next時(“老二”),才會去嘗試獲取資源,有剩余的話還會喚醒之后的隊友,那么問題就來了,假如老大用完后釋放了5個資源,而老二需要6個,老三需要1個,老四需要2個,老大先喚醒老二,老二一看資源不夠,他是把資源讓給老三呢,還是不讓?答案是否定的!老二會繼續park()等待其他執行緒釋放資源,也更不會去喚醒老三和老四了,獨占模式,同一時刻只有一個執行緒去執行,這樣做未嘗不可;但共享模式下,多個執行緒是可以同時執行的,現在因為老二的資源需求量大,而把后面量小的老三和老四也都卡住了,當然,這并不是問題,只是AQS保證嚴格按照入隊順序喚醒罷了(保證公平,但降低了并發),
== setHeadAndPropagate(Node, int)==

    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

== doReleaseShared()==

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

4.自定義鎖

不同的自定義同步器爭用共享資源的方式也不同,自定義同步器在實作時只需要實作共享資源state的獲取與釋放方式即可,至于具體執行緒等待佇列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實作好了,自定義同步器實作時主要實作以下幾種方法:

 	isHeldExclusively():該執行緒是否正在獨占資源,只有用到condition才需要去實作它,
    tryAcquire(int):獨占方式,嘗試獲取資源,成功則回傳true,失敗則回傳falsetryRelease(int):獨占方式,嘗試釋放資源,成功則回傳true,失敗則回傳falsetryAcquireShared(int):共享方式,嘗試獲取資源,負數表示失敗;0表示成功,但沒有剩余可用資源;正數表示成功,且有剩余資源,
    tryReleaseShared(int):共享方式,嘗試釋放資源,如果釋放后允許喚醒后續等待結點回傳true,否則回傳false

自定義一個簡單的鎖

/**
 * @author yhd
 * @createtime 2020/9/8 9:44
 */
public class MLock implements Lock {
    private AbstractQueuedSynchronizer sync=new Sync();
    @Override
    public void lock() {
        sync.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return null;
    }

    //自定義一個獨占鎖
    private class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            assert arg == 1;
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }
    }

}

Demo測驗:

/**
 * @author yhd
 * @createtime 2020/9/8 9:36
 */
public class Demo6 {
    public static int m = 0;
    public static Lock lock = new MLock();

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[100];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {

                try {
                    lock.lock();
                    for (int j = 0; j < 100; j++) {
                        m++;
                    }
                } finally {
                    lock.unlock();
                }
            });
        }
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();//執行緒順序結束
        System.out.println(m);
    }

}

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/4666.html

標籤:其他

上一篇:國仁網路資訊:抖音小店開通流程與保證金繳納;小店禁品類目抖音哪些?

下一篇:自學安全筆記一些記錄,不喜勿噴

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more