了解LockSupport
LockSupport 定义了一组的公共静态方法,这些方法提供了最基本的线程阻 塞和唤醒功能,而 LockSupport 也成为构建同步组件的基础工具。
LockSupport 定义了一组以 park 开头的方法用来阻塞当前线程,以及 unpark(Threadthread)方法来唤醒一个被阻塞的线程。LockSupport 增加了 park(Objectblocker)、parkNanos(Objectblocker,longnanos)和 parkUntil(Object blocker,longdeadline)3 个方法,用于实现阻塞当前线程的功能,其中参数 blocker 是用来标识当前线程在等待的对象(以下称为阻塞对象),该对象主要用于问题 排查和系统监控。
CLH队列锁
CLH 队列锁即 Craig,Landin,andHagersten(CLH)locks。
CLH 队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程 仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束 自旋。
当一个线程需要获取锁时:
1. 创建一个的QNode,将其中的locked设置为true表示需要获取锁, myPred 表示对其前驱结点的引用
2. 线程 A 对 tail 域调用 getAndSet 方法,使自己成为队列的尾部,同时获取 一个指向其前驱结点的引用 myPred
线程 B 需要获得锁,同样的流程再来一遍
3.线程就在前驱结点的 locked 字段上旋转,直到前驱结点释放锁(前驱节点 的锁值 locked==false)
4.当一个线程需要释放锁时,将当前结点的 locked 域设置为 false,同时回收前驱结点
如上图所示,前驱结点释放锁,线程 A 的 myPred 所指向的前驱结点的 locked 字段变为 false,线程 A 就可以获取到锁。
CLH 队列锁的优点是空间复杂度低(如果有 n 个线程,L 个锁,每个线程每 次只获取一个锁,那么需要的存储空间是 O(L+n),n 个线程有 n 个 myNode, L 个锁有 L 个 tail)。CLH 队列锁常用在 SMP 体系结构下。
Java 中的 AQS 是 CLH 队列锁的一种变体实现。
扩展知识点
SMP(SymmetricMulti-Processor) 。即 对 称 多 处 理 器 结 构 ,指 server中 多 个 CPU对 称 工 作 ,每 一 个 CPU访 问 内 存 地 址 所 需 时 间 同 样 。 其 主 要 特 征 是 共 享 , 包 括 对 CPU , 内 存 , I/O等 进行 共 享 。 SMP的 长 处 是 可 以 保 证 内 存 一 致 性 。 缺 点 是 这 些 共 享 的 资 源 非 常 可 能 成 为 性 能 瓶、颈 。 随 着 CPU数 量 的 添 加 , 每 一 个 CPU都 要 访 问 同 样 的 内 存 资 源 , 可 能 导 致 内 存 訪 问 冲 突 ,可 能 会 导 致 CPU资 源 的 浪 费 。 经 常 使 用 的 PC机 就 属 于 这 样 的 。
非 一 致 存 储 访 问 , 将 CPU分 为 CPU模 块 , 每 个 CPU模 块 由 多 个 CPU组 成 , 并 且 具 有 独立 的 本 地 内 存 、 I/O槽 口 等 , 模 块 之 间 可 以 通 过 互 联 模 块 相 互 访 问 , 访 问 本 地 内 存 ( 本 CPU 模 块 的 内 存 ) 的 速 度 将 远 远 高 于 访 问 远 地 内 存 ( 其 他 CPU 模 块 的 内 存 ) 的 速 度 , 这 也 是 非 一 致 存 储 访 问 的 由 来 。 NUMA 较 好 地 解 决 SMP 的 扩 展 问 题 , 当 CPU 数 量 增 加 时 , 因 为 访 问 远 地 内 存 的 延 时 远 远 超 过 本 地 内 存 , 系 统 性 能 无 法 线 性 增 加。
CLH唯 一 的 缺 点 是 在 NUMA系 统 结 构 下 性 能 很 差 , 但 是 在 SMP系 统 结 构 下 该法还是非常有效的 。解决 NUMA系 统 结 构 的 思 路 是 MCS队 列 锁 。
AbstractQueuedSynchronizer
学习AQS的必要性
队列同步器 AbstractQueuedSynchronizer(以下简称同步器或 AQS),是用 来构建锁或者其他同步组件的基础框架,它使用了一个 int 成员变量表示同步状 态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。并发包的大师(Doug Lea)期望它能够成为实现大部分同步需求的基础。
AQS使用方式和其中的设计模式
AQS 的主要使用方式是继承,子类通过继承 AQS 并实现它的抽象方法来管 理同步状态,在 AQS 里由一个 int 型的 state 来代表这个状态,在抽象方法的实 现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的 3 个方法 (getState()、setState(intnewState)和 compareAndSetState(intexpect,intupdate)) 来进行操作,因为它们能够保证状态的改变是安全的。
在实现上,子类推荐被定义为自定义同步组件的静态内部类,AQS 自身没有 实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义 同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地 获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、 ReentrantReadWriteLock 和 CountDownLatch 等)。
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器。可以这样理解二者之间的关系:
锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;
同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、 线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者 所需关注的领域。
实现者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步 组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者 重写的方法。
模板方法模式
同步器的设计基于模板方法模式。模板方法模式的意图是,定义一个操作中 的算法的骨架,而将一些步骤的实现延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。我们最常见的就是 Spring 框架里的各种 Template。
实际例子
我们开了个蛋糕店,蛋糕店不能只卖一种蛋糕呀,于是我们决定先卖奶油蛋 糕,芝士蛋糕和慕斯蛋糕。三种蛋糕在制作方式上一样,都包括造型,烘焙和涂 抹蛋糕上的东西。所以可以定义一个抽象蛋糕模型
然后就可以批量生产三种蛋糕
这样一来,不但可以批量生产三种蛋糕,而且如果日后有扩展,只需要继承 抽象蛋糕方法就可以了,十分方便,我们天天生意做得越来越赚钱。突然有一天, 我们发现市面有一种最简单的小蛋糕销量很好,这种蛋糕就是简单烘烤成型就可以卖,并不需要涂抹什么食材,由于制作简单销售量大,这个品种也很赚钱,于是我们也想要生产这种蛋糕。但是我们发现了一个问题,抽象蛋糕是定义了抽象的涂抹方法的,也就是说扩展的这种蛋糕是必须要实现涂抹方法,这就很鸡儿蛋疼了。怎么办?我们可以将原来的模板修改为带钩子的模板。
做小蛋糕的时候通过 flag 来控制是否涂抹,其余已有的蛋糕制作不需要任何 修改可以照常进行。
AQS中的方法
模板方法
实现自定义同步组件时,将会调用同步器提供的模板方法
这些模板方法同步器提供的模板方法基本上分为 3 类:独占式获取与释放同 步状态、共享式获取与释放、同步状态和查询同步队列中的等待线程情况。
可重写的方法
访问或修改同步状态的方法
重写同步器指定的方法时,需要使用同步器提供的如下 3 个方法来访问或修 改同步状态。
•getState():获取当前同步状态。
•setState(intnewState):设置当前同步状态。
•compareAndSetState(intexpect,intupdate):使用 CAS 设置当前状态,该方 法能够保证状态设置的原子性。
实现一个自己的独占锁
/**
*不可重入锁
*类说明:实现我们自己独占锁,不可重入
*/
public class SelfLockimplements Lock {
// 静态内部类,自定义同步器
private static class Syncextends AbstractQueuedSynchronizer {
/*判断处于占用状态*/
@Override
protected boolean isHeldExclusively() {
return getState()==1;
}
/*获得锁*/
@Override
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0,1)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/*释放锁*/
@Override
protected boolean tryRelease(int arg) {
if(getState()==0){
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
//compareAndSetState(1,0);
return true;
}
// 返回一个Condition,每个condition都包含了一个condition队列
Condition newCondition() {
return new ConditionObject();
}
}
// 仅需要将操作代理到Sync上即可
private final Syncsync =new Sync();
@Override
public void lock() {
System.out.println(Thread.currentThread().getName()+" ready get lock");
sync.acquire(1);
System.out.println(Thread.currentThread().getName()+" already got lock");
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public void unlock() {
System.out.println(Thread.currentThread().getName()+" ready release lock");
sync.release(1);
System.out.println(Thread.currentThread().getName()+" already released lock");
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
@Override
public void lockInterruptibly()throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
调用我们自定义的锁
**
*类说明:对自定义的锁测试
*/
public class TestMyLock {
public void test() {
final Lock lock =new SelfLock();
class Workerextends Thread {
public void run() {
lock.lock();
System.out.println(Thread.currentThread().getName());
try {
sleep(1000);
}finally {
lock.unlock();
}
}
}
// 启动4个子线程
for (int i =0; i <4; i++) {
Worker w =new Worker();
//w.setDaemon(true);
w.start();
}
// 主线程每隔1秒换行
for (int i =0; i <10; i++) {
SleepTools.second(1);
//System.out.println();
}
}
public static void main(String[] args) {
TestMyLock testMyLock =new TestMyLock();
testMyLock.test();
}
}
这把锁当我们递归调用就会发现,该锁会将自己锁死,原因是该锁并不具备重复性调用
深入源码
AQS中 的 数 据 结 构 节 点 和 同 步 队 列
节点Node
既然说 Java 中的 AQS 是 CLH 队列锁的一种变体实现,毫无疑问,作为队列来 说,必然要有一个节点的数据结构来保存我们前面所说的各种域,比如前驱节点, 节点的状态等,这个数据结构就是 AQS 中的内部类 Node。作为这个数据结构应 该关心些什么信息?
1、线程信息,肯定要知道我是哪个线程;
2、队列中线程状态,既然知道是哪一个线程,肯定还要知道线程当前处在 什么状态,是已经取消了“获锁”请求,还是在“”等待中”,或者说“即将得 到锁”
3、前驱和后继线程,因为是一个等待队列,那么也就需要知道当前线程前 面的是哪个线程,当前线程后面的是哪个线程(因为当前线程释放锁以后,理当 立马通知后继线程去获取锁)。
所以这个 Node 类是这么设计的:
其中包括了:
线程的 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 节 点
当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构 造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步 状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。同步队列 中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱 和后继节点。
head和tail
AQS 还拥有首节点(head)和尾节点(tail)两个引用,一个指向队列头节 点,而另一个指向队列尾节点。
注 意 : 因 为 首 节 点 head是 不 保 存 线 程 信 息 的 节 点 , 仅 仅 是 因 为 数 据 结 构 设 计 上 的 需 要 , 在 数 据 结 构 上 , 这 种 做 法 往 往 叫 做 “ 空 头 节 点 链 表 ” 。 对 应 的 就 有 “ 非 空 头 结 点 链 表 ”
节点在同步队列中的增加和移出
节点加入到同步队列
当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步 状态,也就是获取同步状态失败,AQS 会将这个线程以及等待状态等信息构造成 为一个节点(Node)并将其加入同步队列的尾部。而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于 CAS 的设置尾节点的方法: compareAndSetTail(Nodeexpect,Nodeupdate),它需要传递当前线程“认为”的尾 节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
首节点的变化
首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会 唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。设 置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功 获取到同步状态,因此设置头节点的方法并不需要使用 CAS 来保证,它只需要将 首节点设置成为原首节点的后继节点并断开原首节点的 next 引用即可。
独占式同步状态获取与释放
获取:
通过调用同步器的 acquire(intarg)方法可以获取同步状态,主要完成了同步 状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,其 主要逻辑是:
首先调用自定义同步器实现的 tryAcquire(intarg)方法,该方法需要保证线程 安全的获取同步状态。
如果同步状态获取失败(tryAcquire 返回 false),则构造同步节点(独占式 Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过 addWaiter(Nodenode)方法将该节点加入到同步队列的尾部,
最后调用 acquireQueued(Nodenode,intarg)方法,使得该节点以“死循环” 的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒 主要依靠前驱节点的出队或阻塞线程被中断来实现。
addWaiter(Nodenode)方法中
将当前线程包装成 Node 后,队列不为空的情况下,先尝试把当前节点加入 队列并成为尾节点,如果不成功或者队列为空进入 enq(finalNodenode)方法。
在 enq(finalNodenode)方法中,同步器通过“死循环”来保证节点的正确添 加,这个死循环中,做了两件事,第一件,如果队列为空,初始化队列,new 出 一个空节点,并让首节点(head)和尾节点(tail)两个引用都指向这个空节点; 第二件事,把当前节点加入队列。
在“死循环”中只有通过 CAS 将节点设置成为尾节点之后,当前线程才能从 该方法返回,否则,当前线程不断地尝试设置。
节点进入同步队列之后,观察 acquireQueued(Nodenode,intarg)方法
其实就是一个自旋的过程,每个节点(或者说每个线程)都在自省地观察, 当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在 这个自旋过程中(并会阻塞节点的线程)。
在 acquireQueued(finalNodenode,intarg)方法中,当前线程在“死循环”中 尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,这是为 什么?原因有两个。
第一,头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状 态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节 点是否是头节点。
第二,维护同步队列的 FIFO 原则。 当前线程获取到同步状态后,让首节点(head)这个引用指向自己所在节点。 当同步状态获取成功后,当前线程就从 acquire 方法返回了。如果同步器实现的 是锁,那就代表当前线程获得了锁。
释放:
当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得 后续节点能够继续获取同步状态。通过调用同步器的 release(intarg)方法可以释 放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节 点重新尝试获取同步状态)。
该方法执行时,会唤醒首节点(head)所指向节点的后继节点线程, unparkSuccessor(Nodenode)方法使用 LockSupport 来唤醒处于等待状态的线程。 而在 unparkSuccessor 中,
这段代码的意思,一般情况下,被唤醒的是 head 指向节点的后继节点线程, 如果这个后继节点处于被 cancel 状态,(我推测开发者的思路这样的:后继节点 处于被 cancel 状态,意味着当锁竞争激烈时,队列的第一个节点等了很久(一直 被还未加入队列的节点抢走锁),包括后续的节点 cancel 的几率都比较大,所以) 先从尾开始遍历,找到最前面且没有被 cancel 的节点。
总结
在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被 加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点 为头节点且成功获取了同步状态。在释放同步状态时,同步器调用 tryRelease(int arg)方法释放同步状态,然后唤醒 head 指向节点的后继节点。
共享式同步状态获取与释放
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时 获取到同步状态。以读写为例,如果一个程序在进行读操作,那么这一时刻写操 作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操 作可以是共享式访问。
在 acquireShared(intarg)方法中,同步器调用 tryAcquireShared(intarg)方法尝 试获取同步状态,tryAcquireShared(intarg)方法返回值为 int 类型,当返回值大于 等于 0 时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功 获取到同步状态并退出自旋的条件就是 tryAcquireShared(intarg)方法返回值大于 等于 0。可以看到,在 doAcquireShared(intarg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于 0,表示该次获 取同步状态成功并从自旋过程中退出。
该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够 支持多个线程同时访问的并发组件(比如 Semaphore),它和独占式主要区别在 于 tryReleaseShared(intarg)方法必须确保同步状态(或者资源数)线程安全释放, 一般是通过循环和 CAS 来保证的,因为释放同步状态的操作会同时来自多个线程。
共享式的同步工具类
设计一个同步工具:该工具在同一时刻,只允许至多 3 个线程同时访问,超 过 3 个线程的访问将被阻塞。
首先,确定访问模式。TrinityLock 能够在同一时刻支持多个线程的访问,这 显然是共享式访问,因此,需要使用同步器提供的 acquireShared(intargs)方法等 和 Shared 相关的方法,这就要求 TwinsLock 必须重写 tryAcquireShared(intargs) 方法和 tryReleaseShared(intargs)方法,这样才能保证同步器的共享式同步状态的 获取与释放方法得以执行。
其次,定义资源数。TrinityLock 在同一时刻允许至多三个线程的同时访问, 表明同步资源数为 3,这样可以设置初始状态 status 为 3,当一个线程进行获取, status 减 1,该线程释放,则 status 加 1,状态的合法范围为 0、1 和 2,3,其中 0 表示当前已经有 3 个线程获取了同步资源,此时再有其他线程对同步状态进行获 取,该线程只能被阻塞。在同步状态变更时,需要使用 compareAndSet(int expect,intupdate)方法做原子性保障。
最后,组合自定义同步器。前面的章节提到,自定义同步组件通过组合自定 义同步器来完成同步功能,一般情况下自定义同步器会被定义为自定义同步组件 的内部类。
了解Condition的实现
Condition的数据结构
等待队列是一个 FIFO 的队列,在队列中的每个节点都包含了一个线程引用, 该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await() 方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。事实 上,节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中 节点类型都是同步器的静态内部类。
一个 Condition 包含一个等待队列,Condition 拥有首节点(firstWaiter)和尾 节点(lastWaiter)。当前线程调Condition.await()方法,将会以当前线程构造 节点,并将节点从尾部加入等待队列。Condition 拥有首尾节点的引用,而新增节点只需要将原有的尾节点 nextWaiter 指向它,并且更新尾节点即可。上述节点 引用更新的过程并没有使用 CAS 保证,原因在于调用 await()方法的线程必定是 获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列。
调用 Condition 的 await()方法(或者以 await 开头的方法),会使当前线程 进入等待队列并释放锁,同时线程状态变为等待状态。当从 await()方法返回时, 当前线程一定获取了 Condition 相关联的锁。
如果从队列(同步队列和等待队列)的角度看 await()方法,当调用 await() 方法时,相当于同步队列的首节点(获取了锁的节点)移动到 Condition 的等待 队列中。调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点, 该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同 步队列中的后继节点,然后当前线程会进入等待状态。当等待队列中的节点被唤 醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用 Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出 InterruptedException。
如图所示,同步队列的首节点并不会直接加入等待队列,而是通过 addConditionWaiter()方法把当前线程构造成一个新的节点并将其加入等待队列 中。
调用 Condition 的 signal()方法,将会唤醒在等待队列中等待时间最长的节点 (首节点),在唤醒节点之前,会将节点移到同步队列中。
调用该方法的前置条件是当前线程必须获取了锁,可以看到 signal()方法进 行了 isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程。接着获取 等待队列的首节点,将其移动到同步队列并使用 LockSupport 唤醒节点中的线程。
通过调用同步器的 enq(Nodenode)方法,等待队列中的头节点线程安全地移 动到同步队列。当节点移动到同步队列后,当前线程再使用 LockSupport 唤醒该 节点的线程。
被唤醒后的线程,将从 await()方法中的 while 循环中退出 (isOnSyncQueue(Nodenode)方法返回 true,节点已经在同步队列中),进而调 用同步器的 acquireQueued()方法加入到获取同步状态的竞争中。
成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的 await() 方法返回,此时该线程已经成功地获取了锁。
Condition 的 signalAll()方法,相当于对等待队列中的每个节点均执行一次 signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每 个节点的线程。
回头看Lock的实现
ReentrantLock的实现
锁的可重入
重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞, 该特性的实现需要解决以下两个问题。
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程, 如果是,则再次成功获取
2)锁的最终释放。线程重复 n 次获取了锁,随后在第 n 次释放该锁后,其 他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示 当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于 0 时表示锁已 经成功释放。
nonfairTryAcquire 方法增加了再次获取同步状态的处理逻辑:通过判断当前 线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请 求,则将同步状态值进行增加并返回 true,表示获取同步状态成功。同步状态表 示锁被一个线程重复获取的次数。
如果该锁被获取了 n 次,那么前(n-1)次 tryRelease(intreleases)方法必须返回 false,而只有同步状态完全释放了,才能返回 true。可以看到,该方法将同步状 态是否为 0 作为最终释放的条件,当同步状态为 0 时,将占有线程设置为 null, 并返回 true,表示释放成功。
公平和非公平锁
ReentrantLock 的构造函数中,默认的无参构造函数将会把 Sync 对象创建为 NonfairSync 对象,这是一个“非公平锁”;而另一个构造函数 ReentrantLock(booleanfair)传入参数为 true 时将会把 Sync 对象创建为“公平锁” FairSync。
nonfairTryAcquire(intacquires)方法,对于非公平锁,只要 CAS 设置同步状态 成功,则表示当前线程获取了锁,而公平锁则不同。tryAcquire 方法,该方法与 nonfairTryAcquire(intacquires)比较,唯一不同的位置为判断条件多了 hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的 判断,如果该方法返回 true,则表示有线程比当前线程更早地请求获取锁,因此 需要等待前驱线程获取并释放锁之后才能继续获取锁。
改造我们的独占锁为可重入
/**
* 类说明:实现我们自己独占锁,可重入
*/
public class ReenterSelfLock implements Lock {
// 静态内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
@Override
protected boolean isHeldExclusively() {
return getState() > 0;
}
// 当状态为0的时候获取锁
@Override
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
} else if (getExclusiveOwnerThread() == Thread.currentThread()) {
setState(getState() + 1);
return true;
}
return false;
}
// 释放锁,将状态设置为0
@Override
protected boolean tryRelease(int releases) {
if (getExclusiveOwnerThread() != Thread.currentThread()) {
throw new IllegalMonitorStateException();
}
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setState(getState() - 1);
if (getState() == 0) {
setExclusiveOwnerThread(null);
}
return true;
}
// 返回一个Condition,每个condition都包含了一个condition队列
Condition newCondition() {
return new ConditionObject();
}
}
// 仅需要将操作代理到Sync上即可
private final Sync sync = new Sync();
@Override
public void lock() {
System.out.println(Thread.currentThread().getName() + " ready get lock");
sync.acquire(1);
System.out.println(Thread.currentThread().getName() + " already got lock");
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public void unlock() {
System.out.println(Thread.currentThread().getName() + " ready release lock");
sync.release(1);
System.out.println(Thread.currentThread().getName() + " already released lock");
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
实战,实现一个奇葩点的三元共享同步工具类
/**
*类说明:共享同步工具类
*/
public class TrinityLock implements Lock {
//为n表示允许n个线程同时获得锁
private final Sync sync = new Sync(4);
private static final class Sync extends AbstractQueuedSynchronizer {
//private static final long serialVersionUID = -7889272986162341211L;
Sync(int count) {
if (count <= 0) {
throw new IllegalArgumentException("count must large than zero.");
}
setState(count);
}
/**
*
* @param reduceCount 扣减个数
* @return 返回小于0,表示当前线程获得同步状态失败
* 大于0,表示当前线程获得同步状态成功
*/
@Override
public int tryAcquireShared(int reduceCount) {
for (;;) {
int current = getState();
int newCount = current - reduceCount;
if (newCount < 0 || compareAndSetState(current, newCount)) {
return newCount;
}
}
}
/**
*
* @param returnCount 归还个数
* @return
*/
@Override
public boolean tryReleaseShared(int returnCount) {
for (;;) {
int current = getState();
int newCount = current + returnCount;
if (compareAndSetState(current, newCount)) {
return true;
}
}
}
final ConditionObject newCondition() {
return new ConditionObject();
}
}
@Override
public void lock() {
sync.acquireShared(1);
}
@Override
public void unlock() {
sync.releaseShared(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquireShared(1) >= 0;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(time));
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
ReentrantReadWriteLock的实现
读写状态的设计
读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的 同步状态。
回想 ReentrantLock 中自定义同步器的实现,同步状态表示锁被一个线程重 复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维 护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。
如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变 量,读写锁将变量切分成了两个部分,高 16 位表示读,低 16 位表示写,读写锁 是如何迅速确定读和写各自的状态呢?
答案是通过位运算。假设当前同步状态值为 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(intunused)方法中, 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果 当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠 CAS 保证) 增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程 同时释放读锁)均减少读状态。
锁的升降级
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放, 最后再获取读锁,这种分段完成的过程不能称之为锁降级。
锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥 有的)写锁的过程。
RentrantReadWriteLock 不支持锁升级(把持读锁、获取写锁,最后释放读锁 的过程)。目的是保证数据可见性,如果读锁已被多个线程获取,其中任意线程 成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。