聊聊Semaphore 信号量源码分析

开发 项目管理
Semaphore 信号量, 信号量维护了一组许可。如果有必要每个采集模块都会阻塞,直到有许可可用。然后获取许可证。每次发布都会添加一个许可证,可能会释放一个阻塞资源。

[[422207]]

本文转载自微信公众号「运维开发故事」,作者老郑。转载本文请联系运维开发故事公众号。

概述

Semaphore 信号量, 信号量维护了一组许可。如果有必要每个采集模块都会阻塞,直到有许可可用。然后获取许可证。每次发布都会添加一个许可证,可能会释放一个阻塞资源。但是,没有使用实际的许可对象;信号量可用数量的计数,并且进行操作。 信号量通常可以用于限制访问某些(物理或者逻辑)资源的线程数。例如下面是一个使用信号量控制对线程池访问。

  1. class Pool { 
  2.   private static final int MAX_AVAILABLE = 100; 
  3.   private final Semaphore available = new Semaphore(MAX_AVAILABLE, true); 
  4.  
  5.   public Object getItem() throws InterruptedException { 
  6.     available.acquire(); 
  7.     return getNextAvailableItem(); 
  8.   } 
  9.  
  10.   public void putItem(Object x) { 
  11.     if (markAsUnused(x)) 
  12.       available.release(); 
  13.   } 
  14.  
  15.   // Not a particularly efficient data structure; just for demo 
  16.  
  17.   protected Object[] items = ... whatever kinds of items being managed 
  18.   protected boolean[] used = new boolean[MAX_AVAILABLE]; 
  19.  
  20.   protected synchronized Object getNextAvailableItem() { 
  21.     for (int i = 0; i < MAX_AVAILABLE; ++i) { 
  22.       if (!used[i]) { 
  23.          used[i] = true
  24.          return items[i]; 
  25.       } 
  26.     } 
  27.     return null; // not reached 
  28.   } 
  29.  
  30.   protected synchronized boolean markAsUnused(Object item) { 
  31.     for (int i = 0; i < MAX_AVAILABLE; ++i) { 
  32.       if (item == items[i]) { 
  33.          if (used[i]) { 
  34.            used[i] = false
  35.            return true
  36.          } else 
  37.            return false
  38.       } 
  39.     } 
  40.     return false
  41.   } 

在获取项目之前,每个线程必须从信号量获取一个许可证,以确保项目可用。当线程处理完该项后,它将返回到池中,并向信号量返回一个许可证,允许另一个线程获取该项。请注意,在调用acquire时不会保持同步锁,因为这会阻止项目返回池。信号量封装了限制对池的访问所需的同步,与维护池本身一致性所需的任何同步分开。

初始化为1的信号量,其使用方式是最多只有一个可用的许可证,可以用作互斥锁。这通常被称为二进制信号量,因为它只有两个状态:一个许可证可用,或者零个许可证可用。以这种方式使用时,二进制信号量的属性(与许多java.util.concurrent.locks.Lock实现不同)是“锁”可以由所有者以外的线程释放(因为信号量没有所有权的概念)。这在某些特定的上下文中非常有用,例如死锁恢复。

此类的构造函数可以选择接受公平性参数。当设置为false时,此类不保证线程获取许可的顺序。特别是,允许bargging,也就是说,调用acquire的线程可以在一直在等待的线程之前分配一个许可证-从逻辑上讲,新线程将自己置于等待线程队列的头部。当公平性设置为true时,信号量保证选择调用任何acquire方法的线程,以按照其调用这些方法的处理顺序(先进先出;先进先出)。请注意,FIFO排序必然适用于这些方法中的特定内部执行点。因此,一个线程可以在另一个线程之前调用acquire,但在另一个线程之后到达排序点,类似地,从方法返回时也是如此。还请注意,untimed tryAcquire方法不支持公平性设置,但将接受任何可用的许可。

通常,用于控制资源访问的信号量应该初始化为公平,以确保没有线程因访问资源而耗尽。当将信号量用于其他类型的同步控制时,非公平排序的吞吐量优势往往超过公平性考虑。

此类还提供了方便的方法,可以一次获取和发布多个许可证。当使用这些方法时,如果没有将公平设置为真,则要小心无限期延迟的风险增加。

内存一致性影响:在调用“release”方法(如release())之前的线程中的操作发生在另一个线程中成功的“acquire”方法(如acquire()之后的操作)之前。

原理分析

Semaphore 信号量,是控制并发的有效手段。它底层通过 AQS 实现。如下图所示:

构造方法

Semaphore 构造方法有两个 Semaphore(int permits) 和 Semaphore(int permits, boolean fair) 后者有两个参数:第一个参数是许可数量初始化,第二个参数定义信号量是否公平锁同步(默认为非公平)。

  1. public Semaphore(int permits) { 
  2.     sync = new NonfairSync(permits); 
  3.  
  4.  
  5. public Semaphore(int permits, boolean fair) { 
  6.     sync = fair ? new FairSync(permits) : new NonfairSync(permits); 

acquire 方法

acquire 方法可以为理解获取许可,如果存在剩余许可那么就可以进入后续代码块,如果没有获取线程进入阻塞。在共享模式下获取,如果中断将中止。通过首先检查中断状态,然后调用至少一次tryAcquireShared,并在成功时返回来实现。否则线程将排队,可能会重复阻塞和取消阻塞,调用tryAcquireShared,直到成功或线程中断。

release 方法

acquire 方法可以为理解释放许可,其他等待许可的线程进入资源竞争阶段。然后去查找等待队列队头有效的等待节点进行唤醒。

整体流程

Semaphore 信号量原理.png

举个例子

场景描述

对于控制流量,或者控制并发我们可以使用 Semaphore 信号量来完成。例子:有100 个人需要过桥,但是桥上最多同时能够承受 5 个人的重量。如果我们需要有序的过桥那么就可以采用信号量的方式来控制。

初始化 5 个许可。

上桥之前先去获取 许可,如果有剩余许可就上桥。

如果没有 许可,就等待许可。

image.png

模拟代码

首先定义桥对象,入下所示:

  1. public class Bridge { 
  2.  
  3.     private String name
  4.  
  5.     private String address; 
  6.  
  7.     private Integer max
  8.  
  9.     public String getName() { 
  10.         return name
  11.     } 
  12.  
  13.     public void setName(String name) { 
  14.         this.name = name
  15.     } 
  16.  
  17.     public String getAddress() { 
  18.         return address; 
  19.     } 
  20.  
  21.     public void setAddress(String address) { 
  22.         this.address = address; 
  23.     } 
  24.  
  25.     public Integer getMax() { 
  26.         return max
  27.     } 
  28.  
  29.     public void setMax(Integer max) { 
  30.         this.max = max
  31.     } 

然后定义迁徙者对象,就是过桥的人,然后他有个动作就是过桥。代码如下所示。

  1. public class Migrator { 
  2.  
  3.     private String name
  4.  
  5.     public void gapBridge() { 
  6.         System.out.println("Migrator: " + this.name + ", time:" + System.currentTimeMillis()); 
  7.     } 
  8.  
  9.     public String getName() { 
  10.         return name
  11.     } 
  12.  
  13.     public void setName(String name) { 
  14.         this.name = name
  15.     } 

调用代码如下:

  1. public class MainTest { 
  2.  
  3.     public static void main(String[] args) { 
  4.         Bridge bridge = new Bridge(); 
  5.         bridge.setAddress("云南"); 
  6.         bridge.setName("XX 桥"); 
  7.         bridge.setMax(5); 
  8.  
  9.         Semaphore semaphore = new Semaphore(bridge.getMax()); 
  10.         for (int i=0; i< 100; i++) { 
  11.             int idx = i; 
  12.             new Thread(()-> { 
  13.                 try { 
  14.                     Migrator migrator = new Migrator(); 
  15.                     migrator.setName("name-" + idx); 
  16.                     semaphore.acquire(); 
  17.                     TimeUnit.SECONDS.sleep(1); 
  18.                     migrator.gapBridge(); 
  19.                     System.out.println("name " + migrator.getName() + " 通过"); 
  20.                 } catch (InterruptedException e) { 
  21.                     e.printStackTrace(); 
  22.                 } finally { 
  23.                     semaphore.release(); 
  24.                 } 
  25.             }).start(); 
  26.         } 
  27.     } 

输出日志如下:我们可以看到刚开始的时候有 5 个线程获取到 "许可" 几乎同时过桥,后面逐渐就是释放一个许可,另外一个线程继续执行。

  1. Migrator: name-7, time:1630495912011 
  2. name name-7 通过 
  3. Migrator: name-2, time:1630495912011 
  4. name name-2 通过 
  5. Migrator: name-4, time:1630495912011 
  6. Migrator: name-8, time:1630495912011 
  7. Migrator: name-3, time:1630495912011 
  8. name name-3 通过 
  9. name name-8 通过 
  10. name name-4 通过 
  11. Migrator: name-5, time:1630495913012 
  12. name name-5 通过 
  13. Migrator: name-0, time:1630495913012 
  14. name name-0 通过 
  15. Migrator: name-6, time:1630495913013 

参考文档

 

https://www.cnblogs.com/leesf456/p/5414778.html

 

责任编辑:武晓燕 来源: 运维开发故事
相关推荐

2022-04-13 11:12:43

鸿蒙轻内核信号量模块操作系统

2021-05-31 20:30:55

鸿蒙HarmonyOS应用

2020-11-05 09:59:24

Linux内核信号量

2021-04-13 09:20:15

鸿蒙HarmonyOS应用开发

2020-11-10 15:25:26

SemaphoreLinux翻译

2021-04-30 00:00:50

Semaphore信号量面试官

2024-04-10 08:16:20

多线程编程Java并发编程

2010-04-21 16:50:31

Unix信号量

2020-09-25 07:34:40

Linux系统编程信号量

2010-04-21 15:37:38

Unix信号量

2010-04-21 16:42:48

Unix信号量

2010-04-21 16:25:13

Unix信号量

2023-06-02 08:14:58

信号量对象线程

2024-10-29 15:23:45

Python线程安全

2010-04-21 17:10:25

Unix信号量

2009-12-08 12:14:43

2010-03-17 16:36:10

Java信号量模型

2010-07-15 15:32:10

Perl线程

2019-11-19 09:00:38

JavaAND信号量

2023-10-06 23:31:25

可视化Go
点赞
收藏

51CTO技术栈公众号