并发编程-AbstractQueuedSynchronizer

用来构建锁或者其他同步组件的基础框架,它使用了一个int 成员变量表示同步状态,通过内置的FIFO(先进先出) 队列来完成资源获取线程的排队工作。
在这里插入图片描述

AQS 使用方式和其中的设计模式

AQS 的主要使用方式是继承,子类通过继承AQS 并实现它的抽象方法来管理同步状态,在AQS 里由一个int 型的state 来代表这个状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3 个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。
在这里插入图片描述
实现者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

访问或修改同步状态的方法

重写同步器指定的方法时,需要使用同步器提供的如下3 个方法来访问或修改同步状态。

  1. getState():获取当前同步状态。
  2. setState(int newState):设置当前同步状态。
  3. compareAndSetState(int expect,int update):使用CAS 设置当前状态,该方法能够保证状态设置的原子性。

AQS 中的数据结构-节点和同步队列

节点Node

AQS 中的内部类Node

  1. 线程信息,肯定要知道我是哪个线程;
  2. 队列中线程状态,既然知道是哪一个线程,肯定还要知道线程当前处在什么状态,是已经取消了“获锁”请求,还是在“”等待中”,或者说“即将得到锁”
  3. 前驱和后继线程,因为是一个等待队列,那么也就需要知道当前线程前面的是哪个线程,当前线程后面的是哪个线程(因为当前线程释放锁以后,理当立马通知后继线程去获取锁)。
    在这里插入图片描述

线程的2 种等待模式:
SHARED:表示线程以共享的模式等待锁(如ReadLock)
EXCLUSIVE:表示线程以互斥的模式等待锁(如ReetrantLock),互斥就是一把锁只能由一个线程持有,不能同时存在多个线程使用同一个锁

线程在队列中的状态枚举:
CANCELLED:值为1,表示线程的获锁请求已经“取消”
SIGNAL:值为-1,表示该线程一切都准备好了,就等待锁空闲出来给我
CONDITION:值为-2,表示线程等待某一个条件(Condition)被满足
PROPAGATE:值为-3,当线程处在“SHARED”模式时,该字段才会被使用上初始化Node 对象时,默认为0

成员变量:
waitStatus:该int 变量表示线程在队列中的状态,其值就是上述提到的CANCELLED、SIGNAL、CONDITION、PROPAGATE
prev:该变量类型为Node 对象,表示该节点的前一个Node 节点(前驱)
next:该变量类型为Node 对象,表示该节点的后一个Node 节点(后继)
thread:该变量类型为Thread 对象,表示该节点的代表的线程
nextWaiter:该变量类型为Node 对象,表示等待condition 条件的Node 节点

head 和tail
AQS 还拥有首节点(head)和尾节点(tail)两个引用,一个指向队列头节点,而另一个指向队列尾节点。

节点在同步队列中的增加和移出

1. 节点加入到同步队列

当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,也就是获取同步状态失败,AQS 会将这个线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列的尾部。而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS 的设置尾节点的方法:compareAndSetTail(Node expect,Nodeupdate),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。

2. 首节点的变化

首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。

3. 独占式同步状态获取与释放

获取
通过调用同步器的acquire(int arg)方法可以获取同步状态
在这里插入图片描述
首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法需要保证线程安全的获取同步状态。

如果同步状态获取失败(tryAcquire 返回false),则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部。

最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
在这里插入图片描述
将当前线程包装成Node 后,队列不为空的情况下,先尝试把当前节点加入队列并成为尾节点,如果不成功或者队列为空进入enq(final Node node)方法。
在这里插入图片描述
在“死循环”中只有通过CAS 将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。

释放
当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。
在这里插入图片描述
该方法执行时,会唤醒首节点(head)所指向节点的后继节点线程,unparkSuccessor(Node node)方法使用LockSupport 来唤醒处于等待状态的线程。
unparkSuccessor 中:
在这里插入图片描述
这段代码的意思,一般情况下,被唤醒的是head 指向节点的后继节点线程,
如果这个后继节点处于被cancel 状态,先从尾开始遍历,找到最前面且没有被cancel 的节点。

总结
在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被
加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点
为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int
arg)方法释放同步状态,然后唤醒head 指向节点的后继节点。

4. 共享式同步状态获取与释放

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。

在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg)方法返回值为int 类型,当返回值大于等于0 时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int arg)方法返回值大于等于0。可以看到,在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。

相关锁的实现

1. ReentrantLock 的实现

锁的可重入

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。

1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,
如果是,则再次成功获取。
2)锁的最终释放。线程重复n 次获取了锁,随后在第n 次释放该锁后,其
他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示
当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0 时表示锁已
经成功释放。

公平和非公平锁

ReentrantLock 的构造函数中,默认的无参构造函数将会把Sync 对象创建为NonfairSync 对象,这是一个“非公平锁”;而另一个构造函数ReentrantLock(boolean fair)传入参数为true 时将会把Sync 对象创建为“公平锁”FairSync。

2. ReentrantReadWriteLock 的实现

ReentrantReadWriteLock 的实现

读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。

假设当前同步状态值为S,写状态等于S&0x0000FFFF(将高16 位全部抹去),读状态等于S>>>16(无符号补0 右移16 位)。当写状态增加1 时,等于S+1,当读状态增加1 时,等于S+(1<<16),也就是S+0x00010000。根据状态的划分能得出一个推论:S 不等于0 时,当写状态(S&0x0000FFFF)等于0 时,则读状态(S>>>16)大于0,即读锁已被获取。

写锁的获取与释放

写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。

写锁的释放与ReentrantLock 的释放过程基本类似,每次释放均减少写状态,当写状态为0 时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。

读锁的获取与释放

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。

如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal 中,由线程自身维护。在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS 保证)增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态。

锁的升降级

锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

RentrantReadWriteLock 不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值