抽象队列同步器(独占锁)
- 基础介绍
- AQS代码概览
- Node类解析
- 通过ReentrantLock窥探AQS独占锁
- 最简单的实例
- 重入锁实例
- 锁竞争实例
- 结尾
文章图片
它主要实现了对同步状态的管理以及对阻塞线程进行排队、等待通知,就拿ReetrantLock为例,它有以下的功能
- 获取锁
- 争抢这把锁却没有成功的这些线程要被存放到一个集合中
- 释放锁,集合中的线程会被唤醒重现来争抢锁
- 使用锁来创建Condition对象
- .....
所以AQS中的内容主要可以分为四部分
- CLH队列:存储等待线程,其主要是通过双向链表的方式实现,CLH是它的发明者的三个大佬的名字的首字母。
- 独占锁
- 共享锁
- Condition实现
文章图片
AbstractQueuedSynchronizer这个类当中包含两个内部类,其中ConditionObject就是Condition功能的主要实现,一般创建Condition的方式就是Lock.newCondition(),而我们通过查看ReentrantLock源码可以发现,其实际创建的Condition就是一个ConditionObject实例。
Node是等待线程的载体,也就是等待线程所在的双向链表上的节点。
文章图片
文章图片
AbstractQueuedSynchronizer中有大量的方法,其中类似于tryAcquire和tryAcquireShared就是"类似方法"在独占锁和共享锁中的不同实现。
文章图片
下图中是AbstractQueuedSynchronizer中的一些成员变量,其中head和tail都是一个Node变量分别用于表示队头和队尾节点,state表示同步状态,stateOffset表示state变量相对于java对象的偏移量,也就是相对于AbstractQueuedSynchronizer.class的偏移量(class也是一个对象,java中万物皆对象),主要是用于后面使用CAS的方式给相应变量设置值、修改值等操作,headOffset、tailOffset同理,waitStatusOffset和nextOffset是相对于Node.class的偏移量。另外在AbstractQueuedSynchronizer 的父类AbstractOwnableSynchronizer中还有一个重要的变量exclusiveOwnerThread表示独占模式下拥有当前锁的线程。
文章图片
文章图片
Node类解析
static final class Node {
//用于标记共享模式
static final Node SHARED = new Node();
//用于标记独占模式
static final Node EXCLUSIVE = null;
// waitStatus 为这个值的时候 表示线程已经被取消
static final int CANCELLED =1;
// waitStatus 为这个值的时候 表示后继线程需要取消阻塞
static final int SIGNAL= -1;
// waitStatus 为这个值的时候 表示线程处于Condition下的等待状态
static final int CONDITION = -2;
//waitStatus 为这个值的时候 表示下个acquireShared操作将被允许
static final int PROPAGATE = -3;
/**
*这个字段可能有以下状态
*SIGNAL:该节点的后继节点被(或即将)阻塞(通过停放),因此当前节点在
*释放或取消时必须解除其后继节点的停放。为了避免竞争,获取方法
*必须首先表明它们需要一个信号,然后重试原子获取,然后在失败时
*阻塞。
*CANCELLED:节点因超时或中断而被取消
*CONDITION:这个节点被用于condition队列,在装个状态下这个节点不会被用于
*同步队列。
*PROPAGATE:这个节点是被共享的
*0:以上都不是
*
*这个字段的初始值为0,且是通过cas的方式对他进行安全写操作
*/
volatile int waitStatus;
//前置节点
volatile Node prev;
//继承节点
volatile Node next;
//该节点拥有的线程
volatile Thread thread;
//可能有两种作用
//1.独占模式下的condition条件下的等待节点
//2.用于判断是共享模式
Node nextWaiter;
//是否是共享模式
final boolean isShared() {
return nextWaiter == SHARED;
}//返回前置节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}Node() {// Used to establish initial head or SHARED marker
}Node(Thread thread, Node mode) {// Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
通过ReentrantLock窥探AQS独占锁 下面我们通过几个实例来探究AQS中的一些方法的实现以及在ReentrantLock中起到的作用。
最简单的实例
下面我们就通过一个简单的lock & unLock实例来入手
文章图片
通过断点进入,lock的具体实现在ReentrantLock的NonfairSync内部类中,这是由于我们为lock对象设置的非公平锁。
文章图片
文章图片
然后我们会进入AQS中的compareAndSetState方法,它主要是通过cas的方式判断state是否为0,是-就将其更改为1并返回true、否-不修改直接返回false,若为0就意味着这个锁现在是没有被任何线程占有的,然后我们将它的状态更改为1表示将其占用。
文章图片
返回是后,将AQS中的独占线程的字段赋值为当前线程,然后就加锁成功了。
文章图片
然后我们进入ReentrantLock的unlock方法,这个方法的主要实现就是在AQS的release方法中
文章图片
然后进入tryRelease方法,会获取当前锁的状态,然后用c表示要被更改成的目标状态,校验后,将锁的独占线程置为空并修改其状态字段state为目标状态,到这里锁已经解除占用了。
文章图片
tryRelease返回为true后会判断AQS中存不存在等待节点,如果存在则就将其唤醒(后面会看这里的源码)
重入锁实例
我们使用同一把ReentrantLock进行两次lock操作,由于第一次和上面的简单实例流程是一样的所以我们只关注第二次lock和unLock
文章图片
此时,由于已经lock过一次,即state=1,所以compareAndSetState(0,1)不会赋值成功,所以会进入到acquire方法,进而会首先进入到tryAcquire方法
文章图片
我们会在TryAcquire中再次判断锁的状态(因为在此过程中上一次lock可能被释放),然后由于当前线程就是这把锁的独占线程,所以我们是可重入这把锁的,最后将state的值改为2代表这把锁被当前线程重入了两次。
文章图片
由于tryAcquire(1)返回的是true所以!tryAcquire(1)为false导致程序不会进入acquire方法中的后续执行流程,到此,意味着第二次lock已经完成。
和简单实例中的unlock一样,程序会先进入release方法然后进入tryRelease方法,再这里面因为更改后的state为1所以不会讲当前锁的独占线程设置为null(会在最后一次unlock中设置)
文章图片
锁竞争实例
锁竞争就会涉及到等待队列以及等待节点的阻塞与唤醒,所以它的一系列操作的复杂度相对于上面的例子要更高一些。使用以下实例来体验一下多线程竞争锁的过程。
t1会首先获取到lock,这过程与无竞争锁的获取是一样的,主要的不同点在于t2获取锁和t1释放锁的过程。
文章图片
在idea中可以在下入这个位置切换调试的线程
文章图片
在t1线程获取到锁之后,我们切换到t2线程,发现idea此时已经给我们标注了lock这把锁已经被t1占用了。
文章图片
然后会进入到acquire方法,由于此时t1已经占用了锁,所以state ≠ 0且拥有锁的当前线程为t1≠t2所以 tryAcquire返回的是false,因此程序会进入addWaiter方法。
文章图片
文章图片
在这个方法中,会首先将t2线程封装到一个Node对象当中,然后通过tail节点判断队列是否被初始化了,由于CLH队列此时并没有元素存在,所以会进入到enq方法进行队列的首次初始化。
文章图片
在enq中会初始化这个队列会初始化队列,然后将传入的node插入到队尾,在这里面我们看到了for(; ; )的死循环(优雅点可以叫做自旋),那么它的作用是什么呢?
文章图片
在此次进入到enq中实际上for只进行了两次,第一次给头节点设置了一个没有实际数据的head节点,第二次将传入的node加入到了队尾,那么以上工作我们是可以在一次循环中完成的,就比如以下代码块中的实现方式
private Node enq(final Node node) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
}
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
其实自旋是为了保证线程安全,在t2线程获取锁的时候可能也有其它线程正在争抢lock,就比如恰好有线程在t2执行
Node t = tail
和 compareAndSetHead(new Node())
之间的时候初始化成功了队列设置了head节点, 那么compareAndSetHead
就会返回false不会进入这个分支,这时候就会重新获取tail节点再将传入的node节点插入到tail地next中,但是,此时tail可能也会被别的线程更改,那么就需要不断地自旋尝试修改直到成功位置,自旋结束。addWaiter结束后会进入acquireQueued,这个方法主要是会进行锁地争抢以及阻塞等待,最后根据failed字段判断是否要取消获取线程,这种情况一般就是状态被置为了Canceled
文章图片
shouldParkAfterFailedAcquire判断节点是否应该阻塞等待,如果这个节点为SIGNAL状态就说明该节点的后继节点应该被阻塞,继而会执行parkAndCheckInterrupt方法对其进行阻塞,并在它被唤醒的时候判断此线程是否是被interrupt的。
文章图片
正常情况下如果如果t1线程不unlock,那么t2线程将一直阻塞在parkAndCheckInterrupt方法,当其被唤醒后会继续自旋尝试获取锁。
然后我们切换回t1线程,进入unlock方法,调用AQS的release方法,然后tryRelease里面操作跟上面两个实例相同不在赘述,唯一不同的是之前实例的等待队列都为空,也就是head节点都是null,所以不会去唤醒阻塞节点,因为此时我们有t2线程所在的节点是被存储到了队列中所以,程序会进入到unparkSuccessor方法中,执行完这个方法后t2线程会从之前的WAIT状态转换为RUNNING状态即被唤醒!
文章图片
文章图片
t2被唤醒后会去再次tryAcquire,成功后去执行临界区的内容,然后正常释放lock锁。
结尾 【抽象队列同步器(独占锁)】上面利用ReentrantLock介绍了AQS独占锁相关内容,除此之外,后面还会通过ReentrantReadWriteLock介绍共享锁的实现、Condition的实现以及其它相关的JUC类中AQS的使用。
推荐阅读
- 面向对象与设计原则
- 抽象数据类型(ADT)
- sersync实时同步
- 如何使用C#下载Web文件并同步和异步显示下载进度
- Android JNI - 线程同步
- java|一个抽奖的例子演示线程的同步,暂停和恢复
- Android studio 3.1.3创建新项目,c ++支持同步失败
- 如何在WinForms中使用带有C#的SSH.NET(同步和异步)访问SFTP服务器
- Jenkins 获取构建队列排队时间 queueDuration
- 同步/阻止Application.Invoke()for GTK#