信号量是由大名鼎鼎的计算机科学家迪杰斯特拉(Dijkstra)于 1965 年提出,在这之后的 15 年,信号量一直都是并发编程领域的终结者,直到 1980 年管程被提出来,我们才有了第二选择。目前几乎所有支持并发编程的语言都支持信号量机制,所以学好信号量还是很有必要的。
信号量模型
信号量模型可以简单概括为:一个计数器、一个等待队列、三个方法。在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down()和up()。
- init():设置计数器的初始值;
- down():计数器的值减1;如果此时计数器的值小于0,则当前线程将被阻塞,否则当前线程可以继续执行;
- up():计数器的值加1,如果此时计数器的值小于等等于0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。
这三个方法都是原子性的,并且这个原子性是由信号量模型的实现方保证的。
class Semaphore{
// 计数器
int count;
// 等待队列
Queue queue;
// 初始化操作
Semaphore(int c){
this.count=c;
}
//
void down(){
this.count--;
if(this.count<0){
//将当前线程插入等待队列
//阻塞当前线程
}
}
void up(){
this.count++;
if(this.count<=0) {
//移除等待队列中的某个线程T
//唤醒线程T
}
}
}
在java SDK并发包里,down、up对应的方法是acquice和release方法;
如何使用信号量
对于信号量的使用可以类比红绿灯,十字路口的红绿灯可以控制交通,得益于它的一个关键规则:车辆在通过路口前必须检查是否是绿灯,只有是绿灯才能通行。
public class SemaphoreDemo {
private static Semaphore semaphore = new Semaphore(3);
private static void task(){
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"正在执行任务...");
System.out.println("当前信号量中可进入线程数:" + semaphore.availablePermits());
System.out.println("当前等待线程数:" + semaphore.getQueueLength());
Thread.sleep(1000 * 2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(SemaphoreDemo::task,String.valueOf(i)).start();
}
}
}
实现限流器
public class ObjPool<T,R> {
private List<T> pool;
//使用信号量实现限流器
private final Semaphore semaphore;
ObjPool(T[] tArray){
pool=new Vector<T>(){};
int size=tArray.length;
for(int i=0;i<size;i++){
pool.add(tArray[i]);
}
semaphore=new Semaphore(size);
}
R exec(Function<T,R> func) throws InterruptedException {
T t=null;
semaphore.acquire();
try{
t=pool.remove(0);
return func.apply(t);
}finally {
pool.add(t);
semaphore.release();
}
}
public static void main(String[] args){
String[] messages=new String[10];
for(int i=0;i<10;i++){
messages[i]="obj_"+i;
}
ObjPool<String,String> pool=new ObjPool<>(messages );
for(int i=0;i<100;i++){
Thread thread=new Thread(() ->{
try {
pool.exec(t -> {
System.out.println("当前线程id:"+Thread.currentThread().getId()+",当前获取到的对象:"+t);
return t;
});
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
信号量可以实现的独特功能就是同时允许多个线程进入临界区,但是信号量不能做的就是同时唤醒多个线程去争抢锁,只能唤醒一个阻塞中的线程,而且信号量模型是没有Condition的概念的,即阻塞线程被醒了直接就运行了而不会去检查此时临界条件是否已经不满足了,基于此考虑信号量模型才会设计出只能让一个线程被唤醒,否则就会出现因为缺少Condition检查而带来的线程安全问题。正因为缺失了Condition,所以用信号量来实现阻塞队列就很麻烦,因为要自己实现类似Condition的逻辑。