引言
本篇博客讲解《Java并发编程实战》中的同步工具类:信号量 的使用和理解。
从概念、含义入手,突出重点,配以代码实例及讲解,并以生活中的案例做类比加强记忆。
什么是信号量
Java中的同步工具类信号量即计数信号量(Counting Semaphore),是用来控制访问某个特定资源的操作数量,或同时执行某个指定操作的数量。可以简单理解为信号量用来限制对某个资源的某种操作的数量。
一般用于实现某种资源池,或对容器施加边界。
信号量管理着一组有限个数的虚拟许可(permit),而许可的数量就是限制特定操作数量的关键。
信号量的使用
前面已经说过,信号量一般用于实现某种资源池或对容器施加边界,这都是一个对特定操作的限制用途。那么想象一下,如何限制操作的数量,达到为一个再普通不过的容器施加边界的效果呢?答案是给容器的某种操作(可以是添加或删除元素,应该广义的理解“某种操作”这个关键字眼)增加一道执行许可,只有在获得许可的情况下才可以执行这个操作:
上图左边是普通的对容器的操作,右边是有了信号量的对容器的操作。可以看出,在增加了中间的信号量之后,对容器的操作将会受限。
Semaphore
了解了信号量的大概含义,那么进一步深入到Java类库的层面,JDK为开发者提供了java.util.concurrent包下的Semaphore类,它的含义就是上面所述的信号量,管理着一组permit。
以“为容器施加边界”这一信号量用途为例。首先我们要明确一点,使用信号量的方式来实现施加边界的方式,其针对的是操作而不是容器的容量!再一次重申,是限制了操作,而不是容器的容量!
强调限制操作,是为了要明白一点:使用信号量来施加边界,必然会对这个容器的某些操作进一步封装。比如添加方法,就会在调用add之前先行调用Semaphore对象的acquire()方法,在与这个操作相反的操作中去release()。并且,acquire()方法是阻塞式的,这就代表没有闲置许可的时候,操作将会阻塞直到有许可被释放。
下面代码用信号量来对HashSet这个最普通的容器来施加一个添加限制,进一步封装,使其成为一个有界的阻塞式的容器。
public class BoundedHashSet<T> {private final Set<T> set;private final Semaphore sem;public BoundedHashSet(int bound) {this.set = Collections.synchronizedSet(new HashSet<>());this.sem = new Semaphore(bound);}public boolean add(T o) throws InterruptedException {sem.acquire();boolean wasAdded = false;try {wasAdded = set.add(o);return wasAdded;} finally {if (!wasAdded)sem.release();}}public boolean remove(Object o) {boolean wasRemoved = set.remove(o);if (wasRemoved)sem.release();return wasRemoved;}/** 只是为了方便打印的 */public void print() {System.out.print(Thread.currentThread().getName() + " : ");this.set.forEach(o -> System.out.print(o + " "));}/** 用于测试的主方法 */public static void main(String[] args) {BoundedHashSet<String> names = new BoundedHashSet<>(5);new Thread(() -> {for (int i = 0; i < 100; i++) {try {names.add("name" + i);names.print();System.out.println();TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}}}, "TH-ADD").start();new Thread(() -> {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + "--------执行清理,删除name" + i);names.remove("name" + i);try {TimeUnit.SECONDS.sleep(10);} catch (Exception e) {e.printStackTrace();}}},"TH-REMOVE").start();}
}
执行结果如下:
我们用一个线程为这个“有界”容器每隔1秒钟添加一个元素,然后另一个线程每隔10秒钟移除一个元素。且初始化了这个容器的信号量为5,那么当容器中添加元素的数量达到5之后,5个许可全部被占用,添加操作将进入阻塞状态,直到remove的时候释放一个许可,才可以继续添加元素。从上述结果可以看出两点:
1、拥有5个许可的信号量成功的限制了容器的元素个数(即为容器施加了一个边界);
2、添加的操作在没有获得许可的情况下将进入阻塞状态,在执行的过程中也恰恰印证了这一点:当remove执行并release()之后,添加操作会立刻执行。
生活中的类比
其实这个类比博主认为,从严谨的角度来讲,并不是完全符合信号量的概念,但是我们可以类比的同时找出不同点,不仅有效的通过生活案例理解了信号量,还对与之不同的地方增加了深刻的印象,所以还是决定拿出来供大家参考。
上过学的同学可能都知道,学校有奖学金制度。虽然我没怎么得过奖学金,但是大概的逻辑还是比较好理解。
学校的奖学金制度是怎样的呢?
学校每年都会给全校的学生指定数量的全额奖学金名额,比如全额奖学金5名。那么如果想获得全额奖学金,就必须先获得名额才行。
从这个简单的逻辑我们可以找出关键的与信号量中的概念相匹配的内容:
奖学金 = 特定资源
获得(奖学金) = 指定操作(如remove操作)
名额 = 一组定额许可的信号量
名额已满,来年再报 = 操作阻塞,等待释放许可
有了上面的等式,信号量的神秘面纱就算彻底被我们揭开了,原来它就是一个管理一组定额许可的通行证,要想执行操作,那就必须先得到许可,否则就阻塞。
总结
信号量的概念:限制操作数量。
一个类:Semaphore ,两个方法:acquire()、release()。
用途:对容器施加边界,对容器的操作的再封装。
另外,奖学金和信号量之间的类比并不完全匹配,不过这种程度的类比已经相当清晰,至于哪些信息有所差异,留给各位看官自己去挖掘。如果有什么新的发现,真诚希望在博客下方留言。
愿所有热爱编程的开发者共同进步!