Java併發基礎六:併發工具類(3)Semaphore

Semaphore是什麼?

Semaphore中文意思是信號量,也是一個線程併發的輔助類,Semaphore實現了線程同步框架AQS,它的本質是一個"共享鎖",使用Semaphore可以控制同時訪問資源的線程個數,但是不保證線程執行順序。

Semaphore原理

Semaphore維護了有限數量的許可證,只有得到了許可證的線程才能進行共享資源的訪問,如果得不到許可證,說明當前共享資源的訪問已經達到最大限制,所以會掛起當前線程,直到前面的線程處理完任務之後,把許可證歸還,後面排隊的線程纔有機會獲取,然後處理任務。

Semaphore的構造及常用方法

構造方法:

Semaphore(int permits) //非公平模式指定最大允許訪問許可證數量
Semaphore(int permits, boolean fair)//可以通過第二個參數控制是否使用公平模式

一些常用的方法:

acquire() //申請獲取一個許可證,如果沒有許可證,就阻塞直到能夠獲取或者被打斷
availablePermits() // 返回當前有多少個有用的許可證數量hasQueuedThreads()//查詢是否有線程正在等待獲取許可證

drainPermits()//獲得並返回所有立即可用的許可證數量
getQueuedThreads()//返回一個List包含當前可能正在阻塞隊列裏面所有線程對象
getQueueLength()//返回當前可能在阻塞獲取許可證線程的數量
hasQueuedThreads()//查詢是否有線程正在等待獲取許可證
isFair()//返回是否爲公平模式
reducePermits(int reduction)//減少指定數量的許可證
reducePermits(int reduction)//釋放一個許可證
release(int permits)//釋放指定數量的許可證
tryAcquire()//非阻塞的獲取一個許可證

使用案例

Demo:模擬用戶同時訪問時,同時只能允許兩個線程執行。

public class UseSemaphore {

        public static void main(String[] args) {
            // 線程池
            ExecutorService exec = Executors.newCachedThreadPool();
            // 只能2個線程同時訪問
            final Semaphore semp = new Semaphore(2);
            // 模擬12個客戶端訪問
            for (int index = 0; index < 6; index++) {
                final int NO = index;
                Runnable run = new Runnable() {
                    public void run() {
                        try {
                            System.out.println("用戶開始進入"+Thread.currentThread().getName());
                            // 獲取許可
                            semp.acquire();
                            System.out.println("拿到進入許可 Accessing: " + Thread.currentThread().getName());
                            //模擬實際業務邏輯
                            Thread.sleep(5000);
                            // 訪問完後,釋放
                            System.out.println("我處理完事情了,釋放許可:"+Thread.currentThread().getName());
                            semp.release();
                        } catch (InterruptedException e) {
                        }
                    }
                };
                exec.execute(run);
            }

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //System.out.println(semp.getQueueLength());
            // 退出線程池
            exec.shutdown();
        }
    }

Semaphore底層原理:

Semaphore底層與CountDownLatch類似都是通過AQS的共享鎖機制來實現的,指定的數量會設置到AQS裏面的state裏面,然後對於每一個 調用acquire方法線程,state都會減去一,如果state等於0,那麼調用該方法的線程會被添加到同步隊列裏面,同時使用 LockSupport.park方法掛起等待,知道有線程調用了release方法,會對state加1,然後喚醒共享隊列裏面的線程,注意這裏如果是 公平模式,就直接喚醒下一個等待線程即可,如果是非公平模式就允許新加入的線程與已有的線程進行競爭,誰先得到就是誰的,如果新加入的 競爭失敗,就會走公平模式進入隊列排隊。

源碼:

1.acquire函數--獲取許可

  先從獲取一個許可看起,並且先看非公平模式下的實現。首先看acquire方法,acquire方法有幾個重載,但主要是下面這個方法

   public void acquire(int permits) throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();
        sync.acquireSharedInterruptibly(permits);
    }

從上面可以看到,調用了Sync的acquireSharedInterruptibly方法,該方法在父類AQS中,如下:

  public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //如果線程被中斷了,拋出異常
        if (Thread.interrupted())
            throw new InterruptedException();
        //獲取許可失敗,將線程加入到等待隊列中
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

AQS子類如果要使用共享模式的話,需要實現tryAcquireShared方法,下面看NonfairSync的該方法實現:   

     protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
該方法調用了父類中的nonfairTyAcquireShared方法,如下:

        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                //獲取剩餘許可數量
                int available = getState();
                //計算給完這次許可數量後的個數
                int remaining = available - acquires;
                //如果許可不夠或者可以將許可數量重置的話,返回
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

這裏的釋放就是對 state 變量減一(或者更多)的。

返回了剩餘的 state 大小。

當返回值小於 0 的時候,說明獲取鎖失敗了,那麼就需要進入 AQS 的等待隊列了。代碼如下:

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 添加一個節點 AQS 隊列尾部
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        // 死循環
        for (;;) {
            // 找到新節點的上一個節點
            final Node p = node.predecessor();
            // 如果這個節點是 head,就嘗試獲取鎖
            if (p == head) {
                // 繼續嘗試獲取鎖,這個方法是子類實現的
                int r = tryAcquireShared(arg);
                // 如果大於0,說明拿到鎖了。
                if (r >= 0) {
                    // 將 node 設置爲 head 節點
                    // 如果大於0,就說明還有機會獲取鎖,那就喚醒後面的線程,稱之爲傳播
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 如果他的上一個節點不是 head,就不能獲取鎖
            // 對節點進行檢查和更新狀態,如果線程應該阻塞,返回 true。
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 阻塞 park,並返回是否中斷,中斷則拋出異常
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            // 取消節點
            cancelAcquire(node);
    }
}

總的邏輯就是:

創建一個分享類型的 node 節點包裝當前線程追加到 AQS 隊列的尾部。
如果這個節點的上一個節點是 head ,就是嘗試獲取鎖,獲取鎖的方法就是子類重寫的方法。如果獲取成功了,就將剛剛的那個節點設置成 head。
如果沒搶到鎖,就阻塞等待。
看完了非公平的獲取,再看下公平的獲取,代碼如下:

  protected int tryAcquireShared(int acquires) {
            for (;;) {
                //如果前面有線程再等待,直接返回-1
                if (hasQueuedPredecessors())
                    return -1;
                //後面與非公平一樣
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

從上面可以看到,FairSync與NonFairSync的區別就在於會首先判斷當前隊列中有沒有線程在等待,如果有,就老老實實進入到等待隊列;而不像NonfairSync一樣首先試一把,說不定就恰好獲得了一個許可,這樣就可以插隊了。
看完了獲取許可後,再看一下release()方法。

   2.release()函數--釋放許可

 釋放許可也有幾個重載方法,但都會調用下面這個帶參數的方法

 public void release(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        sync.releaseShared(permits);
    }

releaseShared方法在AQS中,如下:

 public final boolean releaseShared(int arg) {
        //如果改變許可數量成功
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

AQS子類實現共享模式的類需要實現tryReleaseShared類來判斷是否釋放成功,實現如下:

  protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                //獲取當前許可數量
                int current = getState();
                //計算回收後的數量
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                //CAS改變許可數量成功,返回true
                if (compareAndSetState(current, next))
                    return true;
            }
        }

從上面可以看到,一旦CAS改變許可數量成功,那麼就會調用doReleaseShared()方法釋放阻塞的線程

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                // 設置 head 的等待狀態爲 0 ,並喚醒 head 上的線程
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            // 成功設置成 0 之後,將 head 狀態設置成傳播狀態
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

文章參考:

https://blog.csdn.net/qq_39241239/article/details/87069630

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章