写点什么

并发王者课 - 铂金 4:令行禁止 - 为何说信号量是线程间的同步利器

用户头像
秦二爷
关注
发布于: 2 小时前

欢迎来到《并发王者课》,本文是该系列文章中的第 17 篇


在并发编程中,信号量是线程同步的重要工具。在本文中,我将带你认识信号量的概念、用法、种类以及 Java 中的信号量。


信号量(Semaphore) 是线程间的同步结构,主要用于多线程协作时的信号传递,以及对共享资源的保护、防止竞态的发生等。信号量这一概念听起来比较抽象,然而读完本文你会发现它竟然也是如此通俗易懂且挺有用。

一、认识简单的信号量

虽然信号量的概念很抽象,但理解起来可以很简单。比如下面这幅图,在峡谷对局中,大乔使用大招向哪吒发起了救援,而哪吒在接收到求救信号后前往救援



在救援的过程中,信号无疑是关键的。如果把大乔和哪吒看作两个线程,那么他们在求救、救援过程中的信号就可以看作是信号量用于线程间的同步和通信


接下来,我们写一个简单的信号量,模拟还原刚才的求救和施救的过程。


定义一个求救的信号量,里面包含信号信号发送信号接收


// 求救信号public class ForHelpSemaphore {    private boolean signal = false;
public synchronized void sendSignal() { this.signal = true; this.notify(); System.out.println("呼救信号已经发送!"); }
public synchronized void receiveSignal() throws InterruptedException { System.out.println("已经就绪,等待求救信号..."); while (!this.signal) { wait(); } this.signal = false; System.out.println("求救信号已经收到,正在前往救援!"); }}
复制代码


再创建两个线程,分别代表大乔和哪吒。


 public static void main(String[] args) {   ForHelpSemaphore helpSemaphore = new ForHelpSemaphore();
Thread 大乔 = new Thread(helpSemaphore::sendSignal); Thread 哪吒 = new Thread(() -> { try { helpSemaphore.receiveSignal(); } catch (InterruptedException e) { e.printStackTrace(); } });
大乔.start(); 哪吒.start(); }
复制代码


从运行结果中可以看到,他们通过信号量的机制完成了救援行动。


你看,最简单的信号量就是这样的简单。

二、理解宽泛意义上的的信号量

如果把上面大乔和哪吒救援的例子做个梳理的话,可以发展信号量中的一些关键信息:


  • 共享的资源。比如signal字段是两个线程共享的,它是两个线程协同的基础;

  • 多个线程访问相同的共享资源,并根据资源状态采取行动。比如大乔和哪吒都会读写signal字段,然后采取行动。


基于上面的两点理解,我们可以把信号量抽象为下面这张图所示:



从图中可以看到,多个线程共享一份资源列表,但是资源是有限的。所以,线程之间必然要按照一定的顺序有序地访问资源,并在访问结束后释放资源。没有获得资源的线程,只能等待其他线程释放资源后再次尝试获取


多线程对共享资源的访问过程,也可以用下面这张流程图表示:



如果你能把这两幅图理解了,那么你也就把信号量的机制理解了。而一旦理解了机制,所谓的源码不过只是某种具体的实现。

三、认识不同类型的信号量

根据信号量的机制和应用场景,一般有下面几种不同类型的信号量。

1. 计数型信号量

public class CountingSemaphore {  private int signals = 0;  public synchronized void take() {    this.signals++;    this.notify();  }  public synchronized void release() throws InterruptedException {    while (this.signals == 0)      wait();    This.signals--;  }}
复制代码

2. 边界型信号量

在计数型信号量中,信号的数量是没有限制的。换句话说,所有的线程都可以发送信号。与此不同的是,在边界型信号量中,通过bound字段增加了信号量的限制。


public class BoundedSemaphore {  private int signal = 0;  private int bound = 0;
public BoundedSemaphore(int upperBound) { this.bound = upperBound; } public void synchronized take() throws InterruptedException { while (this.signal == bound) wait(); this.signal++; this.notify++; } public void synchronized release() throws InterruptedException { while (this.signal == 0) wait(); this.signal--; }}
复制代码

3. 定时型信号量

**定时型(timed)**信号量指的是允许线程在指定的时间周期内才能执行任务。时间周期结束后,定时器将会重置,所有的许可也都会被回收。

4. 二进制型信号量

二进制信号量和计数型信号量类似,但许可的值只有 0 和 1 两种。实现二进制型信号量相对也是比较容易的,如果是 1 就是成功,否则是 0 就是失败。

四、Java 中的信号量

在理解了信号量机制并且也理解它很有用之后,先不用着急实现它。在 Java 中,已经提供了相应的信号量工具类,即java.util.concurrent.Semaphore。并且,Java 中的信号量实现已经比较全面,你不需要再重写它。

1. Semaphore 的核心构造

Semaphore 类有两个核心构造:


  1. Semaphore(int num)

  2. Semaphore(int num, boolean fair)


其中,num表示的是允许访问共享资源的线程数量,而布尔类型的fair则表示线程等待时是否需要考虑公平。

2. Semaphore 的核心方法

  1. acquire(): 获取许可,如果当前没有可用的许可,将进入阻塞等待状态;

  2. tryAcquire():尝试获取许可,无论有没有可用的许可,都会立即返回;

  3. release(): 释放许可;

  4. availablePermits():返回可用的许可数量。

五、如何通过信号量实现锁的能力

在上面的示例中,由于信号量可以用于保护多线程对共享资源的访问,所以直觉你可能会觉得它像一把锁,而事实上信号量确实可以用于实现锁的能力。


比如,借助于边界信号量,我们把线程访问的上线设置为 1,那么此时将只有 1 个线程可以访问共享资源,而这不就是锁的能力嘛!


下面是通过信号量实现锁的一个示例:


BoundedSemaphore semaphore = new BoundedSemaphore(1);...semaphore.take();try {  //临界区} finally {  semaphore.release();}
复制代码


我们把信号量中的信号数量上限设置为 1,代码中的take()就相当于lock(),而release()则相当于unlock()。如此,信号量摇身一变就成了名副其实的锁

小结

以上就是关于信号量的全部内容。在本文中,我们介绍了信号量的概念、运行机制、信号量的几种类型、Java 中的信号量实现,以及如果通过信号量实现一把锁。


理解信号量的关键在于理解它的概念,也就是它所要解决的问题和它的方案。在理解概念和机制之后,再去看 Java 中的源码时,就会发现原来如此,又是队列...


正文到此结束,恭喜你又上了一颗星✨


夫子的试炼


  • 基于对信号量的理解,尝试自己实现一个简单的信号量。


延伸阅读与参考资料



关于作者


关注公众号【庸人技术笑谈】,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶尔也聊聊生活和理想。不贩卖焦虑,不做标题党。


如果本文对你有帮助,欢迎点赞关注监督,我们一起从青铜到王者

发布于: 2 小时前阅读数: 3
用户头像

秦二爷

关注

微信公众号:【庸人技术笑谈】 2018.05.13 加入

记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶尔也聊聊生活和理想。不贩卖焦虑,不兜售课程。

评论

发布
暂无评论
并发王者课-铂金4:令行禁止-为何说信号量是线程间的同步利器