Java锁(三):Semaphore共享锁详解
热衷学习,热衷生活!😄
沉淀、分享、成长,让自己和他人都能有所收获!😄
一、基于AQS实现的锁
AQS(AbstractQueuedSynchronizer) 是Java并发包JUC中非常重要的一个类,大部分锁都是基于AQS实现的,主要实现的类如下:
ReentrantLock
:可重入锁,独占锁,实现了公平锁和非公平锁,这个是上篇内容介绍的类,也是最常用类,通常会和synchronized
作比较。ReentrantReadWriteLock
:读写锁,可共享也可独占锁,读是共享锁,写是独占锁,也实现了公平锁和非公平锁。Semaphore
:信号锁,共享锁,也实现了公平锁和非公平锁,主要同于控制流量,比如:数据库连接池给你分配10个链接,来一个分配一个,如果10个都分配完了且没有释放那就等待释放。CountDownLatch
:闭锁,共享锁,也实现了公平锁和非公平锁,Latch门闩的意思,比如:说四个人一个漂流艇,坐满了就推下水。
二、Semaphore
Semaphore是什么
上面已经介绍了Semaphore
,基于AQS实现的信号锁,是共享锁,实现了公平锁和非公平锁。可以用来控制同时访问特定资源的线程数,通过协调各个线程以保证合理的使用资源。
Semaphore使用场景
通常用于资源有明确访问数量限制的场景,常用于限流。
比如:数据库连接池,同时进行连接的线程数量有限制,连接不能超过一定的数量,当连接达到了限制的数量后,后面的线程只能排队等待前面的线程释放了数据库链接才能获取数据库链接。
比如:停车场场景,车位数量有限,同时只能听一定数量的车,当停满了之后外面的车只能等里面的车出来才能进去停车。
Semaphore的常用方法
1 | // 从信号锁获取获取一个锁,在获取到锁之前一直处理阻塞状态,除非线程被中断 |
Semaphore实现原理
初始化
Semaphore提供了两个构造方法,默认构造方法创建指定锁数量的非公平信号锁,另外一个构造方法多了一个指定是公平锁还是非公平锁的参数,源码如下:
1 | // 构建指定数量锁的非公平信号锁 |
获取锁过程
acquire
acquire()
是获取锁方法,调用了内部类同步器Sync
继承了AQS
实际调用AQS
的acquireSharedInterruptibly()
核心,源码如下:
1 | public void acquire() throws InterruptedExcetion { |
加JDK中,与锁相关的方法,Interruptibly
表示可中断,也就是可中断锁。可中断锁的意思是线程在等待获取锁的过程中是可以被中断的,换言之,线程在等待锁的过程中可以响应中断。
acquireSharedInterruptiby
acquireSharedInterruptibly
方法是获取可中断锁,源码如下:
1 | public final void acquireSharedInterruptibly(int arg) |
acquireSharedInterruptibly
方法首先会判断当前线程的中断状态如果是中断状态则响应中断,抛异常,然后调用tryAcquireShared()
方法获取锁,如果锁被获取完了就为当前线程创建一个节点加入等待队列。
tryAcquireShared
tryAcquireShared()
是AQS
定义的一个模版方法,具体由子类实现,Semaphore
也实现了公平锁和非公平锁,两种锁大同小异,我们具体来看一下公平锁的具体实现,源码如下:
1 | protected int tryAcquireShared(int acquires) { |
tryAcquireShared()
通过自旋+CAS的方式获取锁和保证线程安全。
doAcquireSharedInterruptibly
doAcquireSharedInterruptibly()
方法在公平锁的时候如果当前线程前面有等待线程或者锁被获取完了之后,当前线程需要进入等待状态时会被调用,用于为当前线程创建节点并加入等待队列,源码如下:
1 | private void doAcquireSharedInterruptibly(int arg) |
获取锁过程总结
- 首先调用
Semaphore.acquire()
方法获取令牌,该方法调用内置同步器Sync.acquireSharedInterruptibly()
方法,该同步器继承AQS
实际调用的是AQS
的acquireSharedInterruptibly()
方法。 acquireSharedInterruptibly()
方法首先判断当前线程是否被中断,如果中断了就抛InterruptedException
异常,如果没有被中单,就调用tryAcquireShared()
方法尝试获取锁。tryAcquireShared()
方法AQS
只是定了一个模版方法由子类实现,Semaphore.Sync
同步器提供了两种实现,分别是FairSync
(公平锁)和NonfairSync
(非公平锁),这两种锁实现差不多,公平锁就是多一个hasQueuedPredecessor()
方法判断是否有排在自己前面的线程,如果有则返回-1。tryAcquireShared()
方法通过自旋或许锁,先获取当前可用锁,减去需要获取的锁获取到剩余锁,如果剩余锁小于0直接返回,表示获取失败,否则通过CAS去获取锁,成功获取返回成功,失败获取返回失败。tryAcquireShared()
如果获取锁失败,就要调用doAcquireSharedInterruptibly()
方法用于为当前线程创建节点加入等待队列,该方法首先创建共享模式的节点加入队列,然后自旋,判断当前节点是不是头节点,如果是头节点也尝试获取锁,获取成功的话设置头节点并成功返回,如果获取失败则会调用shouldParkAfterFailedAcquire()
判断当前线程是否需要等待,如果需要等待然后调用parkAndCheckInterrupt()
方法阻塞线程并判断线程是否中断,如果中断则抛错,如果没有中断就阻塞再通过自旋获取锁。如果自旋异常退出,则调用cancelAcquire()
方法取消线程获取锁。
释放锁过程
release
Semaphore.relese()
方法用于释放锁,释放一个锁,源码如下:
1 | public void release(){ |
release()
方法调用Semaphore.Sync
同步器的releaseShared()
方法,该同步器继承与AQS
实际调用的是AQS.releaseShared()
方法。
releaseShared
releaseShared()
方法释放指定数量的共享锁,释放成功之后会唤醒等待队列中的一个线程,源码如下:
1 | public final boolean releaseShared(int arg) { |
tryReleaseShared
tryReleaseShared()
方法是AQS
定义的模版方法由子类实现,调用了Semaphore.tryReleaseShared()
,该方法通过自旋+CAS释放锁,源码如下:
1 | protected final boolean tryReleaseShared(int releases) { |
doReleaseShared
doReleaseShared()
方法用于释放锁成功之后唤醒等待队列中的线程,源码如下:
1 | private void doReleaseShared() { |
释放锁过程总结
- 首先调用
Semaphore.release()
方法释放锁,该方法调用同步器Sync
的releasShared()
方法,因为同步器继承与AQS
所以实际调用的是AQS.releaseShared()
方法。 AQS.releaseShared()
方法首先调用tryReleaseShared()
方法尝试释放锁,该方法是AQS
定义的模版方法,通过子类实现,调用的是Semaphore.tryReleaseShared()
方法。Semaphore.tryReleaseShared()
方法通过自旋+CAS释放锁,先获取当前的锁数量加上释放的锁数量,会判断超过判断,然后通过CAS修改锁的数量达到释放锁的目的。tryReleaseShared()
释放锁成功之后,会调用AQS.doReleaseShared()
唤醒等待队列中的线程。AQS.doReleaseShared()
用于释放锁成功之后唤醒等待队列中的线程,也是通过自旋+CAS实现,首先获取头节点h,先判断节点h不为null且不是尾节点,得到节点h的状态,如果状态是Node.SIGNAL
说明这个节点已经要被唤醒应该唤醒下一节点,通过CAS操作设置节点h状态为取消,然后调用unparkSuccessor()
方法唤醒下一节点。如果节点h的状态为0,就设置状态为PROPAGATE
说明下一次应该被无条件传播。如果队列中头节点发生变化就继续循环,没有发生变化就终止循环。
三、使用Semaphor实现停车场提示牌功能
每个停车场入口都有一个提示牌,上面显示着停车场剩余的车位是多少,当剩余车位为0时,则不允许车辆进入停车场,直到停车场有车离开停车场,这是提示牌显示新的剩余车位数。
业务场景
- 停车场容纳量为10。
- 当一辆车进入停车场后,显示牌的剩余车位数响应的减1.
- 每有一辆车驶出停车场后,显示牌的剩余车位数响应的加1。
- 停车场剩余车位不足时,车辆只能在外面等待。
代码实现
1 | public class SemaphoreDemo { |
上面代码输出如下:
1 | =====Thread-0车辆来到停车场 |
从上面输出可以看出,当10个车位被停满了之后,再进来的5辆车进入等待状态直到有车驶出停车场,然后再停车,达到了我们预期的效果。