目录
注意:本文参考 java并发编程的艺术
从ReentrantLock的实现看AQS的原理及应用 | JavaGuide
重入锁ReentrantLock
ReentrantLock简介
重入锁ReentrantLock, 顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对 资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。
回忆在同步器一节中的示例(Mutex), 同时考虑如下场景当一个线程调用Mutex的lock() 方法获取锁之后,如果再次调用lock()方法,则该线程将会被自己所阻塞,原因是Mutex在实现 try Acquire(int acquires)方法时没有考虑占有锁的线程再次获取锁的场景,而在调用
try Acquire(int acquires)方法时返回了false, 导致该线程被阻塞。简单地说,Mutex是一个不支持 重进入的锁。而synchronized关键字隐式的支持重进入,比如一个synchronized修饰的递归方 法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁,而不像Mutex由于获 取了锁,而在下一次获取锁时出现阻塞自己的情况。
ReentrantLock虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方 法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。
这里提到一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定先 被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线 程最优先获取锁,也可以说锁获取是顺序的。ReentrantLock提供了一个构造函数,能够控制锁 是否是公平的。
事实上,公平的锁机制往往没有非公平的效率高,但是,并不是任何场景都是以TPS作为 唯一的指标,公平锁能够减少“饥饿“发生的概率,等待越久的请求越是能够得到优先满足。
ReentrantLock与Synchronized区别

// **************************Synchronized的使用方式**************************
// 1.用于代码块
synchronized (this) {}
// 2.用于对象
synchronized (object) {}
// 3.用于方法
public synchronized void test () {}
// 4.可重入
for (int i = 0; i < 100; i++) {
synchronized (this) {}
}
// **************************ReentrantLock的使用方式**************************
public void test () throw Exception {
// 1.初始化选择公平锁、非公平锁
ReentrantLock lock = new ReentrantLock(true);
// 2.可用于代码块
lock.lock();
try {
try {
// 3.支持多种加锁方式,比较灵活; 具有可重入特性
if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
} finally {
// 4.手动释放锁
lock.unlock()
}
} finally {
lock.unlock();
}
}
ReentrantLock 与 AQS 的关联
ReentrantLock 是如何通过公平锁和非公平锁与 AQS 关联起来呢? 我们着重从这两者的加锁过程来理解一下它们与 AQS 之间的关系(加锁过程中与 AQS 的关联比较明显,解锁流程后续会介绍)。

// java.util.concurrent.locks.ReentrantLock#NonfairSync
// 非公平锁
static final class NonfairSync extends Sync {
...
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
...
}
public ReentrantLock() {
sync = new NonfairSync();
}
当然你也可以用如下构造方法来指定使用公平锁:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}内部有一个静态内部类NonfairSync继承了Sync,Sync继承了AQS
这块代码的含义为:
1 若通过 CAS 设置变量 State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程。
2 若通过 CAS 设置变量 State(同步状态)失败,也就是获取锁失败,则进入 Acquire 方法进行后续处理。
再看下公平锁源码中获锁的方式:
// java.util.concurrent.locks.ReentrantLock#FairSync
static final class FairSync extends Sync {
...
final void lock() {
acquire(1);
}
...
}
看到这块代码,我们可能会存在这种疑问:Lock 函数通过 Acquire 方法进行加锁,但是具体是如何加锁的呢?
结合公平锁和非公平锁的加锁流程,虽然流程上有一定的不同,但是都调用了 Acquire 方法,而 Acquire 方法是 FairSync 和 UnfairSync 的父类 AQS 中的核心方法。
非公平锁的加锁流程


加锁:
通过 ReentrantLock 的加锁方法 Lock 进行加锁操作。
会调用到内部类 Sync 的 Lock 方法,由于 Sync#lock 是抽象方法,根据 ReentrantLock 初始化选择的公平锁和非公平锁,执行相关内部类的 Lock 方法,本质上都会执行 AQS 的 Acquire 方法。
AQS 的 Acquire 方法会执行 tryAcquire 方法,但是由于 tryAcquire 需要自定义同步器实现,因此执行了 ReentrantLock 中的 tryAcquire 方法,由于 ReentrantLock 是通过公平锁和非公平锁内部类实现的 tryAcquire 方法,因此会根据锁类型不同,执行不同的 tryAcquire。
tryAcquire 是获取锁逻辑,获取失败后,会执行框架 AQS 的后续逻辑,跟 ReentrantLock 自定义同步器无关。
解锁:
通过 ReentrantLock 的解锁方法 Unlock 进行解锁。
Unlock 会调用内部类 Sync 的 Release 方法,该方法继承于 AQS。
Release 中会调用 tryRelease 方法,tryRelease 需要自定义同步器实现,tryRelease 只在 ReentrantLock 中的 Sync 实现,因此可以看出,释放锁的过程,并不区分是否为公平锁。
释放成功后,所有处理由 AQS 框架完成,与自定义同步器无关。
通过上面的描述,大概可以总结出 ReentrantLock 加锁解锁时 API 层核心方法的映射关系。

非公平锁的具体实现与可重入
重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。
ReentrantLock以通过组合自定义同步器来实现锁的获取与释放,以非公平性(默认的)实现为例,获取同步状态的代码如代码清单
注意:ReentrantLock的NonfairSync类的tryAcquire方法直接调用了Sync类的nonfairTryAcquire方法

该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来 决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加(state++)并返回 true, 表示获取同步状态成功。
公平与非公平获取锁的区别
公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合 请求的绝对时间顺序,也就是FIFO。
与公平锁相比,非公平锁nonFairSync的lock实现略有不同。
公平锁的lock实现是直接调用acquire(1),而非公平锁的lock实现会先尝试CAS修改state,如果能够将state从0改成1,那么说明当前线程获取锁,既然获取锁,那么便直接插队setExclusiveOwnerThread(Thread.currentThread())。
如果CAS操作失败,再走正常流程,调用父类函数acquire(1)。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
final void lock() {
// 获取锁的第一步便是调用acquire,里面的参数1代表state需要增加1。
acquire(1);
}还有nonfairTryAcquire(int acquires)方法(nonFairSync的tryAcquire方法直接调用了sync的nonfairTryAcquire方法),对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同
FairSync的tryAcquire方法如下

该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了 hasQueuedPredecessors()方法
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}首先要知道,hasQueuedPredecessors返回true代表有别的线程在CHL队列中排了当前线程之前,当前线程需要入队等待;
返回false代表队列中没有节点或者当前线程处于CHL队列的第一个线程。
先判断head是否等于tail,如果head和tail不相等,说明队列中有等待线程创建的节点(第一个条件为true,由于后面是||,继续后面的判断)
接着判断head的后置节点,这里肯定会不是null,为false。如果此Node节点对应的线程和当前的线程不是同一个线程,那么则会返回true,代表head的next节点不是当前线程的,不能获取锁。
如果队列中只有一个Node节点,那么head会等于tail,此时直接返回false,可以获取锁。

即加入了同步队列中当前节点是否有前驱节点的判断,如果该 方法返回true, 则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释 放锁之后才能继续获取锁。
公平性锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁出现了一个线程连续获取锁的情况。
为什么会出现线程连续获取锁的情况呢?回顾nonfairTry Acquire(int acquires)方法,当一个线程请求锁时只要获取了同步状态即成功获取锁。在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。
非公平性锁可能使线程”饥饿",为什么它又被设定成默认的实现呢?公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽 然可能造成线程饥饿,但极少的线程切换,保证了其更大的吞吐量。
ReentrantLock锁的释放与可重入
// java.util.concurrent.locks.ReentrantLock
public void unlock() {
sync.release(1);
}可以看到,本质释放锁的地方,是通过框架来完成的,调用了AQS的release方法。
release方法里面调用了Sync的tryRelease方法
在 ReentrantLock 里面的公平锁和非公平锁的父类 Sync 定义了可重入锁的释放锁机制。
成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放 同步状态时减少同步状态值,该方法的代码如代码清单
在 ReentrantLock 里面的公平锁和非公平锁的父类 Sync 定义了可重入锁的释放锁机制。 // java.util.concurrent.locks.ReentrantLock.Sync
// 方法返回当前锁是不是没有被线程持有
protected final boolean tryRelease(int releases) {
// 减少可重入次数
int c = getState() - releases;
// 当前线程不是持有锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果持有线程全部释放,将当前独占锁所有线程设置为null,并更新state
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false, 而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null, 并返回true, 表示释放成功。
读写锁
之前提到锁(如Mutex和ReentrantLock)基本都是排他锁,这些锁在同一时刻只允许一个线 程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读 线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写 锁,使得并发性相比一般的排他锁有了很大提升。
除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场 景的编程方式。假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务 (例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读 服务可见。
在没有读写锁支持的(Java 5之前)时候,如果需要完成上述工作就要使用Java的等待通知 机制,就是当写操作开始时,所有晚于写橾作的读操作均会进入等待状态,只有写操作完成并 进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键进行同 步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。改用读写锁实现上述功 能,只需要在读操作时获取读锁,写橾作时获取写锁即可。当写锁被获取到时,后续(非当前写 操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用 等待通知机制的实现方式而言,变得简单明了。
一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写 的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包提供读写锁的实现是 ReentrantReadWri teLock, 它提供的特性如表

读写锁的接口与示例
ReadWri teLock仅定义了获取读锁和写锁的两个方法,即readLock()方法和wri teLock()方法,而其实现 ReentrantReadWri teLock, 除了接口方法之外,还提供了一些便于外界监控其 内部工作状态的方法,这些方法以及描述如表



上述示例中,Cache组合一个非线程安全的HashMap作为缓存的实现同时使用读写锁的 读锁和写锁来保证Cache是线程安全的。在读操作get(String key)方法中,需要获取读锁,这使 得并发访问该方法时不会被阻塞。
写橾作put(String key,Object value)方法和clear()方法,在更新 HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而 只有写锁被释放之后,其他读写操作才能继续。
Cache使用读写锁提升读操作的并发性,也保 证每次写操作对所有的读写操作的可见性,同时简化了编程方式。
读写锁的实现分析
分析ReentrantReadWri teLock的实现,主要包括:读写状态的设计、写锁的获取与释 放读锁的获取与释放以及锁降级(以下没有特别说明读写锁均可认为是 ReentrantReadWri teLock)。
读写状态的设计
读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。
回想ReentrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读 写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状 态,使得该状态的设计成为读写锁实现的关键。
如果在一个整型变量上维护多种状态,就一定需要”按位切割使用“这个变量,读写锁将 变量切分成了两个部分,高16位表示读,低16位表示写,划分方式如图

当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次 读锁。读写锁是如何迅速确定读和写各自的状态呢?
答案是通过位运算。假设当前同步状态 值为S, 写状态等于S&0x0000FFFF (将高16位全部抹去),读状态等于S>>>16(无符号补0右移 16位)。
当写状态增加1时,等于S+1, 当读状态增加l时,等于S+(1<<16), 也就是 S+0x00010000。
根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读 状态(S>>> 16)大于0, 即读锁已被获取。
写锁的获取与释放
写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。
如果当 前线程在获取写锁时读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态,获取写锁的代码如代码清单

该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:
读写锁要确保写锁的操作对读锁可见,如 果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当 前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写 锁一旦被获取,则其他读写线程的后续访问均被阻塞。
写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0 时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对 后续读写线程可见。
读锁的获取与释放
读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问 (或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。
如 果当前线程已经获取了读锁,则增加读状态。
如果当前线程在获取读锁时,写锁已被其他线程 获取,则进入等待状态。
获取读锁的实现从Java 5到Java 6变得复杂许多,主要原因是新增了一 些功能,例如getReadHoldCount()方法,作用是返回当前线程获取读锁的次数。读状态是所有线 程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由 线程自身维护,这使获取读锁的实现变得复杂。因此,这里将获取读锁的代码做了删减,保留 必要的部分,如代码清单

在try AcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读 锁失败,进入等待状态。
如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全, 依靠CAS保证)增加读状态,成功获取读锁。
读锁的每次释放(线程安全的可能有多个读线程同时释放读锁)均减少读状态,减少的 值是(1 <<16)。
锁降级
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读 锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到 读锁,随后释放(先前拥有的)写锁的过程。
接下来看一个锁降级的示例。因为数据不常变化,所以多个线程可以并发地进行数据处 理,当数据变更后,如果当前线程感知到数据变化,则进行数据的准备工作,同时其他处理线 程被阻塞,直到当前线程完成数据的准备工作,如代码清单

上述示例中,当数据发生变更后,update变量(布尔类型且volatile修饰)被设置为false, 此 时所有访问processData()方法的线程都能够感知到变化,但只有一个线程能够获取到写锁,其 他线程会被阻塞在读锁和写锁的lock()方法上。当前线程获取写锁完成数据准备之后,再获取 读锁,随后释放写锁,完成锁降级。
锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果 当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修 改了数据,那么当前线程无法感知线程T的数据更新。
如果当前线程获取读锁,即遵循锁降级 的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进 行数据更新。
RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的 也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了 数据,则其更新对其他获取到读锁的线程是不可见的。
Condition接口
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、 wait(long timeout)、notify()以及notify All()方法,这些方法与synchronized同步关键字配合,可以 实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等 待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。
通过对比Object的监视器方法和Conclition接口,可以更详细地了解Conclition的特性,对比 项与结果如表

Condition接口与示例
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到 Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创 建出来的,换句话说,Condition是依赖Lock对象的。
Condition的使用方式比较简单,需要注意在调用方法前获取锁,使用方式如代码清单

如示例所示,一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会 释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程 才从await()方法返回,并且在返回前已经获取了锁。
Condition定义的(部分)方法以及描述如表

获取一个Condition必须通过Lock的newCondition()方法。下面通过一个有界队列的示例来 深入了解Condition的使用方式。有界队列是一种特殊的队列,当队列为空时,队列的获取操作 将会阻塞获取线程,直到队列中有新增元素,当队列已满时,队列的插入操作将会阻塞插入线 程,直到队列出现“空位”,如代码清单


上述示例中,Bounded Queue通过add(T t)方法添加一个元素,通过remove()方法移出一个 元素。以添加方法为例。
首先需要获得锁,目的是确保数组修改的可见性和排他性。当数组数量等于数组长度时 表示数组已满,则调用notFull.await(), 当前线程随之释放锁并进入等待状态。如果数组数量不 等于数组长度,表示数组未满,则添加元素到数组中,同时通知等待在notEmpty上的线程,数 组中已经有新元素可以获取。
在添加和删除方法中使用while循环而非if判断,目的是防止过早或意外的通知,只有条件 符合才能够退出循环。回想之前提到的等待/通知的经典范式,二者是非常类似的。
Condition的实现分析
ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要 获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含一个队 列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。
下面将分析Condition的实现,主要包括:等待队列、等待和通知,下面提到的Condition如 果不加说明均指的是ConditionObj ect。
等待队列
等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是 在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会 释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了同步器中节点 的定义也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node。
一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部 加入等待队列,等待队列的基本结构如图

如图所示,Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点next Waiter 指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用 await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列,其对应关系如图

如图所示,Condition的实现是同步器的内部类,因此每个Condition实例都能够访问同步器 提供的方法,相当于每个Condition都拥有所属同步器的引用。
等待
调用Condition的aWait()方法(或者以aWait开头的方法),会使当前线程进入等待队列并释 放锁,同时线程状态变为等待状态。当从aWait()方法返回时,当前线程一定获取了Condition相 关联的锁。
如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同 步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。
Condition的await()方法,如代码清单

调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前 线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当 前线程会进入等待状态。
当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过 其他线程调用Condition. signal()方法唤醒,而是对等待线程进行中断,则会抛出 InterruptedExcepti on。
如果从队列的角度去看,当前线程加入Condition的等待队列,该过程如图

如图所示,同步队列的首节点并不会直接加入等待队列,而是通过addConditionWaiter()方法把当前线程构造成一个新的节点并将其加入等待队列中。
通知
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在 唤醒节点之前,会将节点移到同步队列中。

调用该方法的前置条件是当前线程必须获取了锁,可以看到signal()方法进行了isHeldExclusi vel y()检查,也就是当前线程必须是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。
节点从等待队列移动到同步队列的过程如图

通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队 列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。
被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node)方法 返回true, 节点已经在同步队列中),进而调用同步器的acquire Queued()方法加入到获取同步状 态的竞争中。
成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的await()方法返回,此 时该线程已经成功地获取了锁。
Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效 果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。
Condition实现原理图解
demo
/**
* ReentrantLock 实现源码学习
* @author 一枝花算不算浪漫
* @date 2020/4/28 7:20
*/
public class ReentrantLockDemo {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
System.out.println("线程一加锁成功");
System.out.println("线程一执行await被挂起");
condition.await();
System.out.println("线程一被唤醒成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("线程一释放锁成功");
}
}).start();
new Thread(() -> {
lock.lock();
try {
System.out.println("线程二加锁成功");
condition.signal();
System.out.println("线程二唤醒线程一");
} finally {
lock.unlock();
System.out.println("线程二释放锁成功");
}
}).start();
}
}
这里线程一先获取锁,然后使用await()方法挂起当前线程并释放锁,线程二获取锁后使用signal唤醒线程一。
我们还是用上面的demo作为实例,执行的流程如下:

线程一执行await()方法:
await()方法中首先调用addConditionWaiter()将当前线程加入到Condition队列中。
执行完后我们可以看下Condition队列中的数据:

具体实现代码为:
private Node addConditionWaiter() {
Node t = lastWaiter;
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
} 这里会用当前线程创建一个Node节点,waitStatus为CONDITION。接着会释放该节点的锁,调用之前解析过的release()方法,释放锁后此时会唤醒被挂起的线程二,线程二会继续尝试获取锁。
接着调用isOnSyncQueue()方法判断当前节点是否为Condition队列中的头部节点,如果是则调用LockSupport.park(this)挂起Condition中当前线程。此时线程一被挂起,线程二获取锁成功。
具体流程如下图:

线程二执行signal()方法:
首先我们考虑下线程二已经获取到锁,此时AQS等待队列中已经没有了数据。
接着就来看看线程二唤醒线程一的具体执行流程:
先判断当前线程是否为获取锁的线程,如果不是则直接抛出异常。接着调用doSignal()方法来唤醒线程。
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}这里先从transferForSignal()方法来看,通过上面的分析我们知道Condition队列中只有线程一创建的一个Node节点,且waitStatue为CONDITION,先通过CAS修改当前节点waitStatus为0,然后执行enq()方法将当前线程加入到等待队列中,并返回当前线程的前置节点。
加入等待队列的代码在上面也已经分析过,此时等待队列中数据如下图:

接着开始通过CAS修改当前节点的前置节点waitStatus为SIGNAL,并且唤醒当前线程。此时AQS中等待队列数据为:
线程一被唤醒后,继续执行await()方法中的 while 循环。
因为此时线程一的waitStatus已经被修改为0,所以执行isOnSyncQueue()方法会返回false。跳出while循环。
接着执行acquireQueued()方法,这里之前也有讲过,尝试重新获取锁,如果获取锁失败继续会被挂起。直到另外线程释放锁才被唤醒。
此时线程一的流程都已经分析完了,等线程二释放锁后,线程一会继续重试获取锁,流程到此终结。
Condition 和 wait/notify 的比较
Condition 可以精准的对多个不同条件进行控制,wait/notify 只能和 synchronized 关键字一起使用,并且只能唤醒一个或者全部的等待队列;
Condition 需要使用 Lock 进行控制,使用的时候要注意 lock() 后及时的 unlock(),Condition 有类似于 await 的机制,因此不会产生加锁方式而产生的死锁出现,同时底层实现的是 park/unpark 的机制,因此也不会产生先唤醒再挂起的死锁,一句话就是不会产生死锁,但是 wait/notify 会产生先唤醒再挂起的死锁。
java中的18把锁
乐观锁和悲观锁
悲观锁
悲观锁对应于生活中悲观的人,悲观的人总是想着事情往坏的方向发展。
举个生活中的例子,假设厕所只有一个坑位了,悲观锁上厕所会第一时间把门反锁上,这样其他人上厕所只能在门外等候,这种状态就是「阻塞」了。
回到代码世界中,一个共享数据加了悲观锁,那线程每次想操作这个数据前都会假设其他线程也可能会操作这个数据,所以每次操作前都会上锁,这样其他线程想操作这个数据拿不到锁只能阻塞了。

在 Java 语言中 synchronized 和 ReentrantLock等就是典型的悲观锁,还有一些使用了 synchronized 关键字的容器类如 HashTable 等也是悲观锁的应用。
乐观锁
乐观锁 对应于生活中乐观的人,乐观的人总是想着事情往好的方向发展。
举个生活中的例子,假设厕所只有一个坑位了,乐观锁认为:这荒郊野外的,又没有什么人,不会有人抢我坑位的,每次关门上锁多浪费时间,还是不加锁好了。你看乐观锁就是天生乐观!
回到代码世界中,乐观锁操作数据时不会上锁,在更新的时候会判断一下在此期间是否有其他线程去更新这个数据。

乐观锁可以使用版本号机制和CAS算法实现。在 Java 语言中 java.util.concurrent.atomic包下的原子类就是使用CAS 乐观锁实现的。
两种锁的使用场景
悲观锁和乐观锁没有孰优孰劣,有其各自适应的场景。
乐观锁适用于写比较少(冲突比较小)的场景,因为不用上锁、释放锁,省去了锁的开销,从而提升了吞吐量。
如果是写多读少的场景,即冲突比较严重,线程间竞争激励,使用乐观锁就是导致线程不断进行重试,这样可能还降低了性能,这种场景下使用悲观锁就比较合适。
独占锁和共享锁
独占锁
独占锁是指锁一次只能被一个线程所持有。如果一个线程对数据加上排他锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。

JDK中的synchronized和java.util.concurrent(JUC)包中Lock的实现类就是独占锁。
共享锁
共享锁是指锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据。

在 JDK 中 ReentrantReadWriteLock 就是一种共享锁。
互斥锁和读写锁
互斥锁
互斥锁是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。

互斥锁一次只能一个线程拥有互斥锁,其他线程只有等待。
读写锁
读写锁是共享锁的一种具体实现。读写锁管理一组锁,一个是只读的锁,一个是写锁。
读锁可以在没有写锁的时候被多个线程同时持有,而写锁是独占的。写锁的优先级要高于读锁,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
读写锁相比于互斥锁并发程度更高,每次只有一个写线程,但是同时可以有多个线程并发读。

在 JDK 中定义了一个读写锁的接口:ReadWriteLock
ReentrantReadWriteLock 实现了ReadWriteLock接口
公平锁和非公平锁
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买,后来的人在队尾排着,这是公平的。

在 java 中可以通过构造函数初始化公平锁
/** * 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁 */ Lock lock = new ReentrantLock(true);
非公平锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转,或者饥饿的状态(某个线程一直得不到锁)。

在 java 中 synchronized 关键字是非公平锁,ReentrantLock默认也是非公平锁。
可重入锁
可重入锁又称之为递归锁,是指同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁。

对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁。对于Synchronized而言,也是一个可重入锁。
敲黑板:可重入锁的一个好处是可一定程度避免死锁。
以 synchronized 为例,看一下下面的代码:
public synchronized void mehtodA() throws Exception{
// Do some magic tings
mehtodB();
}
public synchronized void mehtodB() throws Exception{
// Do some magic tings
}上面的代码中 methodA 调用 methodB,如果一个线程调用methodA 已经获取了锁再去调用 methodB 就不需要再次获取锁了,这就是可重入锁的特性。如果不是可重入锁的话,mehtodB 可能不会被当前线程执行,可能造成死锁。
自旋锁
自旋锁是指线程在没有获得锁时不是被直接挂起,而是执行一个忙循环,这个忙循环就是所谓的自旋。

自旋锁的目的是为了减少线程被挂起的几率,因为线程的挂起和唤醒也都是耗资源的操作。
如果锁被另一个线程占用的时间比较长,即使自旋了之后当前线程还是会被挂起,忙循环就会变成浪费系统资源的操作,反而降低了整体性能。因此自旋锁是不适应锁占用时间长的并发情况的。
在 Java 中,AtomicInteger 类有自旋的操作,我们看一下代码:
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}CAS 操作如果失败就会一直循环获取当前 value 值然后重试。
另外自适应自旋锁也需要了解一下。
在JDK1.6又引入了自适应自旋,这个就比较智能了,自旋时间不再固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果虚拟机认为这次自旋也很有可能再次成功那就会次序较多的时间,如果自旋很少成功,那以后可能就直接省略掉自旋过程,避免浪费处理器资源。
分段锁
分段锁 是一种锁的设计,并不是具体的一种锁。
分段锁设计目的是将锁的粒度进一步细化,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

在 Java 语言中 CurrentHashMap 底层就用了分段锁,使用Segment,就可以进行并发使用了。
锁升级(无锁|偏向锁|轻量级锁|重量级锁)
JDK1.6 为了提升性能减少获得锁和释放锁所带来的消耗,引入了4种锁的状态:无锁、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。
无锁
无锁状态其实就是上面讲的乐观锁,这里不再赘述。
偏向锁
Java偏向锁(Biased Locking)是指它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁。
偏向锁的实现是通过控制对象Mark Word的标志位来实现的,如果当前是可偏向状态,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入。
轻量级锁
当线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待上一个线程释放锁。
重量级锁
如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。
升级到重量级锁其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态。
在 Java 中,synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。这一过程在后续讲解 synchronized 关键字的原理时会详细介绍。
锁优化技术(锁粗化、锁消除)
锁粗化
锁粗化就是将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求。
举个例子,一个循环体中有一个代码同步块,每次循环都会执行加锁解锁操作。
private static final Object LOCK = new Object();
for(int i = 0;i < 100; i++) {
synchronized(LOCK){
// do some magic things
}
}
经过锁粗化后就变成下面这个样子了:
synchronized(LOCK){
for(int i = 0;i < 100; i++) {
// do some magic things
}
}锁消除
锁消除是指虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除。
举个例子让大家更好理解。
public String test(String s1, String s2){
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(s1);
stringBuffer.append(s2);
return stringBuffer.toString();
}
上面代码中有一个 test 方法,主要作用是将字符串 s1 和字符串 s2 串联起来。
test 方法中三个变量s1, s2, stringBuffer, 它们都是局部变量,局部变量是在栈上的,栈是线程私有的,所以就算有多个线程访问 test 方法也是线程安全的。
我们都知道 StringBuffer 是线程安全的类,append 方法是同步方法,但是 test 方法本来就是线程安全的,为了提升效率,虚拟机帮我们消除了这些同步锁,这个过程就被称为锁消除。
StringBuffer.class
// append 是同步方法
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}18把锁总结
