Java并发编程之AQS(一)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/xu768840497/article/details/79220206
引语
我们会经常见到像ReentrantReadWriteLock、ReentrantLock这样的锁,在JUC中Lock接口和AQS(AbstractQueuedSynchronizer)非常重要。
这里会设计到CAS,如果没有CAS操作的基础,建议先学习CAS操作。


Lock接口相关要实现API




AQS相关要实现API:




AQS提供给程序员调用的API:




AQS提供的模板方法:




Lock和AQS的关系

当我们要实现一个Lock接口的实现类时,需要依赖一个AQS(队列同步器的),因为队列同步器帮我们维护是线程阻塞和释放的逻辑,比如,线程竞争锁时,当一个线程没有竞争到时,要把它丢进一个FIFO的队列,当获取到锁的线程释放锁时,要通知在队列里的线程出来竞争。

如果让你自己维护这种竞争的逻辑,会很麻烦。所以JDK给我们提供了AQS,暴露出来了一些底层的API,让我们去覆盖,然后有一些模板API来调用我们底层的API,并且维护好队列。这样我们就只用把精力放在获得到锁和没有获得到锁的逻辑上了。不用管没有线程获得锁后以及线程释放锁后如何通知其他线程怎么维护。

Lock和AQS的调用关系大致如下。
自定义的锁MyLock实现Lock接口,然后重写了Lock接口的方法。
自定义的MyAQS继承AQS类,然后我们重写tryAcquire、tryRelease和isHeldExclusively等方法。




红色箭头的起始点其实就是AQS提供给我们的模板API,比如我们重写的tryAcquire、tryRelease和isHeldExclusively,我们要实现的API只需要调用AQS提供给我们的state相关API(绿色箭头尾部)来维护是否获取到锁就可以了。

MyLock的API调用AQS的模板方法(右边蓝色箭头尾部),也可以调用MyAQS重写的方法(左边蓝色箭头尾部)来实现加锁、释放锁。



AQS模板API的对应关系



绿色箭头表示独占性锁的实现逻辑,红色箭头表示共享式锁实现的逻辑。
所谓的独占性锁意思就是,只要有一个线程拿到锁,其他线程全部T出去到队列等待。
共享性锁就好理解了,一部分个性化(根据tryAcquire返回值决定)的线程可以拿到锁,没拿到的到队列。



自定义独占锁

  1. package com.anyco.aqs;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.locks.AbstractQueuedSynchronizer;
  4. import java.util.concurrent.locks.Condition;
  5. import java.util.concurrent.locks.Lock;
  6. public class OnlyOneLock implements Lock {
  7. private final OnlyOneLockAQS aqs=new OnlyOneLockAQS();
  8. private static class OnlyOneLockAQS extends AbstractQueuedSynchronizer {
  9. @Override
  10. protected boolean tryAcquire(int arg) {
  11. //状态为0时,设置为1,表示当前线程获取到锁
  12. if(compareAndSetState(0,1)){
  13. //设置当前线程为获取到锁的线程
  14. setExclusiveOwnerThread(Thread.currentThread());
  15. return true ;
  16. }
  17. return false;
  18. }
  19. @Override
  20. protected boolean tryRelease(int arg) {
  21. //如果状态为0,表示还没有线程获取到锁,就没有释放锁的可能
  22. if(getState()==0) {throw new IllegalMonitorStateException();}
  23. setExclusiveOwnerThread(null);
  24. setState(0);
  25. return true;
  26. }
  27. @Override
  28. protected boolean isHeldExclusively() {
  29. //判断该线程是否被占有
  30. return getState()==1; //state为表示锁被占有
  31. }
  32. }
  33. @Override
  34. public void lock() {
  35. aqs.acquire(1); //获取锁成功时,返回,没有成功则进入队列等待,会调用OnlyOneLockAQS重写的tryAcquire
  36. }
  37. @Override
  38. public void lockInterruptibly() throws InterruptedException {
  39. aqs.acquireInterruptibly(1); //带中断检测的获取
  40. }
  41. @Override
  42. public boolean tryLock() {
  43. return aqs.tryAcquire(1);
  44. }
  45. @Override
  46. public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
  47. return aqs.tryAcquireNanos(1,unit.toNanos(time)); //带超时限制的获取锁,指定时间内没有获取达到则返回false
  48. }
  49. @Override
  50. public void unlock() {
  51. aqs.release(1);
  52. }
  53. @Override
  54. public Condition newCondition() {
  55. return null;
  56. }
  57. }

OnlyOneLock中的实现API都是调用了同步器AQS的模板方法和我们实现的方法来实现锁的逻辑的。
我们实现的try开头的那几个API只需要管是否获取到锁的逻辑。
这里强调一下,其实在Lock接口中只有tryLock()这个API会直接调用tryAcquire()这个我们实现的API之外,其他的API其实都是调用的AQS的模板方法,因为模板方法封装了很多复杂的队列通知等逻辑。



自定义共享锁

写共享锁之前,可以想象一个场景。大家应该都知道限流,现在假如有个需求,一个应用级的限流(业务接口层面的),要求一个接口最后只能被5个线程并发访问,后续的线程再访问,直接返回,不做业务逻辑处理.(常用语秒杀业务中某个商品只有5个,那么有必要放很多请求进来吗?)

  1. public class ShareLock implements Lock {
  2. private ShareLockAQS aqs=new ShareLockAQS(5);
  3. private static class ShareLockAQS extends AbstractQueuedSynchronizer {
  4. protected ShareLockAQS(Integer count) { //count表示锁的总数
  5. super();
  6. setState(count);
  7. }
  8. @Override
  9. protected int tryAcquireShared(int arg) { //arg表示请求的锁的数量
  10. for (; ; ) { //for循环
  11. Integer state = getState();
  12. Integer newCount = state - arg; //剩余的可获取锁的数量需要减少
  13. if (newCount < 0 || compareAndSetState(state, newCount)) { //CAS操作,更新剩余的可获取锁的数量
  14. return newCount; //剩余的可获取锁的数量
  15. }
  16. }
  17. }
  18. @Override
  19. protected boolean tryReleaseShared(int arg) {
  20. for (; ; ) {
  21. //注意这里不能直接setState了,因为可能多个线程同时release
  22. Integer state = getState();
  23. Integer newCount = state + arg;
  24. if (compareAndSetState(state,newCount)) { //CAS操作,更新剩余的可获取的锁数量
  25. return true;
  26. }
  27. }
  28. }
  29. @Override
  30. protected boolean isHeldExclusively() {
  31. return getState()==0;
  32. }
  33. }
  34. @Override
  35. public void lock() {
  36. aqs.acquireShared(1);
  37. }
  38. @Override
  39. public void lockInterruptibly() throws InterruptedException {
  40. aqs.acquireInterruptibly(1);
  41. }
  42. @Override
  43. public boolean tryLock() {
  44. return aqs.tryAcquireShared(1)>=0;
  45. }
  46. @Override
  47. public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
  48. return aqs.tryAcquireSharedNanos(1,unit.toNanos(time));
  49. }
  50. @Override
  51. public void unlock() {
  52. aqs.releaseShared(1);
  53. }
  54. @Override
  55. public Condition newCondition() {
  56. return null;
  57. }
  58. }

内部维护了一个state,初始化为5,获取一个锁,减1,释放一个锁,+1。获取锁和释放锁的两个方法我们都用到了for循环。在这里需要强调,务必使释放锁最终成功。


使用ShareLock 来测试

上面我们自定义了一个共享锁ShareLock ,这把锁我们定义的是同一时间只有5个线程可以持有。接下来我们使用这个自定义锁,然后启动10个线程去获取锁。
  1. public class TestShareLock {
  2. private static ShareLock shareLock = new ShareLock(); //new一个共享锁,只能有5个线程可以同时持有
  3. public static void main(String[] args) {
  4. for(int i=0; i<10; i++) { //启动10个线程去获取锁
  5. Thread t = new Thread(new MyLockThread(shareLock));
  6. t.start();
  7. }
  8. }
  9. }
  10. class MyLockThread implements Runnable {
  11. private ShareLock shareLock;
  12. public MyLockThread(ShareLock shareLock) {
  13. this.shareLock = shareLock;
  14. }
  15. @Override
  16. public void run() {
  17. boolean succ = shareLock.tryLock(); //请求锁
  18. System.out.println(succ);
  19. }
  20. }

执行结果:
  1. true
  2. true
  3. true
  4. true
  5. true
  6. false
  7. false
  8. false
  9. false
  10. false

很明显,如果前面5个线程不释放锁,后面的线程都是没有办法获取锁的。
这和我们刚刚讲到的限流有何关系呢?如果我们一个接口最多只能被5个线程并发访问,访问成功就是获取锁成功了,后续的线程再访问因为无法获取锁,所有会访问失败,这样便起到了限流的作用,当然我们还可以在这里加释放锁的逻辑,如果获取锁的线程操作完毕,我们可以让这个线程释放锁,从而让其他的线程能够获取锁。




参考:
https://juejin.im/post/5a3a09d9f265da4312810fb9
https://juejin.im/post/5a3c6aa551882538d3101d5f

展开阅读全文

没有更多推荐了,返回首页