java学习之Semaphore信号量

一、需求发生场景

            新碰到一个项目,该项目中有一些接口的供应商对接口的并发量有所限制,所以解决方案就是用了Java中的Semaphore信号量。

二、Semaphore信号量的理解

      Semaphore是java.util.concurrent包下的一个类。从JDK1.5开始开始引入的,Semaphore是内部的维护了一组虚拟的许可,其许可的数量是通过改构造函数的参数来指定。

     访问后端资源时,需要先获取此许可,获取此许可的方法acquire(),当资源使用完后,需要使用release方法释放许可。

     Semaphore可以理解成,一个公共厕所,由于厕所中的坑位是有限的,坑位就相当于许可,当进去一个人时,占了一个坑位那么就相当于调用了一下acquire方法,当从厕所出来一个人时空出了一个坑位,就相当于调用了一次release方法,Semaphore的许可为0时类似厕所中的坑位已经被占满了,后面的请求就会被阻塞就类似外面的人在厕所外排队等着。

三、Semaphore的方法说明

  • acquire()           从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断
  • acquire(int permits)       从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断
  • acquireUninterruptibly()     从此信号量中获取许可,在有可用的许可前将其阻塞。
  • acquireUninterruptibly(int permits)    从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。
  • availablePermits()      返回此信号量中当前可用的许可数。
  • drainPermits()         获取并返回立即可用的所有许可。
  • getQueueLength()        返回正在等待获取的线程的估计数目。
  • hasQueuedThreads()      查询是否有线程正在等待获取。
  • isFair()             如果此信号量的公平设置为 true,则返回 true
  • release()             释放一个许可,将其返回给信号量。
  • release(int permits)     释放给定数目的许可,将其返回到信号量。
  • toString()            返回标识此信号量的字符串,以及信号量的状态。
  • tryAcquire()           仅在调用时此信号量存在一个可用许可,才从信号量获取许可。
  • tryAcquire(int permits)   仅在调用时此信号量中有给定数目的许可时,才从此信号量中获取这些许可。
  • tryAcquire(int permits, long timeout, TimeUnit unit)  如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。
  • tryAcquire(long timeout, TimeUnit unit)  如果在给定的等待时间内,此信号量有可用的许可并且当前线程未被中断,则从此信号量获取一个许可。

四、代码示例

 1 package com.ssc.Semaphore;
 2 
 3 import java.util.concurrent.Semaphore;
 4 import java.util.concurrent.TimeUnit;
 5 
 6 public class SemaphoreDemo {
 7     public static void main(String[] args){
 8         Runnable rester=new Runnable() {
 9             //厕所里现在只有五个坑位
10             final Semaphore pitSemaphore=new Semaphore(5,true);
11             int count=1;
12             @Override
13             public void run() {
14                 int time =(int)(Math.random()*10+2);
15                 int num=count++;
16                 try{
17                     //当前如厕者占用了一个坑
18                     pitSemaphore.acquire();
19                     System.out.println("第"+num+"个如厕者正在蹲坑,需要时间"+time+"秒;"+pitSemaphore.toString());
20                     Thread.sleep(time*1000);
21 
22                     //判断厕所外是否有人再等待
23                     if(pitSemaphore.hasQueuedThreads()){
24                         //打印等待人数
25                         System.out.println("厕所外的等待人数"+pitSemaphore.getQueueLength());
26                     }
27                     if(num==2){
28                         System.out.println("查看许可数"+pitSemaphore.drainPermits()+"     "+pitSemaphore.availablePermits());
29                         //查看当前阻塞队列是否使用的时公平锁
30                         System.out.println("查看信号量是否公平:"+pitSemaphore.isFair());
31                     }
32                     //检查是否能获得许可
33                     System.out.println("查看是否可获得许可"+pitSemaphore.tryAcquire());
34                     System.out.println("查看是否可获得许可"+pitSemaphore.tryAcquire(2,100, TimeUnit.MICROSECONDS));
35                 } catch (InterruptedException e) {
36                     e.printStackTrace();
37                 }finally {
38                     pitSemaphore.release();
39                     System.out.println("第"+num+"个如厕者出去了");
40                 }
41             }
42         };
43         for (int i=1;i<10;i++){
44             new Thread(rester).start();
45         }
46     }
47 }

查看运行结果:

 

 五、源码分析

      1、创建信号量

       构造函数的参数主要有两种,第一种默认的时非公平锁的信号量  Semaphore semaphore=new Semaphore(2);   此构造函数源码为:

       

 

     另外一种是 Semaphore pitSemaphore=new Semaphore(5,true);  此构造函数的源码为

 

      通过此源码可以看出当第二参数为true则采用公平锁,否则采用的是非公平锁

          2、获取令牌

                    pitSemaphore.acquire();

              对应的源码为:

1 /** 获取一个信号量**/
2 public void acquire() throws InterruptedException {
3         sync.acquireSharedInterruptibly(1);
4     }

        acquireSharedInterruptibly方法为:

1     public final void acquireSharedInterruptibly(int arg)
2             throws InterruptedException {
3      //判断当前线程是否被中断
4         if (Thread.interrupted())
5             throw new InterruptedException();
6 //尝试获得令牌,如果可用令牌不够申请的令牌数时arg,则会创建一个节点,将其加入阻塞队列,挂起当前线程
7         if (tryAcquireShared(arg) < 0)
8             doAcquireSharedInterruptibly(arg);
9     }
doAcquireSharedInterruptibly方法为:
 1 /**当前线程获取共享资源失败后,会调用此方法**/
 2     private void doAcquireSharedInterruptibly(int arg)  throws InterruptedException {
 3        //通过调用addWaiter方法把线程封装成node节点,并将该节点设置成共享模式,将该节点加到队列的尾部
 4         final Node node = addWaiter(Node.SHARED);
 5         boolean failed = true;
 6         try {
 7             for (;;) {
 8                //拿到节点的前驱
 9                 final Node p = node.predecessor();
10               //如果该前驱是head,即证明该节点为第二位置,有资格去试图获取资源
11                 if (p == head) {
12                //试图获取许可
13                     int r = tryAcquireShared(arg);
14                     if (r >= 0) {
15                   //调用setHeadAndPropagate方法把该节点设置为新的头节点,同时唤醒队列中所有共享类型的节点,去获取共享资源。
16                         setHeadAndPropagate(node, r);
17                         p.next = null; // help GC
18                         failed = false;
19                         return;
20                     }
21                 }
22                //重组双向链表,清空无效节点,挂起当前线程
23                 if (shouldParkAfterFailedAcquire(p, node) &&
24                     parkAndCheckInterrupt())
25                     throw new InterruptedException();
26             }
27         } finally {
28             if (failed)
29                 //取消获取动作
30                 cancelAcquire(node);
31         }
32     }

3、释放令牌

       pitSemaphore.release();

1 ublic void release() {
2        //未传值的情况下释放一个资源
3         sync.releaseShared(1);
4     }
1  public final boolean releaseShared(int arg) {
2 //获取共享模式资源释放,如果释放成功那么会调用doReleaseShared继续唤醒下一个节点
3         if (tryReleaseShared(arg)) {
4             doReleaseShared();
5             return true;
6         }
7         return false;
8     }
 1  private void doReleaseShared() {
 2         for (;;) {
 3             Node h = head;
 4             if (h != null && h != tail) {
 5                 int ws = h.waitStatus;
 6              //Node.SIGNAL的为-1
 7                 if (ws == Node.SIGNAL) {
 8 //更新当前现成的状态值为0,如果失败则继续
 9                     if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
10                         continue;            // loop to recheck cases
11 //unparkSuccessor()此方法是唤醒共享锁的第一个节点。如果本身头节点属于重置状态waitStatus==0,并且把它设置为传播状态那么就向下一个节点传播。
12                     unparkSuccessor(h);
13                 }
14                 else if (ws == 0 &&
15                          !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
16                     continue;                // loop on failed CAS
17             }
18             if (h == head)                   // loop if head changed
19                 break;
20         }
21     }



主要参考资料:
Semaphore使用及原理
AQS共享锁模式

  

 

posted on 2021-09-26 21:35  山水爱恋  阅读(986)  评论(0编辑  收藏  举报