Semaphore-信号量
信号量模型和管程是等价的,信号量的模型更加简单。
- 计数器
- 等待队列
3个方法
- init 初始化 初始化计数器的值
- down计数器的值减 1 ,如果此时计数器的值小于 0,则当前线程将被阻塞,否则当前线程可以继续执行。
- up计数器的值加1,如果此时计数器的值小于 或者等于0 ,则唤醒线程,并从等待队列中将其移除。
什么意思呢?就是说,如果down操作完后,在运行前先判断计数器计数器小于0 了,我们就阻塞,大于0,我们就执行。在up操作完后,再次判断计数器,如果计数器大于0 ,没有阻塞,小于或等于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中的信号量
Java中的信号量是Semaphore,acquire() 就是信号量里的 down() 操作,release() 就是信号量里的 up() 操作。
代码示例:
static int count;//初始化信号量static final Semaphore s= new Semaphore(1);//用信号量保证互斥static void addOne() {s.acquire();try {count+=1;} finally {s.release();}}
限流器
如果仅仅是为了实现锁的功能,lock那一套已经够了,那为什么还要提供Semaphore呢?
Semaphore 可以允许多个线程访问一个临界区
实际问题:
比较常见的需求就是我们工作中遇到的各种池化资源,例如连接池、对象池、线程池等等。其中,你可能最熟悉数据库连接池,在同一时刻,一定是允许多个线程同时使用连接池的,当然,每个连接在被释放前,是不允许其他线程使用的。
实例
在工作中也遇到了一个对象池的需求。所谓对象池呢,指的是一次性创建出 N 个对象,之后所有的线程重复利用这 N 个对象,当然对象在被释放前,也是不允许其他线程使用的。对象池,可以用 List 保存实例对象,这个很简单。但关键是限流器的设计,这里的限流,指的是不允许多于 N 个线程同时进入临界区。那如何快速实现一个这样的限流器呢?这种场景,我立刻就想到了信号量的解决方案。信号量的计数器,在上面的例子中,我们设置成了 1,这个 1 表示只允许一个线程进入临界区,但如果我们把计数器的值设置成对象池里对象的个数 N,就能完美解决对象池的限流问题了。下面就是对象池的示例代码。
class ObjPool<T, R> {final List<T> pool;// 用信号量实现限流器final Semaphore sem;// 构造函数ObjPool(int size, T t){pool = new Vector<T>(){};for(int i=0; i<size; i++){pool.add(t);}sem = new Semaphore(size);}// 利用对象池的对象,调用funcR exec(Function<T,R> func) {T t = null;sem.acquire();try {t = pool.remove(0);return func.apply(t);} finally {pool.add(t);sem.release();}}}// 创建对象池ObjPool<Long, String> pool =new ObjPool<Long, String>(10, 2);// 通过对象池获取t,之后执行pool.exec(t -> {System.out.println(t);return t.toString();});
最后
信号量在 Java 语言里面名气并不算大,但是在其他语言里却是很有知名度的。Java 在并发编程领域走的很快,重点支持的还是管程模型。 管程模型理论上解决了信号量模型的一些不足,主要体现在易用性和工程化方面,例如用信号量解决我们曾经提到过的阻塞队列问题,就比管程模型麻烦很多,你如果感兴趣,可以课下了解和尝试一下。
