前言
Semaphore
的官方注释如下。
计数信号量。从概念上讲,信号量维护一组许可证(permits)。通常,每次调用
Semaphore#acquire
方法时如果已经没有许可证,则会阻塞线程,直到获取到许可证。每次调用Semaphore#release
方法都会添加一个许可证,此时某个阻塞的线程就会拿到许可证并解除阻塞状态。
Semaphore
在初始化时需要指定许可证数,Semaphore#acquire
方法会尝试获取一定数量的许可证,若许可证数量不足,则当前线程进入阻塞状态。相应地,获取到了许可证的线程,在执行完毕后,需要调用Semaphore#release
方法来归还许可证。有以下两点需要注意。
- 建议在
finally
代码块中调用Semaphore#release
方法,确保许可证的归还; - 没有拿到许可证的线程,也可以调用
Semaphore#release
方法。故需要在业务代码层面保证Semaphore
的正确使用。
正文
一. api
整理
Semaphore
的常用api
如下表所示。
api | 说明 |
---|---|
Semaphore(int permits) | 构造函数。permits指定初始许可证的数量,此时Semaphore 的工作方式是非公平锁的方式 |
Semaphore(int permits, boolean fair) | 构造函数。permits指定初始许可证的数量,fair指定Semaphore 是公平锁还是非公平锁的工作方式 |
void acquire() | 获取许可证。默认获取1 张许可证,获取失败则当前线程进入阻塞状态,且响应中断 |
void acquire(int permits) | 获取许可证。permits指定需要获取的许可证数量,获取失败则当前线程进入阻塞状态,且响应中断 |
void release() | 归还许可证。默认归还1 张许可证 |
void release(int permits) | 归还许可证。permits指定归还许可证的数量 |
boolean tryAcquire() | 尝试获取许可证。获取成功返回true,获取失败返回false。就算Semaphore 的工作方式是公平锁方式,但是该方法在被调用的那一刻,也是会以非公平锁 的方式尝试去获取许可证 |
boolean tryAcquire(long timeout, TimeUnit unit) | 尝试在一段时间内获取许可证。获取成功返回true,获取失败返回false。该方法会尝试获取许可证,如果没有许可证,则等待timeout的时间,等待期间响应中断 ,如果等待时间到,也没有获取到许可证,则返回false |
有一点需要注意,就是无论Semaphore
是公平锁
还是非公平锁
的工作方式,tryAcquire()
方法会在调用的那一刻,以非公平锁
的方式(就是不管是否已经有其它线程正在等待获取许可证)去获取许可证。如果想要tryAcquire()
方法以公平锁
的方式去获取许可证,则方法如下。
- 将
Semaphore
设置为公平锁
的工作方式; - 调用
tryAcquire(0, TimeUnit.SECONDS)
方法。
二. 公平锁方式的Semaphore
的简单使用
公平锁方式的Semaphore
能够保证在没有许可证时,最先等待获取许可证的线程一定能够在有许可证时最先获取到许可证。示例如下所示。
public class SemaphoreTest {
@Test
public void 公平锁方式的简单使用() throws Exception {
// 创建Semaphore对象,且指定公平锁的工作方式
Semaphore semaphore = new Semaphore(1, true);
// 创建一个简单任务
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 执行了");
} catch (InterruptedException e) {
// ignore
} finally {
semaphore.release();
}
}
};
// 创建多个线程来执行同一个任务
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable, "线程" + i);
thread.start();
// 放弃时间片,确保上一线程已经拿到许可证或者已经进入阻塞状态
for (int k = 0; k < 100; k++) {
Thread.yield();
}
}
// 主线程睡眠1秒,以便观察现象
Thread.sleep(1000);
}
}
上述示例演示了在某一刻,同步队列中有等待获取许可证的线程,以及有正在调用acquire()
方法获取许可证的线程,此时一定是同步队列中等待获取许可证最久的线程能够获取到许可证。运行测试程序,结果如下。
三. 非公平锁方式的Semaphore
的简单使用
非公平锁方式的Semaphore
不能够保证在没有许可证时,最先等待获取许可证的线程一定能够在有许可证时最先获取到许可证。示例如下所示。
public class SemaphoreTest {
@Test
public void 非公平锁方式的简单使用() throws Exception {
// 创建Semaphore对象,且指定非公平锁的工作方式
Semaphore semaphore = new Semaphore(1, false);
// 创建一个简单任务
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 执行了");
} catch (InterruptedException e) {
// ignore
} finally {
semaphore.release();
}
}
};
// 创建多个线程来执行同一个任务
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable, "线程" + i);
thread.start();
// 放弃时间片,确保上一线程已经拿到许可证或者已经进入阻塞状态
for (int k = 0; k < 100; k++) {
Thread.yield();
}
}
// 主线程睡眠1秒,以便观察现象
Thread.sleep(1000);
}
}
上述示例演示了在某一刻,同步队列中有等待获取许可证的线程,以及有正在调用acquire()
方法获取许可证的线程,此时会出现调用acquire()
方法的线程比同步队列中等待最久的线程先获取到许可证的情况。运行测试程序,结果如下。
总结
关于Semaphore
的使用,总结如下。
Semaphore
有两种工作方式,第一种是公平锁
的工作方式,第二种是非公平锁
的工作方式;- 初始化
Semaphore
时需要指定初始许可证的数量permits,这个数量可以为负数,但是如果初始许可证数量指定为负数,那么需要保证在acquire()
方法调用前先调用release()
方法来填充许可证数量; - 调用
Semaphore#acquire
方法能够获取许可证,获取到许可证的线程会从Semaphore#acquire
方法立即返回,从而可以继续往下执行,而获取失败的线程就会阻塞在Semaphore#acquire
方法上,直到获取到许可证,或者线程被中断; - 调用
Semaphore#release
方法能够归还许可证,此时如果有线程正在等待获取许可证,那么其中一个线程能够获取到许可证并从等待中返回; Semaphore#release
方法调用时,并不要求线程已经获取到许可证;Semaphore
还提供了tryAcquire()
方法来让线程在获取许可证失败时不进入阻塞状态而是直接返回false。