Java併發編程——AQS組件之信號量(Semaphore)

一、AQS組件——信號量Semaphore

1. 信號量(Semaphore)介紹:

  • 作用:用於保證同一時間併發訪問線程的數目。
  • 信號量在操作系統中是很重要的概念,Java併發庫裏的Semaphore就可以很輕鬆的完成類似操作系統信號量的控制;
  • Semaphore使用acquire方法和release方法來實現控制;
  • 在數據結構中我們學過鏈表,鏈表正常是可以保存無限個節點的,而Semaphore可以實現有限大小的列表。

2.使用場景

 僅能提供有限訪問的資源。比如數據庫連接。


二、信號量(Semaphore)的使用

1.允許單一許可場景

期望:有20個線程,每次只允許3個線程同時執行test()方法,具體代碼如下:

@Slf4j
public class SemaphorehExample1 {

    private static final int threadCount = 200;

    public static void main(String[] args) throws InterruptedException {

        ExecutorService executorService = Executors.newCachedThreadPool();

        //實例化Semaphore,並設置允許的併發數
        final Semaphore semaphore = new Semaphore(3);

        for (int i=0; i<threadCount; i++){
            final int threadNum = i;
            executorService.execute(()->{
                try {
                    semaphore.acquire(); //拿到一個許可,執行後面的test()語句
                    test(threadNum);
                    semaphore.release();//釋放一個許可
                }catch (Exception e){
                    log.error("exception",e);
                }finally {
                }
            });
        }
        executorService.shutdown();
    }

    private static void test(int threadNum) throws InterruptedException {
        log.info("{}",threadNum);
        Thread.sleep(1000);
    }
}

執行結果部分內容如下:

...
23:06:41.896 [pool-1-thread-2] INFO com.mmall.concurrency.example.aqs.SemaphorehExample1 - 1
23:06:41.896 [pool-1-thread-1] INFO com.mmall.concurrency.example.aqs.SemaphorehExample1 - 0
23:06:41.903 [pool-1-thread-3] INFO com.mmall.concurrency.example.aqs.SemaphorehExample1 - 2
23:06:41.905 [pool-1-thread-4] INFO com.mmall.concurrency.example.aqs.SemaphorehExample1 - 3
23:06:41.905 [pool-1-thread-5] INFO com.mmall.concurrency.example.aqs.SemaphorehExample1 - 4
...
  • 可以看到,控制檯日誌每隔一秒就出現一塊日誌,而且每一個塊都是隻有3條日誌,也就說每秒只有同時只有3個線程同時在執行test()命令;
  • 線程執行順序沒有規律;

2.允許多個許可場景

代碼示例:

@Slf4j
public class SemaphorehExample2 {

    private static final int threadCount = 20;

    public static void main(String[] args) throws InterruptedException {

        ExecutorService executorService = Executors.newCachedThreadPool();

        //實例化Semaphore,並設置允許的併發數
        final Semaphore semaphore = new Semaphore(3);

        for (int i=0; i<threadCount; i++){
            final int threadNum = i;
            executorService.execute(()->{
                try {
                    semaphore.acquire(3); //拿到3個許可,執行後面的test()語句
                    test(threadNum);
                    semaphore.release(3);//釋放3個許可
                }catch (Exception e){
                    log.error("exception",e);
                }finally {
                }
            });
        }
        executorService.shutdown();
    }

    private static void test(int threadNum) throws InterruptedException {
        log.info("{}",threadNum);
        Thread.sleep(1000);
    }
}
  • 上述代碼運行結果會是:逐行逐行地輸出日誌。因爲設置了總共只有3個許可,而每次運行一個線程,就需要消耗3個許可,其他線程無法拿到足夠的許可去運行代碼;
  • 上述的代碼邏輯,等同於單線程程序

線程拿到多個許可之後,不一定需要一次性全部釋放掉,可以逐個釋放許可。

3. 超過線程數就丟棄場景

場景:假如當前允許併發數爲3個,超哥3個的話,就丟棄掉多的線程。代碼示例如下:

@Slf4j
public class SemaphorehExample3 {

    private static final int threadCount = 20;

    public static void main(String[] args) throws InterruptedException {

        ExecutorService executorService = Executors.newCachedThreadPool();

        //實例化Semaphore,並設置允許的併發數爲3
        final Semaphore semaphore = new Semaphore(3);

        for (int i=0; i<threadCount; i++){
            final int threadNum = i;
            executorService.execute(()->{
                try {

                    //嘗試去拿許可,如果拿到了就執行後面的test()語句,否則就丟棄該線程
                    if(semaphore.tryAcquire()){
                        test(threadNum);
                    }
                    semaphore.release();//釋放一個許可
                }catch (Exception e){
                    log.error("exception",e);
                }finally {
                }
            });
        }
        executorService.shutdown();
    }

    private static void test(int threadNum) throws InterruptedException {
        log.info("{}",threadNum);
        Thread.sleep(1000);
    }
}

執行結果:

23:32:09.549 [pool-1-thread-2] INFO com.mmall.concurrency.example.aqs.SemaphorehExample3 - 1
23:32:09.550 [pool-1-thread-5] INFO com.mmall.concurrency.example.aqs.SemaphorehExample3 - 4
23:32:09.553 [pool-1-thread-1] INFO com.mmall.concurrency.example.aqs.SemaphorehExample3 - 0
23:32:09.550 [pool-1-thread-3] INFO com.mmall.concurrency.example.aqs.SemaphorehExample3 - 2
23:32:09.553 [pool-1-thread-8] INFO com.mmall.concurrency.example.aqs.SemaphorehExample3 - 7
23:32:09.557 [pool-1-thread-11] INFO com.mmall.concurrency.example.aqs.SemaphorehExample3 - 10
23:32:09.559 [pool-1-thread-13] INFO com.mmall.concurrency.example.aqs.SemaphorehExample3 - 12
23:32:09.561 [pool-1-thread-15] INFO com.mmall.concurrency.example.aqs.SemaphorehExample3 - 14
23:32:09.562 [pool-1-thread-17] INFO com.mmall.concurrency.example.aqs.SemaphorehExample3 - 16
23:32:09.562 [pool-1-thread-19] INFO com.mmall.concurrency.example.aqs.SemaphorehExample3 - 18
23:32:09.562 [pool-1-thread-6] INFO com.mmall.concurrency.example.aqs.SemaphorehExample3 - 5

Process finished with exit code 0

執行結果分析:

  • 每一次執行,最終輸出的日誌的行數都有可能不同;
  • 沒有輸出的日誌,說明該線程因爲沒獲取到許可,直接被丟棄掉了;
  • 上述情況,是申請實時獲取許可,tryAcquire()方法中可以設置等待一段時間,線程可以在該時間段內申請許可,沒有申請到則被拋棄;

4.其他場景使用

tryAcquire()方法提供了多個帶參數的方法:
源碼如下:


    * @param permits the number of permits to acquire
    * @return {@code true} if the permits were acquired and
    *         {@code false} otherwise
    * @throws IllegalArgumentException if {@code permits} is negative
    */
    //設置每次嘗試獲取的許可數
   public boolean tryAcquire(int permits) {
       if (permits < 0) throw new IllegalArgumentException();
       return sync.nonfairTryAcquireShared(permits) >= 0;
   }


    * @param timeout the maximum time to wait for a permit
    * @param unit the time unit of the {@code timeout} argument
    * @return {@code true} if a permit was acquired and {@code false}
    *         if the waiting time elapsed before a permit was acquired
    * @throws InterruptedException if the current thread is interrupted
    */
    //設置每次運行獲取許可的等待時間以及時間單位
   public boolean tryAcquire(long timeout, TimeUnit unit)
       throws InterruptedException {
       return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
   }

    * @param permits the number of permits to acquire
    * @param timeout the maximum time to wait for the permits
    * @param unit the time unit of the {@code timeout} argument
    * @return {@code true} if all permits were acquired and {@code false}
    *         if the waiting time elapsed before all permits were acquired
    * @throws InterruptedException if the current thread is interrupted
    * @throws IllegalArgumentException if {@code permits} is negative
    */
    //該方法是上面兩個方法的總和,即設置在允許時間段內可以獲取的許可數
   public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
       throws InterruptedException {
       if (permits < 0) throw new IllegalArgumentException();
       return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
   }

上述方法的使用,此處只演示其中tryAcquire(long timeout, TimeUnit unit)方法的關鍵示例代碼:

       try {
            if (semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS)) { // 嘗試獲取一個許可
               test(threadNum);
               semaphore.release(); // 釋放一個許可
           }
       } catch (Exception e) {
           log.error("exception", e);
       }

Java併發編程學習系列

如有幫助,煩請點贊收藏一下啦 (◕ᴗ◕✿)

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