主頁 >  其他 > 并發編程之java鎖

并發編程之java鎖

2020-09-11 07:04:58 其他

一,鎖的分類

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/qita/4706.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)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more