Semaphore
基本使用
synchronized 可以起到锁的作用,但某个时间段内,只能有一个线程允许执行。我们可以把Semaphore看作一个包含多个许可(permit)的集合,例如一个代表5个许可的Semaphore、6个许可的Semaphore等等(为便于表达,后文用字母P表示许可)。Semaphore上的acuqire操作申请P,而release操作则产生P,Semaphore可用于追踪可用资源的个数。
Semaphore(信号量)用来限制能同时访问共享资源的线程上限,非重入锁
构造方法:
public Semaphore(int permits)
:permits 表示许可线程的数量(state)public Semaphore(int permits, boolean fair)
:fair 表示公平性,如果设为 true,下次执行的线程会是等待最久的线程
常用API:
public void acquire()
:表示获取许可public void release()
:表示释放许可,acquire() 和 release() 方法之间的代码为同步代码
1 | public static void main(String[] args) { |
单机限流用法
Semaphore也可以来实现一个单机限流工具(针对单台机器的线程而言)——即限制同时访问某资源的线程数。实现思路:让1个线程以固定的速度生产P,而让多个线程消费P,这样,消费者线程就能以低于某个上限的速度消费资源,不会导致系统超负荷。
特殊用法
我们可以创建一个只有1个P的Semaphore,即二元信号量。它的功能与锁类似,但是没有所有权的概念。然后,我们可以在一个线程中进行加锁(acquire),而在另一个线程中执行解锁动作(release),并且负责解锁的线程不需要事先获得这个锁。与之相反,ReentrantLock的加锁和解锁动作都必须在同一个线程中完成。
Semaphore的实现原理
首先,Semaphore内部并没有真正保存P,而是只保存了P的个数。其次,Semaphore直接复用了AQS框架的共享模式锁,其acquire和release操作直接调用共享模式的AQS加锁和AQS解锁,没有增加其他逻辑,只不过在加锁和解锁的过程中,是把P的个数存入AQS原子整数。
具体的讲,Semaphore的acquire操作(acquireUninterruptibly操作及tryAcquire操作都与acquire类似)尝试取走1个P,而如果P的个数等于0无法取出就阻塞等待。
acquire
acquire的全部实现就是直接调用AQS类的acquireSharedInterruptibly方法。
1 | // acquire() -> sync.acquireSharedInterruptibly(1),可中断 |
1 | private void doAcquireSharedInterruptibly(int arg) { |
1 | private void setHeadAndPropagate(Node node, int propagate) { |
release
release操作也非常简单,直接调用AQS的releaseShared方法
1 | // release() -> releaseShared() |
- 接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,并且 unpark 接下来的共享状态的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态
面试
问题1.semaphore初始化有10个令牌,11个线程同时各调用1次acquire方法,会发生什么?
答案:拿不到令牌的线程阻塞,不会继续往下运行。
问题2.semaphore初始化有10个令牌,一个线程重复调用11次acquire方法,会发生什么?
答案:线程阻塞,不会继续往下运行。可能你会考虑类似于锁的重入的问题,很好,但是,令牌没有重入的概念。你只要调用一次acquire方法,就需要有一个令牌才能继续运行。
问题3.semaphore初始化有1个令牌,1个线程调用一次acquire方法,然后调用两次release方法,之后另外一个线程调用acquire(2)方法,此线程能够获取到足够的令牌并继续运行吗?
答案:能,原因是release方法会添加令牌,并不会以初始化的大小为准。
问题4.semaphore初始化有2个令牌,一个线程调用1次release方法,然后一次性获取3个令牌,会获取到吗?
答案:能,原因是release会添加令牌,并不会以初始化的大小为准。Semaphore中release方法的调用并没有限制要在acquire后调用。