本文已参与【请查收|你有一次免费申请掘金周边礼物的机会】活动。
前言
有幸申请到掘金送福利活动名额,参与评论就有机会获得掘金官方提供的新版徽章,具体的抽奖细节在文末。
Semaphore简介
Semaphore(信号量)是JUC包下的一个并发工具类,用来控制并发访问临界资源(共享资源)的线程数,确保访问临界资源的线程能够正确、合理的使用公共资源。Semaphore和ReetrantLock一样,都是通过直接或间接的调用AQS框架的方法实现。
Semaphore内部维护了一组虚拟的许可,许可的数量可以通过构造函数的参数指定。访问特定资源前,必须使用acquire方法获得许可,如果许可数量为0,该线程则一直阻塞,直到有可用许可。访问资源后,使用release释放许可。
Semaphore的实现
Semaphore类中所提供的方法:
1 | java复制代码// 调用该方法后线程会从许可集中尝试获取一个许可 |
Semaphore的实现是基于AQS的共享锁,分为公平和非公平两种模式。
Sync实现
1 | java复制代码abstract static class Sync extends AbstractQueuedSynchronizer { |
公平的FairSync实现
1 | java复制代码static final class FairSync extends Sync { |
公平锁的实现与非公平锁的不同点在于:公平锁的模式下获取锁,会先调用hasQueuedPredecessors()方法判断同步队列中是否存在节点,如果存在则直接返回-1回到acquireSharedInterruptibly()方法if(tryAcquireShared(arg)<0)判断,调用doAcquireSharedInterruptibly(arg)方法将当前线程封装成Node.SHARED共享节点加入同步队列等待。如果队列中不存在节点则尝试直接获取锁或许可。
非公平的Sync实现
1 | scala复制代码static final class NonfairSync extends Sync { |
Semaphore中的非公平锁NonfairSync类的构造函数是基于调用父类Sync构造函数完成的,而在创建Semaphore对象时传入的许可数permits最终则会传递给AQS同步器的同步状态标识state,如下:
1 | java复制代码// 父类 - Sync类构造函数 |
Semaphore对象创建时传入的许可数permits,其实最终是在对AQS内部的state进行初始化。初始化完成后,state代表着当前信号量对象的可用许可数。
非公平锁NonfairSync获取许可
1 | java复制代码// Semaphore类 → acquire()方法 |
信号量获取许可的方法acquire()最终是通过Sync对象调用AQS内部的acquireSharedInterruptibly()方法完成的,而acquireSharedInterruptibly()在获取同步状态标识的过程中是可以响应线程中断操作的,如果该操作没有没中断,则首先调用tryAcquireShared(arg)尝试获取一个许可数,获取成功则返回执行业务,方法结束。如果获取失败,则调用doAcquireSharedInterruptibly(arg)将当前线程加入同步队列阻塞等待。tryAcquireShared(arg)方法是AQS提供的方法,没有具体实现,在NonfairSync类中的实现如下:
1 | arduino复制代码 // Semaphore类 → NofairSync内部类 → tryAcquireShared()方法 |
首先获取到state值后,减去一得到remaining值,若不小于0则代表着当前信号量中还有可用许可,当前线程开始尝试cas更新state值,cas成功则代表获取同步状态成功,返回remaining值。反之,如果remaining值小于0则代表着信号量中的许可数已被其他线程获取,目前不存在可用许可数,直接返回小于0的remaining值,nonfairTryAcquireShared(acquires)方法执行结束,回到AQS的acquireSharedInterruptibly()方法。当返回的remaining值小于0时,if(tryAcquireShared(arg)<0)条件成立,进入if执行doAcquireSharedInterruptibly(arg)方法将当前线程加入同步队列阻塞,等待其他线程释放同步状态。
1 | java复制代码// AbstractQueuedSynchronizer类 → doAcquireSharedInterruptibly()方法 |
释放许可
公平锁释放许可的逻辑与非公平锁的实现是一致的,因为都是Sync类的子类,而释放锁的逻辑都是对state减一更新后,唤醒后继节点的线程。所以关于释放锁的具体实现则是交由Sync类实现
1 | arduino复制代码// Semaphore类 → release()方法 |
释放锁则调用的是Semaphore.release()方法,调用该方法之后线程持有的许可会被释放,同时permits/state加一
与之前获取许可的方法一样,Semaphore释放许可的方法release()也是通过间接调用AQS内部的releaseShared(arg)完成。因为AQS的releaseShared(arg)是魔法方法,所以最终的逻辑实现由Semaphore的子类Sync完成,如下:
1 | arduino复制代码// Semaphore类 → Sync子类 → tryReleaseShared()方法 |
对比获取许可的逻辑要简单许多,只需更新state值后调用doReleaseShared()方法唤醒后继节点线程即可。但是调用doReleaseShared()方法的线程会存在两种:
一是释放共享锁/许可数的线程。调用release()方法释放许可时必然调用它唤醒后继线程
二是刚获取到共享锁/许可数的线程。
总结
在初始化时传递的许可数/计数器最终都会间接的传递给AQS的同步状态标识state。当一条线程尝试获取共享锁时,会对state减一,当state为0时代表没有可用共享锁了,其他后续请求的线程会被封装成共享节点加入同步队列等待,直至其他持有共享锁的线程释放(state加一)。与独占模式不同的是:共享模式中,除开释放锁时会唤醒后继节点的线程外,获取共享锁成功的线程也会在满足一定条件下唤醒后继节点。关于AQS具体可参见之前的文章:java锁:AQS详解(一)
java锁:AQS详解(二)
抽奖说明
1.参与评论即可(讨论技术相关内容);
2.抽奖规则:如果本文评论达到掘金活动的要求,热评区前两名分别获赠掘金新版徽章一枚(若无热评将从评论区抽取两位幸运用户);
本文转载自: 掘金