Java提供了一个类Semaphore来实现信号量,概念上讲,一个信号量相当于持有一些许可(permits),线程可以调用Semaphore对象的acquire()方法获取一个许可,调用release()来归还一个许可
1 构造方法:
Semaphore有两个构造方法 Semaphore(int)
、Semaphore(int,boolean)
,参数中的int表示该信号量拥有的许可数量,boolean表示获取许可的时候是否是公平的,如果是公平的那么,当有多个线程要获取许可时,会按照线程来的先后顺序分配许可,否则,线程获得许可的顺序是不定的。这里在jdk中讲到 “一般而言,非公平时候的吞吐量要高于公平锁”,这是为什么呢?附上链接中的一段话:
非公平锁性能高于公平锁性能的原因:在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。假设线程A持有一个锁,并且线程B请求这个锁。由于锁被A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此B会再次尝试获取这个锁。与此同时,如果线程C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样就是一种双赢的局面:B获得锁的时刻并没有推迟,C更早的获得了锁,并且吞吐量也提高了。当持有锁的时间相对较长或者请求锁的平均时间间隔较长,应该使用公平锁。在这些情况下,插队带来的吞吐量提升(当锁处于可用状态时,线程却还处于被唤醒的过程中)可能不会出现。
2 获取许可
可以使用acquire()、acquire(int)、tryAcquire()
等去获取许可,其中int参数表示一次性要获取几个许可,默认为1个,acquire方法在没有许可的情况下,要获取许可的线程会阻塞,而tryAcquire()方法在没有许可的情况下会立即返回 false,要获取许可的线程不会阻塞,这与Lock类的lock()与tryLock()类似
3 释放许可
线程可调用 release()、release(int)来释放(归还)许可,注意一个线程调用release()之前并不要求一定要调用了acquire (There is no requirement that a thread that releases a permit must have acquired that permit by calling {@link #acquire})
4 使用场景
我们一般使用信号量来限制访问资源的线程数量,比如有一个食堂,最多允许5个人同时吃饭,则如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
结果如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
当我们在构造Semaphore对象时,如果设置的许可数量为1,这时便会达到一个互斥排他锁的效果,只有一个许可,有一个线程获取了这个许可,那么其他线程只有等待这个线程归还了许可才能获取到许可,当将Semaphore用作互斥排他锁的作用时,要注意:
A semaphore initialized to one, and which is used such that it only has at most one permit available, can serve as a mutual exclusion lock. This is more commonly known as a binary semaphore, because it only has two states: one permit available, or zero permits available. When used in this way, the binary semaphore has the property (unlike many Lock implementations), that the “lock” can be released by a thread other than the owner (as semaphores have no notion of ownership). This can be useful in some specialized contexts, such as deadlock recovery.
文档中提到,Semaphore与jdk中的Lock
的区别是
1. 使用Lock.unlock()之前,该线程必须事先持有这个锁(通过Lock.lock()获取),如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
则会抛出异常,因为该线程事先并没有获取lock对象的锁:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
对于Semaphore来讲,如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
结果如下:
- 1
- 2
- 1
- 2
i. 并没有抛出异常,也就是线程在调用release()之前并不要求先调用acquire()
ii. 我们看到可用的许可数目增加了一个,但我们的初衷是保证只有一个许可来达到互斥排他锁的目的,所以这里要注意一下
2 Semaphore(1)可以做到一个deadlock recovery,我们来看下面一个例子
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
在注释最后面几行代码的情况下,结果为,陷入了一个死锁:
- 1
- 2
- 1
- 2
把注释删除,即在主线程释放Semaphore,这样就能解决死锁:
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
这即符合文档中说的,通过一个非owner的线程来实现死锁恢复,但如果你使用的是Lock则做不到,可以把代码中的两个信号量换成两个锁对象试试。很明显,前面也验证过了,要使用Lock.unlock()来释放锁,首先你得拥有这个锁对象,因此非owner线程(事先没有拥有锁)是无法去释放别的线程的锁对象