
Semaphore:实现一个限流器
Semaphore:实现一个限流器
Semaphore 现在普遍翻译成 "信号量",从概念上讲信号量维护着一组 "凭证",获取到凭证的线程才能访问资源,使用完成后释放, 我们可以使用信号量来限制访问特定资源的并发线程数。
就像现实生活中的停车场车位,当有空位的时候才能放车子进入,不然就只能等待,出来的车子则释放凭证。
信号量模型
可以简单的概括为:一个计数器、一个等待队列、三个方法。 在信号量模型里,计数器和等待队列对外是透明的,只能通过信号量模型提供的三个方法访问它们,init()、acquire()、release()。
● init(): 设置计数器的初始值,初始化凭证数量。可以理解为停车场的车位数量。
● acquire():计数器的值减 1 ;如果此时计数器的值小于 0,则当前线程将被阻塞,放到等待队列之中,否则当前线程可以继续执行。
● release():计数器值加 1;如果此时计数器的值小于或者等于 0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。
这里提到的 init()、acquire()、release() 三个方法都是原子性的,并且这个原子性是由信号量模型的实现方保证的。在 Java SDK 里面,信号量模型是由 java.util.concurrent.Semaphore 实现的,Semaphore 这个类能够保证这三个方法都是原子操作。
通过一个简化版的信号模型代码便于理解:
信号量的使用
通过上文我们了解到信号量模型原理,接下来则看如何在实际场景中使用。这里我们还是用累加器的例子来说明信号量的使用吧。在累加器的例子里面,count++ 操作是个临界区,只允许一个线程执行,也就是说要保证互斥。
我们来分析下信号量如何保证互斥的。
假设两个线程 T1 和 T2 同时访问 addOne(),当他们都调用semaphore.acquire();的时候,由于这是一个原子操作,所以只有一个线程能把信号量计数器减为 0,另外一个线程 T2 则是将计数器减为 -1。对应线程 T1 计数器的值为 0 ,满足大于等于 0,所以线程 T1 会继续执行;对于线程 T2,信号量计数器的值为 -1,小于 0 ,按照我们之前的信号量模型 acquire()描述,线程 T2 将被阻塞进入等待队列。所以此刻只有线程 T1 进入临界区执行 count++。
当前信号量计数器的值为 -1 ,当线程 T1 执行 semaphore.release()操作执行完后 计数器 +1 则变成了 0,满足小于等于 0,按照模型的定义,此刻等待队列中的 T2 将会被唤醒,于是 T2 在 T1 执行完临界区代码后才获得进入代码临界区的机会,从而保证了互斥性。
实现一个限流器
上面的例子我们利用信号量实现了一个简单的互斥锁,你会不会觉得奇怪,既然 Java SDK 里面提供了 Lock,为啥还要提供一个 Semaphore ?其实 Semaphore 还有一个功能是 Lock 不容易实现的,那就是:Semaphore 可以允许多个线程访问一个临界区。
常见的就是池化资源,比如连接池、对象池、线程池等。比如熟悉的数据库连接池,在同一时刻允许多个线程同时使用连接,当然在每个连接被释放之前,是允许其他线程使用的。
现在我们假设有一个场景,对象池需求,就是一次性创建 N 个对象,之后所有的线程都复用这 N 个对象,在对象被释放前,是不允许其他线程使用。
初始化线程池大小 2 ,我们模拟 10 个线程,每次只能两个线程分配对象 InputSaleMapDO。
执行完回调函数之后,它们就会释放对象(这个释放工作是通过 pool.add(obj) 实现的),同时调用 release() 方法来更新信号量的计数器。如果此时信号量里计数器的值小于等于 0,那么说明有线程在等待,此时会自动唤醒等待的线程。
思考
上面的例子中 保存对象池使用了 ArrayBlockingQueue ,是一个线程安全的容器,那么是否可以换成 ArrayList?欢迎留言给出答案。还有假设是停车场的车位作为对象池,车主停车是不是也可以使用 Semaphore 实现?
文章转载自公众号:码哥字节
