并发第六弹 AQS抽象队列同步器

小伙伴们,我来了!好几天没写文章了,最近几天的空余时间里我都在强化自己的并发技术,觉得自己掌握到了一定的精髓,来给大家分享一下!

今天的文章介绍J.U.C下的AQS框架,AQS是Java锁的核心,难度相对较大。

AQS,即AbstractQueuedSynchronizer,抽象队列同步器,是一个Java类,继承自AbstractOwnableSynchronizer。
AbstractOwnableSynchronizer里只有一个属性和它的getter/setter方法:

// 当前持有独占锁的线程
private transient Thread exclusiveOwnerThread;



AQS框架

AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch等。
并发第六弹 AQS抽象队列同步器
AQS维护了一个等待队列,等待队列是CLH(Craig Landin Hagersten)锁双向队列(什么是CLH有兴趣的小伙伴可以自查一下),多线程争用资源被阻塞时会进入此队列
 
AQS有一个内部类Node,队列中的每一个结点正是Node类,Node类有如下字段:

static final Node SHARED = new Node(); // 共享资源标识符
static final Node EXCLUSIVE = null; // 独占资源标识符

// 等待状态常量
static final int CANCELLED = 1; // 表示线程已取消
static final int SIGNAL = -1; // 表示后继线程需要被释放
static final int CONDITION = -2; // 线程在条件队列里面等待
static final int PROPAGATE = -3; // 释放共享资源时需要通知其他节点

// 等待状态
volatile int waitStatus;

// 结点前驱
volatile Node prev;
// 结点后继
volatile Node next;

// 结点的线程
volatile Thread thread;
// 下一个等待者
Node nextWaiter;



AQS用了两个指针来维护队列:

private transient volatile Node head;
private transient volatile Node tail;


注意:

  • head指针指向一个标杆结点(或称哨兵结点),标杆结点也是一个Node对象,但该Node对象不维护任何线程,只是起一个标杆的作用。


 


AQS维护了一个volatile int state(代表共享资源),AQS只是定义了state,但state具体的含义由同步器子类定义,state的访问方式有三种:
  • getState()
  • setState()
  • compareAndSetState()
 
state具体的含义举例:
  • 比如ReentantLock独占锁,state初始化为0,第一个来的线程成功将state从0修改为1,那么第一个线程将持有锁,且该线程可重入,每重入一次state就加1;但重入多少次就要释放(state–)多少次,直到state为0表示第一个线程释放了锁。在第一个线程持有锁期间,由于是独占锁,其它线程只能等待;
  • 再比如CountDownLatch,一开始有n个子线程,state也初始化为n,这n个子线程是并行执行的,且主线程会调用await()方法等待;每有一个线程执行结束,就调用一次countDown()方法,state–;直到n个线程都执行完了,state减为0,会调用unpark()方法唤醒主线程使其从await()方法中返回;
 
AQS定义两种资源共享方式:
  • Exclusive(独占,只有一个线程能执行,如ReentrantLock);
  • Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch);
 

基于AQS的自定义同步器实现准则

不同的自定义同步器争用共享资源的方式不同,自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了
 
自定义同步器实现时主要实现以下几种方法:
  • isHeldExclusively():该线程是否正在独占资源,只有用到condition才需要去实现它
  • tryAcquire(int):独占方式,尝试获取资源;
  • tryRelease(int):独占方式,试释放资源;
  • tryAcquireShared(int):共享方式,尝试获取资源;
  • tryReleaseShared(int):共享方式,尝试释放资源;
 
为什么要实现这五个方法呢?查看AQS中这五个方法源码如下。

protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}



一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现
  • tryAcquire+tryRelease
  • tryAcquireShared+tryReleaseShared
中的一种即可,但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock读写锁。
 
同步类在实现时一般都将自定义同步器(sync)定义为内部类,供自己使用;而同步类自己则实现某个接口(如Lock接口),对外服务。当然,接口的实现要直接依赖sync。sync只用实现资源state的获取释放方式tryAcquire+tryRelelase。至于线程的排队、等待、唤醒等,上层的AQS都已经实现好了,我们不用关心。


原文始发于微信公众号(初心JAVA):并发第六弹 AQS抽象队列同步器

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之家整理,本文链接:https://www.bmabk.com/index.php/post/35540.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之家——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!