Java 线程同步与锁
锁的相关概念
按照其性质分类
公平锁, 非公平锁
公平锁
- 多个线程按照申请锁的顺序来获取锁
- 优点: 等待锁的线程不会长时间获取不到锁
- 缺点: 吞吐效率不高, 因为还要对顺序进行判断
非公平锁
- 多个线程随机获取锁
- 优点: 吞吐效率高
- 缺点: 等待锁的线程可能长时间获取不到锁
悲观锁, 乐观锁
悲观锁
- 认为在使用数据的时候一定有别的线程来修改数据, 因此在获取数据的时候会先加锁, 确保数据不会被别的线程修改
- 适合写操作多的场景, 先加锁可以保证写操作时数据正确
synchronized关键字和Lock的实现类都是悲观锁
乐观锁
- 认为在使用数据时不会有别的线程修改数据, 所以不会添加锁, 只在更新数据的时候去判断之前有没有别的线程更新了这个数据
- 如果数据已经被其他线程更新, 则根据不同的实现方式执行不同的操作(例如报错或者自动重试)
- 适合读操作多的场景, 避免读的时候被阻塞
- 在 Java 中是通过使用无锁编程来实现, 最常采用的是 CAS 算法, Java 原子类中的递增操作就通过 CAS 自旋实现的
CAS
- 全称 Compare And Swap (比较与交换)
- CAS 涉及到三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 要写入的新值 B
- 当且仅当 V 的值等于 A 时, CAS 通过原子方式用新值 B 来更新 V 的值(“比较+更新”整体是一个原子操作)
- 存在的问题
- ABA 问题
- 在 CAS 操作时, 其他线程将变量值 A 改为了 B, 但是又被改回了 A, 等到本线程使用期望值 A 与当前变量进行比较时, 发现变量 A 没有变, 于是 CAS 就将 A 值进行了交换操作, 但是实际上该值已经被其他线程改变过
- 解决思路是在变量前面添加版本号, 每次变量更新的时候都把版本号加一, 这样变化过程就从 “A-B-A” 变成了 “1A-2B-3A”
- JDK 从 1.5 开始提供了
AtomicStampedReference类来解决 ABA 问题, 具体操作封装在compareAndSet()中compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等, 如果都相等, 则进行 CAS
- 循环时间长开销大
- CAS 操作如果长时间不成功, 会导致其一直自旋, 给 CPU 带来非常大的开销
- 只能保证一个共享变量的原子操作
- JDK 从 1.5 开始提供了
AtomicReference类来保证引用对象之间的原子性, 可以把多个变量放在一个对象里来进行 CAS 操作
- JDK 从 1.5 开始提供了
- ABA 问题
可重入锁
- 在同一个线程在获取锁之后, 再次获取相同的锁时, 不会因为之前已经获取过还没释放而阻塞
- Java 中
ReentrantLock和synchronized都是可重入锁 - 优点: 可一定程度避免死锁
独享锁 (排他锁), 共享锁, 互斥锁, 读写锁
- 独享锁 (排他锁), 共享锁是一种广义的说法, 互斥锁, 读写锁是具体的实现
独享锁
- 该锁一次只能被一个线程所持有
- 获得独享锁的线程即能读数据又能修改数据
- JDK 中的
synchronized和 JUC 中Lock的实现类就是互斥锁
共享锁
- 该锁可被多个线程所持有
- 获得共享锁的线程只能读数据, 不能修改数据
- JUC 中
ReadWriteLock的实现类其读锁是共享锁, 其写锁是独享锁 - 优点: 可保证并发读, 提高读的效率
按照设计方案来分类
自旋锁, 适应性自旋锁
自旋锁
- 指尝试获取锁的线程不会立即阻塞, 而是采用循环的方式去尝试获取锁
- 优点: 减少线程上下文切换的消耗
- 缺点: 循环会一直占用 CPU
适应性自旋锁
- 自适应意味着自旋的时间(次数)不再固定, 而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
锁粗化, 锁消除
- 锁粗化和锁消除是虚拟机即时编译器 (JIT) 对锁的优化
锁粗化
- 如果一系列的连续操作都对同一个对象反复加锁和解锁, 甚至加锁操作是出现在循环体中的
- 即使没有线程竞争, 频繁地进行互斥同步操作也会导致不必要的性能损耗
- 如果 JIT 探测到有这样的操作, 将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
锁消除
- JIT 在运行时检测到某些代码上要求同步, 但是不可能存在锁竞争, 会对其进行消除
- 主要判定依据来源于逃逸分析的数据支持, 如果判断在一段代码中, 堆上的所有数据都不会逃逸出去从而被其他线程访问到, 那就可以把它们当做栈上数据对待, 认为它们是线程私有的, 同步加锁自然就无须进行
分段锁
- 分段锁是一种锁的设计, 并不是具体的一种锁
ConcurrentHashMap就是用分段锁实现的- 内部持有一个 Entry 数组, 数组中的每个元素又是一个链表, 同时又是一个锁 (
ReentrantLock) - 当需要 put 元素的时候, 并不是对整个 hashmap 进行加锁, 而是先通过 hashcode 来知道他要放在那一个分段中, 然后对这个分段进行加锁, 所以当多线程 put 的时候, 只要不是放在一个分段中, 可以并行插入
- 在统计 size 的时候, 因为是获取全局信息, 所以需要获取所有的分段锁才能统计
- 内部持有一个 Entry 数组, 数组中的每个元素又是一个链表, 同时又是一个锁 (
无锁, 偏向锁, 轻量级锁, 重量级锁
- 这四种锁是指锁的状态, 在 JDK 1.6 中引入针对
synchronized的锁优化 - 级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁
- 锁状态只能升级不能降级 ( JVM中的锁也是能降级的, 不过条件很苛刻 )
无锁
- 无锁没有对资源进行锁定, 所有的线程都能访问并修改同一个资源, 但同时只有一个线程能修改成功
- CAS 原理及应用即是无锁的实现
偏向锁
- 偏向锁是指一段同步代码一直被一个线程所访问, 那么该线程会自动获取锁, 降低获取锁的代价
- 直到另一个线程尝试获取此锁的时候, 偏向锁模式才会结束
- 偏向锁可以提高带有同步但无竞争的程序性能, 但如果在多数锁总会被不同的线程访问时, 偏向锁模式就比较多余
- 偏向锁在 JDK 1.6 以上默认开启
- 可以通过 JVM 参数关闭偏向锁:
-XX:-UseBiasedLocking=false, 关闭之后程序默认会进入轻量级锁状态
- 可以通过 JVM 参数关闭偏向锁:
- 当 JVM 认为存在多线程竞争时, 会将偏向锁升级为轻量级锁
轻量级锁
-
轻量级锁作用于不同的线程交替的执行同步块中的代码, 不存在锁竞争的情况
-
只要存在锁竞争, 轻量级锁就会升级为重量级锁
重量级锁
- 重量级锁会让其他申请的线程进入阻塞, 性能降低
思考: 如何实现锁
- 如果要实现操作系统的锁, 该如何实现?先暂时不考虑性能、可用性等问题, 从最简单粗暴的方式开始思考, 以下都为伪代码
自旋
volatile int status = 0;
void lock() {
while(!compareAndSet(0, 1)) {
}
// get lock
}
void unlock() {
status = 0;
}
boolean compareAndSet(int except, int newValue) {
// CAS 操作, 修改 status 成功则返回 true
}
- 上面的代码通过自旋和 CAS 来实现一个最简单的锁
- 这样实现的锁显然有个致命的缺点:耗费 CPU 资源, 没有竞争到锁的线程会一直占用 CPU 资源进行 CAS 操作
yield + 自旋
- 要解决自旋锁的性能问题必须让竞争锁失败的线程不忙等, 而是在获取不到锁的时候能把 CPU 资源给让出来, 说到让 CPU 资源, 可能想到了
yield()方法
void lock() {
while(!compareAndSet(0, 1)) {
yield();
}
// get lock
}
yield()方法并没有完全解决问题yield()不一定能成功让出 CPU, 还跟线程的优先级有关- 如果有 100 个线程竞争锁, 当线程 1 获得锁后, 还有 99 个线程在反复的自旋 + yield, 假如运行在单核 CPU 下, 在竞争锁时最差只有 1% 的 CPU 利用率, 导致获得锁的线程 1 一直被中断, 执行实际业务代码时间变得更长, 从而导致锁释放的时间变的更长
sleep + 自旋
- 当竞争锁失败后, 可以将用
Thread.sleep将线程休眠, 从而不占用 CPU 资源
void lock() {
while(!compareAndSet(0, 1)) {
sleep(10);
}
// get lock
}
- 通常用于实现上层锁, 不适合用于操作系统级别的锁, 因为作为一个底层锁, 其 sleep 时间很难设置
- sleep 的时间取决于同步代码块的执行时间
- sleep 时间如果太短了, 会导致线程切换频繁 (极端情况和 yield 方式一样)
- sleep 时间如果设置的过长, 会导致线程不能及时获得锁
park + 自旋
- 那可不可以在获取不到锁的时候让线程释放 CPU 资源进行等待, 当持有锁的线程释放锁的时候将等待的线程唤起呢?
volatile int status = 0;
Queue parkQueue;
void lock() {
while(!compareAndSet(0, 1)) {
lock_wait();
}
// get lock
}
void synchronized unlock() {
lock_notify();
}
void lock_wait() {
// 将当期线程加入到等待队列
parkQueue.add(nowThread);
// 将当期线程释放CPU
releaseCPU();
}
void lock_notify() {
// 得到要唤醒的线程
Thread t = parkQueue.poll();
// 唤醒等待线程
wakeAThread(t);
}
- 这种方案相比于 sleep 而言, 只有在锁被释放的时候, 竞争锁的线程才会被唤醒, 不会存在过早或过晚唤醒的问题
对于锁冲突不严重的情况, 用自旋锁会更适合
- 试想每个线程获得锁后很短的一段时间内就释放锁, 竞争锁的线程只要经历几次自旋运算后就能获得锁, 那就没必要等待该线程了
- 因为等待线程意味着需要进入到内核态进行上下文切换, 而上下文切换的成本不低, 如果锁很快就释放了, 那上下文切换的开销将超过自旋
- 目前操作系统中, 一般是用自旋+等待结合的形式实现锁:在进入锁时先自旋一定次数, 如果还没获得锁再进行等待
linux 如何实现锁
-
linux 底层用 futex 实现锁
-
futex 由一个内核层的队列和一个用户空间层的 atomic integer 构成
-
当获得锁时, 尝试 CAS 更改 integer, 如果 integer 原始值是 0, 则修改成功, 该线程获得锁
-
否则就将当前线程放入到 wait queue中(即操作系统的等待队列)
futex 诞生之前
-
在 futex 诞生之前, linux 下的同步机制可以归为两类
- 用户态的同步机制
- 基本上就是利用原子指令实现的自旋锁 (代码层的 CAS)
- 关于自旋锁其缺点也说过了, 不适用于大的临界区(即锁占用时间比较长的情况)
- 内核同步机制
- 如 semaphore (信号量) 等, 使用的是上文说的自旋+等待的形式
- 它对于大小临界区和都适用
- 但是因为它是内核层的 (释放 CPU 资源是内核级调用), 所以每次 lock 与 unlock 都是一次系统调用, 即使没有锁冲突, 也必须要通过系统调用进入内核之后才能识别
- 用户态的同步机制
-
理想的同步机制应该是没有锁冲突时在用户态利用原子指令就解决问题, 而需要挂起等待时再使用内核提供的系统调用进行睡眠与唤醒。换句话说, 在用户态的自旋失败时, 能不能让进程挂起, 由持有锁的线程释放锁时将其唤醒?
-
可能会想出以下代码
-
void lock(int lockval) { // trylock 是用户级的自旋锁 while(!trylock(lockval)) { wait(); // 释放 CPU, 并将当期线程加入等待队列, 是系统调用 } } boolean trylock(int lockval) { int i = 0; // localval = 1 代表上锁成功 while(!compareAndSet(lockval, 0, 1)) { if(++i > 10) { return false; } } return true; } void unlock(int lockval) { compareAndSet(lockval, 1, 0); notify(); } -
上述代码的问题是
trylock和wait两个调用之间存在一个窗口, 如果一个线程trylock失败后在调用wait前, 持有锁的线程释放了锁, 则该线程执行完wait后就无人唤醒了
-
futex 诞生之后
-
我们来看看 futex 的方法定义
-
// uaddr 指向一个地址, val 代表这个地址期待的值, 当 *uaddr == val 时, 才会进行 wait int futex_wait(int *uaddr, int val); // 唤醒 n 个在 uaddr 指向的锁变量上挂起等待的进程 int futex_wake(int *uaddr, int n); -
futex_wait真正将进程挂起之前会检查 uaddr 指向的地址的值是否等于 val, 如果不相等则会立即返回, 由用户态继续 trylock, 否则将当前线程插入到一个队列中去并挂起 -
futex_wait检查 uaddr 的值前会获取自旋锁, 将当前线程插入等待队列后释放, 最后再挂起线程, 保证条件与等待之间的原子性 -
futex 内部维护了一个队列, 在线程挂起前会线程插入到其中, 同时对于队列中的每个节点都有一个标识, 代表该线程关联锁的uaddr。这样当用户态调用
futex_wake时, 只需要遍历这个等待队列, 把带有相同uaddr的节点所对应的进程唤醒就行了 -
作为优化, futex 维护的其实是个类似 Java 中的
ConcurrentHashMap的结构, 也就是数组加链表的形式- 其持有一个总链表, 总链表中每个元素都是一个带有自旋锁的子链表
- 调用
futex_wait挂起的进程, 通过其 uaddr hash 放到某一个具体的子链表上去 - 这样一方面能分散对等待队列的竞争、另一方面减小单个队列的长度, 便于
futex_wake时的查找 - 每个链表各自持有一把spinlock, 将 *uaddr 和 val 的比较操作 与 把进程加入队列的操作 保护在一个临界区中
-
futex 是支持多进程的, 当使用 futex 在多进程间进行同步时, 需要考虑同一个物理内存地址在不同进程中的虚拟地址是不同的
-
Java 如何实现锁
synchronized
- 在 JDK 1.6 中引入针对
synchronized进行了锁优化, 分为无锁、偏向锁、轻量级锁和重量级锁四种状态 - 在前面锁的概念中有介绍各个锁的使用场景
对象头
-
在 Java 中任意对象都可以用作锁, 因此必定要有一个映射关系, 存储该对象以及其对应的锁信息(比如当前哪个线程持有锁, 哪些线程在等待)
-
在 JVM 中, 对象在内存中除了本身的数据外还会有个对象头
- 对于普通对象而言, 其对象头中有两类信息:
mark word和类型指针mark word用于存储对象的 HashCode、GC分代年龄、锁状态等信息- 在 32 位系统上
mark word长度为 32bit, 64 位系统上长度为 64bit - 为了能在有限的空间里存储下更多的数据, 其存储格式是不固定的, 在 32 位系统上各状态的格式如下:
- 在 32 位系统上
- 类型指针是指向该对象所属类对象的指针
- 对于数组而言还会有一份记录数组长度的数据
- 对于普通对象而言, 其对象头中有两类信息:
重量级锁
- 重量级锁是我们常说的传统意义上的锁, 其利用操作系统底层的同步机制去实现 Java 中的线程同步
- 重量级锁的状态下, 对象的
mark word为指向一个堆中 monitor 对象的指针 - 关于什么是 monitor, 在 << Java 线程与线程池>> 中有相关深入分析
- 当调用一个锁对象的
wait或notify方法时, 如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁
轻量级锁
- 网上很多文章说轻量级锁有自旋, 这在源码中是不存在的, 只有重量级锁获取失败才会自旋
- 线程在执行同步块之前, JVM 会先在当前的线程的栈帧中创建一个
Lock Record, 其包括一个用于存储对象头中的mark word(官方称之为Displaced Mark Word)以及一个指向对象的指针。下图右边的部分就是一个Lock RecordLock Record是从高往低创建的
加锁过程
- 在线程栈中创建一个
Lock Record, 将其obj(即上图的 Object reference)字段指向锁对象 - 直接通过 CAS 将
Lock Record的地址存储在对象头的mark word中- 如果对象处于无锁状态则修改成功, 代表该线程获得了轻量级锁
- 如果失败, 需要判断是否为当前线程的锁重入
- 是, 则设置
Lock Record第一部分(Displaced Mark Word)为 null, 起到了一个重入计数器的作用 - 否, 则说明发生了竞争, 需要膨胀为重量级锁
- 是, 则设置
解锁过程
-
从低往高遍历线程栈, 找到所有
obj字段等于当前锁对象的Lock Record -
如果
Lock Record的Displaced Mark Word为 null, 代表是重入的解锁, 将obj设置为 null -
如果
Lock Record的Displaced Mark Word不为 null, 则利用 CAS 将对象头的mark word恢复成为Displaced Mark Word- 如果失败, 则膨胀为重量级锁
偏向锁
-
在 JDK 1.6 以上默认开启
-
在程序启动后, 通常有几秒的延迟, 可以通过
-XX:BiasedLockingStartupDelay=0来关闭延迟 -
当调用锁对象的
Object#hash或System.identityHashCode()方法会导致该对象的偏向锁升级- 因为对象的 hashcode 是在调用这两个方法时才生成的
- 如果是无锁状态则存放在
mark word中 - 如果是重量级锁则存放在对应的 monitor 中
- 而偏向锁没有地方能存放该信息, 所以必须升级
- 如果是无锁状态则存放在
- 因为对象的 hashcode 是在调用这两个方法时才生成的
对象创建
- 当新创建一个对象的时候, 如果该对象所属的 class 没有关闭偏向锁模式(默认开启), 那新创建对象的
mark word将是可偏向状态, 此时mark word中的 thread id(参见上文偏向状态下的mark word格式)为 0, 表示未偏向任何线程, 也叫做匿名偏向 (anonymously biased)
加锁过程
-
当该对象第一次被线程获得锁的时候, 发现是匿名偏向状态, 则会用 CAS 将
mark word中的 thread id 由 0 改成当前线程 Id- 如果失败, 说明存在另个线程使用锁, 将偏向锁撤销, 升级为轻量级锁
-
当被偏向的线程再次进入同步块时, 发现锁对象偏向的就是当前线程, 在通过一些额外的检查后, 会往当前线程的栈中添加一条
Displaced Mark Word为 null 的Lock Record, 然后继续执行同步块的代码, 因为操纵的是线程私有的栈, 因此不需要用到 CAS 指令 -
当其他线程进入同步块时, 发现已经有偏向的线程了, 则会进入撤销偏向锁的逻辑
- 一般来说, 会在
safepoint中去查看偏向的线程是否还存活- 如果存活且还在同步块中则将锁升级为轻量级锁, 原偏向的线程继续拥有锁, 当前线程则走入到锁升级的逻辑里
- 如果偏向的线程已经不存活或者不在同步块中, 则将对象头的
mark word改为无锁状态(unlocked), 之后再升级为轻量级锁
- 一般来说, 会在
解锁过程
- 当有其他线程尝试获得锁时, 是根据遍历偏向线程的
Lock Record来确定该线程是否还在执行同步块中的代码。因此将栈中的最近一条Lock Record的obj字段设置为 null 即可 - 需要注意的是, 偏向锁的解锁步骤中并不会修改对象头中的 thread id
批量重偏向与撤销
-
重偏向指将已偏向的锁对象头 thread id 指向新的线程 id
-
撤销偏向指将锁的偏向的状态撤销为无锁状态
-
从上文提到当有其他线程尝试获得锁时, 需要等到
safepoint时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁-
safe point在 GC 中经常提到, 其代表了一个状态, 在该状态下所有线程都是暂停的, 详细可以看这篇文章 -
如果运行时存在多线程竞争, 那偏向锁的存在不仅不能提高性能, 而且会导致性能下降
-
-
因此 JVM 中增加了一种批量重偏向/撤销的机制, 存在如下两种情况:(见官方论文第4小节)
-
一个线程创建了大量对象并执行了初始的同步操作, 之后在另一个线程中将这些对象作为锁进行之后的操作。这种情况下会导致大量的偏向锁撤销操作
-
存在明显多线程竞争的场景下使用偏向锁是不合适的, 例如生产者/消费者队列
-
-
批量重偏向(bulk rebias)机制是为了解决第一种场景, 批量撤销(bulk revoke)则是为了解决第二种场景
-
其做法是:以 class 为单位, 为每个 class 维护一个偏向锁撤销计数器
- 每一次该 class 的对象发生偏向撤销操作时, 该计数器 +1
- 当计数器达到重偏向阈值(默认20)时, JVM 就认为该 class 的偏向锁有问题, 因此会进行批量重偏向
- 每个 class 还有一个
epoch字段, 每个处于偏向锁状态对象的mark word中也有该字段 - 其初始值为创建该对象时 class 中的
epoch的值- 每次发生批量重偏向时, 就将该值 +1, 同时遍历 JVM 中所有线程的栈, 找到该 class 所有正处于加锁状态的偏向锁, 将其
epoch字段改为新值 - 下次获得锁时, 发现当前对象的
epoch值和 class 的epoch不相等, 那就算当前已经偏向了其他线程, 也不会执行撤销操作, 而是直接通过 CAS 操作将其mark word的 thread id 改成当前线程 id
- 每次发生批量重偏向时, 就将该值 +1, 同时遍历 JVM 中所有线程的栈, 找到该 class 所有正处于加锁状态的偏向锁, 将其
- 当达到重偏向阈值后, 假设该 class 计数器继续增长, 当其达到批量撤销的阈值后(默认40), JVM就认为该 class 的使用场景存在多线程竞争, 会标记该 class 为不可偏向, 之后对于该 class 的锁, 直接走轻量级锁的逻辑
锁状态转换流程
AQS
-
AQS 是类
AbstractQueuedSynchronizer的简称, Java 中的大部分同步类 (Lock、Semaphore、ReentrantLock等) 都是基于 AQS 实现的 -
AQS 是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架
-
大致原理如下
- AQS 维护一个
state变量和一个双向链表,state用来表示同步状态, 双向链表存储的是等待锁的线程 - 加锁时首先调用
tryAcquire尝试获得锁, 如果获得锁失败, 则将线程插入到双向链表中, 并调用LockSupport.park()方法阻塞当前线程 - 释放锁时调用
LockSupport.unpark()唤起链表中的第一个节点的线程, 被唤起的线程会重新走一遍竞争锁的流程
- AQS 维护一个
架构图
- 上图中有颜色的为方法, 无颜色的为属性
- 总的来说, AQS 框架共分为五层, 自上而下由浅入深, 从 AQS 对外暴露的 API 到底层基础数据
- 当有自定义同步器接入时, 只需重写第一层所需要的部分方法即可, 不需要关注底层具体的实现流程
- 当自定义同步器进行加锁或者解锁操作时, 先经过第一层的 API 进入 AQS 内部方法, 然后经过第二层进行锁的获取, 接着对于获取锁失败的流程, 进入第三层和第四层的等待队列处理, 而这些处理方式均依赖于第五层的基础数据提供层
Node
Node 是 AQS 双向链表中的节点类型
static final class Node {
// 表示线程以共享的模式等待锁
static final Node SHARED = new Node();
// 表示线程正在以独占的方式等待锁
static final Node EXCLUSIVE = null;
// waitStatus 的状态, 表示线程获取锁的请求已经取消了
static final int CANCELLED = 1;
// waitStatus 的状态, 表示节点的后继节点处于挂起等待 unpark 的状态
static final int SIGNAL = -1;
// waitStatus 的状态, 表示节点正在等待 condition signal
static final int CONDITION = -2;
// waitStatus 的状态, 当前线程处在 SHARED 情况下, 该字段才会使用
static final int PROPAGATE = -3;
// 当前节点在队列中的状态, 当一个 Node 被初始化的时候的值为 0
volatile int waitStatus;
// 前驱指针
volatile Node prev;
// 后继指针
volatile Node next;
// 该节点的线程
volatile Thread thread;
// 指向下一个处于 CONDITION 状态的节点
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;
}
}
state
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
......
// 0 代表锁未被占用
private volatile int state;
// 获取 State 的值
protected final int getState() {
return state;
}
// 设置State的值
protected final void setState(int newState) {
state = newState;
}
// 使用 CAS 方式更新 State
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
......
}
- 这几个方法都是
final修饰的, 说明子类中无法重写它们 - 我们可以通过修改
state字段表示的同步状态来实现多线程的独占模式和共享模式(加锁过程)- 独占锁通过
state变量的 0 和 1 两个状态来控制是否有线程占有锁 - 共享锁通过
state变量 0 或者非 0 来控制多个线程访问
- 独占锁通过
在 JUC 中的应用
| 同步工具 | 同步工具与 AQS 的关联 |
|---|---|
| ReentrantLock | 使用 AQS 保存锁重复持有的次数。当一个线程获取锁时, ReentrantLock 记录当前获得锁的线程标识, 用于检测是否重复获取, 以及错误线程试图解锁操作时异常情况的处理 |
| Semaphore | 使用 AQS 同步状态来保存信号量的当前计数。tryRelease 会增加计数, acquireShared 会减少计数 |
| CountDownLatch | 使用 AQS 同步状态来表示计数。计数为 0 时, 所有的 Acquire 操作(CountDownLatch 的 await 方法)才可以通过 |
| ReentrantReadWriteLock | 使用 AQS 同步状态中的 16 位保存写锁持有的次数, 剩下的 16 位用于保存读锁的持有次数 |
| ThreadPoolExecutor | Worker 利用 AQS 同步状态实现对独占线程变量的设置(tryAcquire 和 tryRelease) |
LockSupport
- AQS 中线程的阻塞和唤醒是通过
LockSupport来实现的
成员变量
-
UNSAFE: 用于操作内存和一些底层的指令 -
parkBlockerOffset: 记录parkBlocker在内存中的偏移量-
在
Thread中有如下变量-
/** * The argument supplied to the current call to * Java.util.concurrent.locks.LockSupport.park. * Set by (private) Java.util.concurrent.locks.LockSupport.setBlocker * Accessed using Java.util.concurrent.locks.LockSupport.getBlocker */ volatile Object parkBlocker;
-
-
这个对象是用来记录线程被阻塞时被谁阻塞的, 用于线程监控和分析工具来定位原因
-
可以通过
LockSupport的getBlocker获取到阻塞的对象 -
在
LockSupport初始化的静态代码块中-
static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> tk = Thread.class; parkBlockerOffset = UNSAFE.objectFieldOffset (tk.getDeclaredField("parkBlocker")); ...... } catch (Exception ex) { throw new Error(ex); } }- 先是通过反射机制获取
Thread的parkBlocker字段对象 - 然后通过
Unsafe对象的objectFieldOffset方法获取到parkBlocker在内存里的偏移量
- 先是通过反射机制获取
-
-
为什么要用偏移量来获取对象?
parkBlocker是在线程处于阻塞的情况下才会被赋值- 如果不通过这种内存的方法, 而是直接调用线程内的方法, 线程是不会回应调用的
-
park 方法
-
public static void park(Object blocker) { Thread t = Thread.currentThread(); setBlocker(t, blocker); UNSAFE.park(false, 0L); setBlocker(t, null); } -
park方法调用了 native 方法UNSAFE.park- 第一个参数表明第二个参数是时间间隔还是时间戳
- 第二个参数代表最长阻塞时间, 为 0 代表不判断超时
-
被
park的线程有三种情况会被唤醒, 但是无法知道是哪种原因- 其他线程调用
unpark唤醒该线程 - 其他线程调用该线程的中断, 所以唤醒后需要检查中断状态以响应中断
- 与
Object.wait一样会有虚假唤醒的情况, 需要配合判断条件在循环中调用
- 其他线程调用
ReentrantLock
构造函数
public ReentrantLock() {
sync = new NonfairSync(); // 非公平锁
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock里面有一个内部类Sync,Sync继承AQS, 添加锁和释放锁的大部分操作实际上都是在Sync中实现的ReentrantLock默认使用非公平锁
加锁过程
ReentrantLock.lock : 开始加锁操作
-
lock()内部调用了抽象方法sync.lock(), 需要通过Sync子类实现 -
FairSync中的lock()-
final void lock() { // 进行排队 acquire(1); }
-
-
NonfairSync中的lock()-
final void lock() { // 尝试将 state 值由 0 置换为 1 if (compareAndSetState(0, 1)) // 将当前线程设置为此锁的持有者 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } -
非公平锁比公平锁多了一行
compareAndSetState方法, 如果设置成功说明当前没有其他线程持有该锁, 否则需要通过acquire方法进入等待队列
-
AQS.acquire
-
acquire方法位于 AQS 类中-
public final void acquire(int arg) { if (!tryAcquire(arg) && // 尝试获取锁 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 获取锁失败, 加入到阻塞队列, 然后不断尝试获取锁 selfInterrupt(); // 如果等待过程出现中断, 恢复中断 }
-
tryAcquire : 尝试获取锁并更新 state
-
先调用
tryAcquire方法尝试获取锁, 其由子类NonfairSync和FairSync实现-
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 如果锁没被占用 // 公平锁比非公平锁多了 !hasQueuedPredecessors() 判断 if (!hasQueuedPredecessors() && // 查询是否还有等待时间更久的线程 compareAndSetState(0, acquires)) { // 尝试获取锁 setExclusiveOwnerThread(current); // 获取成功, 标记被该线程抢占 return true; } } else if (current == getExclusiveOwnerThread()) { // 如果为重入的情况 int nextc = c + acquires; if (nextc < 0) // int 自增溢出为负数的情况 throw new Error("Maximum lock count exceeded"); setState(nextc); // 记录重入次数 return true; } return false; }
-
addWaiter : 获取锁失败后把线程放入等待队列
-
如果获取锁失败, 调用
addWaiter方法把线程包装成Node对象, 放入到队列尾部, 并返回该节点-
private Node addWaiter(Node mode) { // 根据当前线程和锁模式新建节点 Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { // 如果尾节点不为空 node.prev = pred; // 新节点的前驱指针指向尾节点 if (compareAndSetTail(pred, node)) { // 将新节点设置为尾节点 pred.next = node; // 旧的尾节点后驱指针指向新的尾节点 return node; } } enq(node); // 将节点插入队列中, 如果必要的话进行初始化 return node; } -
如果尾节点为空 (说明队列中没有节点 ), 或者 CAS 失败 (说明已经被别的线程修改), 就需要通过
enq方法插入节点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; } } } } -
-
现在再来回看下公平锁
tryAcquire中调用的hasQueuedPredecessors方法-
public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); } -
双向链表中,
head节点为虚节点, 并不存储任何信息, 只是占位, 真正有数据的节点是在第二个节点开始的 -
addWaiter中节点入队时不是原子操作, 所以会出现短暂的head != tail情况, 当h != t时- 如果
(s = h.next) == null, 说明有线程正在初始化队列, 但只是进行到了tail指向head, 没有将head指向tail, 此时队列中有元素, 需要返回 true - 如果
(s = h.next) != null, 说明此时队列中至少有一个有效节点, 并且s.thread == Thread.currentThread(), 则说明等待队列的第一个有效节点中的线程与当前线程相同, 那么当前线程是可以获取资源的
- 如果
-
-
acquireQueued : 队列中的节点不断尝试获取锁
-
调用
addWaiter加入到队列后, 再通过acquireQueued方法不断去获取锁, 直到获取成功或者不再需要获取(中断)-
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; // 标记是否成功拿到锁 try { boolean interrupted = false; // 标记线程等待过程中是否中断过 // 开始自旋, 要么获取锁, 要么中断 for (;;) { // 获取前驱节点 final Node p = node.predecessor(); // 前驱节点为头节点时, 说明当前节点在真实数据队列的首部, 有权尝试获取锁 if (p == head && tryAcquire(arg)) { setHead(node); // 获取成功, 将当前节点设置为 head 节点 p.next = null; // help GC // 防止前后互相引用无法回收 failed = false; return interrupted; } // 当前节点不在队列首部, 或者在队列首部但没有获取到锁 (可能是非公平锁被抢占了) if (shouldParkAfterFailedAcquire(p, node) && // 清理和更新前驱节点状态 parkAndCheckInterrupt()) // 挂起线程 // 线程若被中断, 返回 true interrupted = true; } } finally { if (failed) // 发生异常, 取消获取锁的请求 cancelAcquire(node); } } private void setHead(Node node) { head = node; // 设置该节点不关联任何线程, 也就是虚节点 node.thread = null; node.prev = null; } private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) // 前驱节点的状态为 SIGNAL, 说明当前线程可以被挂起(阻塞) return true; if (ws > 0) { // 若前驱节点状态为 CANCELLED, 那就一直往前找正常等待状态的节点, 这个过程也在清理被取消的节点 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); // 找到之后将当前节点排在它后边 pred.next = node; } else { // 判断到这里 waitStatus 只可能是 0 或者 PROPAGATE // 把前驱节点的状态修改为 SIGNAL // 然后 acquireQueued 的循环将会再次 tryAcquire // 确保 tryAcquire 失败后会进入上面的 ws == Node.SIGNAL 判断挂起线程 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } private final boolean parkAndCheckInterrupt() { LockSupport.park(this); // 挂起 return Thread.interrupted(); // 返回并重置中断状态 } -
通过
cancelAcquire方法, 会将Node的状态标记为CANCELLED-
private void cancelAcquire(Node node) { // 将无效节点过滤 if (node == null) return; node.thread = null; // 通过前驱节点, 跳过并清理取消状态的节点 Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; Node predNext = pred.next; // 把当前节点的状态设置为 CANCELLED node.waitStatus = Node.CANCELLED; // 如果当前节点是尾节点, 则将刚刚找到的正常节点设置为尾节点 if (node == tail && compareAndSetTail(node, pred)) { // 将 tail 的后继节点指向 null compareAndSetNext(pred, predNext, null); } else { // 当前节点不是尾节点或者是尾节点但 CAS 失败 int ws; // 如果当前节点不是 head 的后继节点 // 1: 判断当前节点前驱节点的状态是否为 SIGNAL // 2: 如果不是, 则尝试把前驱节点状态设置为 SINGAL // 如果 1 和 2 中有一个为 true, 再判断前驱节点的线程是否不为 null // 如果上述条件都满足, 把当前节点的前驱节点的后继指针指向当前节点的后继节点 if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { Node next = node.next; if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { // 如果当前节点是 head 的后继节点或者上述条件不满足, 那就唤醒当前节点的后继节点 unparkSuccessor(node); } node.next = node; // help GC } }
-
-
流程图
解锁过程
ReentrantLock.unlock, AQS.release : 开始解锁操作
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// 尝试释放锁
if (tryRelease(arg)) {
// 释放成功
Node h = head;
// 头结点不为空并且头结点的 waitStatus 不是初始化节点情况, 解除线程挂起状态
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
- 为什么要
h != null && h.waitStatus != 0这样判断- 若
h == null, 则头节点还没初始化, 说明第一个节点还没入队 - 若
h != null && h.waitStatus == 0表明还没有进入等待的后继节点, 不需要唤醒 - 若
h != null && h.waitStatus < 0表明后继节点可能被阻塞了, 需要唤醒 - 不会出现
h != null && h.waitStatus > 0的情况, 只有获得了锁的节点才会成为 head
- 若
Sync.tryRelease : 尝试解锁并更新 state
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;
}
AQS.unparkSuccessor : 唤醒后继节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// 将节点状态设置为 0
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 如果下个节点是 null 或者下个节点被取消
if (s == null || s.waitStatus > 0) {
s = null;
// 从尾部节点开始找, 找到队列第一个正常状态的节点。
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t; // s 为慢指针, 当 t 遍历到 node 的时候, s 就指向 node 下一个有效节点
}
// 如果找到了正常节点便唤醒
if (s != null)
LockSupport.unpark(s.thread);
}
- 为什么要从后往前找第一个正常状态的节点
- 节点入队不是原子操作, 在
addWaiter方法中先执行node.prev = pred; compareAndSetTail(pred, node)将节点入队, 还没执行pred.next = node;时, 调用unparkSuccessor从前往后是找不到这个节点的 - 在产生
CANCELLED状态节点的时候, 先断开的是next指针,prev指针并未断开, 因此也是必须要从后往前遍历才能够遍历完全部的节点
- 节点入队不是原子操作, 在
中断恢复后的执行流程
-
被唤醒的线程处于
parkAndCheckInterrupt中-
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); // 之前在此处挂起 return Thread.interrupted(); // 返回并清除线程的中断状态 }
-
-
再回到
acquireQueued代码, 如果这个时候获取锁成功, 就会把线程的中断状态返回到acquire, -
如果
acquireQueued返回true, 就会执行selfInterrupt方法-
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } static void selfInterrupt() { Thread.currentThread().interrupt(); } -
为什么获取了锁以后还要中断线程?
- 这部分属于 Java 提供的协作式中断知识内容
- 当等待线程被唤醒时, 并不知道被唤醒的原因, 可能是当前线程在等待中被中断, 也可能是释放了锁以后被唤醒
- 因此我们通过
Thread.interrupted()方法检查中断标记(该方法返回了当前线程的中断状态, 并将当前线程的中断标识设置为false), 并记录下来, 如果发现该线程被中断过, 就再中断一次 - 线程在等待资源的过程中被唤醒, 唤醒后还是会不断地去尝试获取锁, 直到抢到锁为止。也就是说, 在整个流程中, 并不响应中断, 只是记录中断记录。最后抢到锁返回了, 那么如果被中断过的话, 就需要恢复中断
-
Synchronized 和 ReentrantLock 的区别
-
Synchronized 是 JVM 层次的锁实现, ReentrantLock 是 JDK 层次的锁实现
-
Synchronized 的锁状态是无法在代码中直接判断的, 但是 ReentrantLock 可以通过
ReentrantLock#isLocked判断 -
Synchronized 是非公平锁, ReentrantLock 是可以是公平也可以是非公平的
-
Synchronized 是不可以被中断的, 而
ReentrantLock#lockInterruptibly方法是可以被中断的 -
在发生异常时 Synchronized 会自动释放锁(由 javac 编译时自动实现), 而 ReentrantLock 需要开发者在 finally 块中显示释放锁
-
ReentrantLock 获取锁的形式有多种:如立即返回是否成功的 tryLock(), 以及等待指定时长的获取, 更加灵活
-
Synchronized 在特定的情况下对于已经在等待的线程是后来的线程先获得锁(上文有说), 而 ReentrantLock 对于已经在等待的线程一定是先来的线程先获得锁
Condition
- Condition 的作用等同于
Object.wait()和Object.notify(), 条件队列是一个 FIFO 队列, 可以通过signalAll解锁全部的线程, 也可以通过signal单独解锁线程, 可以通过如下方式创建
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
- Condition 内部也是维护了一个 FIFO 队列 condition queue
public class ConditionObject implements Condition, java.io.Serializable {
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
......
}
static final class Node {
......
Node nextWaiter;
......
}
awiat : 等待 Condition
public final void await() throws InterruptedException {
// 判断中断则抛出 InterruptedException
if (Thread.interrupted())
throw new InterruptedException();
// 在 condition queue 中增加节点, 节点状态为 CONDITION, 在增加节点的同时清除掉状态为 CANCELED 的节点
Node node = addConditionWaiter();
// 释放锁并储存释放时的 state, 失败会抛出 IllegalMonitorStateException 异常
int savedState = fullyRelease(node);
int interruptMode = 0;
// 判断节点是否在同步队列中, 只有不在同步队列, 才阻塞线程
while (!isOnSyncQueue(node)) {
// 阻塞 node, 直到被 signal 或者中断
LockSupport.park(this);
// 唤醒后检查中断, 若发生中断则更新节点状态并将其放入同步队列, 并记录对应的中断操作
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 当 node 被 signal 或中断后, 已被加入到同步队列中, 调用 acquireQueued 重新获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 移除掉所有状态为 CANCELED 的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
// 根据 interruptMode 抛出中断异常或恢复中断
reportInterruptAfterWait(interruptMode);
}
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
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;
}
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null; // 慢指针, 指向上个遍历到的有效节点
while (t != null) {
Node next = t.nextWaiter;
// 如果 t 的状态不是 CONDITION, 则把 t 节点从链表中摘除
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
if (node.next != null) // If has successor, it must be on queue
return true;
// 从后往前查找节点是否已经在等待队列中
return findNodeFromTail(node);
}
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
// 如果发生中断, 更新节点状态并将其放入同步队列
// 如果发生在 signal 之前则返回 THROW_IE, 如果中断发生在 signal 之后返回 REINTERRUPT
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;
}
final boolean transferAfterCancelledWait(Node node) {
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// CAS 成功说明该节点还没被 signal, 将其放入同步队列, 待获取到锁后抛出异常
enq(node);
return true;
}
// 若 CAS 失败则该节点正好被 signal 完, 只需自旋确认节点已经进入同步队列即可, 待获取到锁后恢复异常
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
signal, signalAll : 通知 Condition
public final void signal() {
// 非持有锁的线程调用会抛出 IllegalMonitorStateException 异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 取出 condition 队列第一个节点
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
private void doSignal(Node first) {
do {
// 向后移动 firstWaiter 指针
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// 更新当前节点状态并将其放入同步队列
} while (!transferForSignal(first) &&
// 失败时若队列中还有数据则会取下一个继续尝试
(first = firstWaiter) != null);
}
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
final boolean transferForSignal(Node node) {
// 节点状态改为 0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 插入同步队列并获得前驱节点
Node p = enq(node);
int ws = p.waitStatus;
// 设置前驱节点 waitStatus 为 SIGNAL
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 前驱节点被取消或设置状态失败, 此时节点已经在同步队列中, 唤醒节点线程往下走同步的逻辑即可
LockSupport.unpark(node.thread);
return true;
}
Condition 的中断处理
Condition.await()跟Object.wait()相比, 既有可能抛出InterruptedException异常, 也可能恢复中断- 处理中断的逻辑是如果在
signal之前线程被中断则抛出中断异常, 如果在signal之后线程被中断, 则恢复中断, 由调用代码自行处理中断标识
ReentrantReadWriteLock
-
ReentrantLock是独占锁,ReentrantReadWriteLock是读写锁 -
读写锁定义:一个资源能够被多个读线程访问, 或者被一个写线程访问, 但是不能同时存在读写线程
-
ReentrantReadWriteLock中读锁为共享锁, 写锁为独占锁-
public static class ReadLock implements Lock, java.io.Serializable { public void lock() { sync.acquireShared(1); //共享 } public void unlock() { sync.releaseShared(1); //共享 } } public static class WriteLock implements Lock, java.io.Serializable { public void lock() { sync.acquire(1); //独占 } public void unlock() { sync.release(1); //独占 } }
-
state
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
......
}
state的高 16 位表示读锁的 state, 低 16 位表示写锁的 state- 将两个锁的状态放在同一个
int变量的中原因是对state的操作可以使用 CAS 保证原子性 - 读锁和写锁最多可以获取 2^16 -1 = 65535 个 (包括重入)
写锁
加锁过程
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 写锁数量
int w = exclusiveCount(c);
if (c != 0) {
// 存在读锁或有其他线程持有写锁, 则进入队列等待
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 当前线程已持有写锁, 进行重入
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
// 判断是否需要排队 (公平锁)
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
static final class NonfairSync extends Sync {
final boolean writerShouldBlock() {
return false;
}
}
static final class FairSync extends Sync {
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
}
解锁过程
// 逻辑与 ReentrantLock 相似
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
读锁
加锁过程
AQS.acquireShared 和 AQS.doAcquireShared
public final void acquireShared(int arg) {
// 返回值小于 0 代表没有获取到共享锁 (读锁)
if (tryAcquireShared(arg) < 0)
// 进入等待
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
// 新建共享模式节点并进入等待队列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取前驱节点
final Node p = node.predecessor();
if (p == head) {
// 若为队首节点, 则重新尝试获取共享锁
int r = tryAcquireShared(arg);
if (r >= 0) {
// 头节点后移并传播
// 传播即唤醒后面连续的读节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
// 等待过程若发生中断则恢复中断
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&// 清理和更新前驱节点状态
parkAndCheckInterrupt()) // 挂起线程
// 线程若被中断, 返回 true
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
Sync.tryAcquireShared
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 存在被其他线程持有的写锁, 则进入队列等待
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 读锁数量
int r = sharedCount(c);
// 判断是否需要排队
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 开始统计每个读锁获取线程的重入次数
if (r == 0) {
// 首个获得读锁的线程
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 首个获得读锁的线程重入
firstReaderHoldCount++;
} else {
// 当前线程不是第一个获取读锁的线程, 放入该线程的本地变量中
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
// 快速获取失败, 进入 TryAcquireShared 完全版本重试
return fullTryAcquireShared(current);
}
static final class NonfairSync extends Sync {
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
// AQS.apparentlyFirstQueuedIsExclusive
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
// 在非公平模式, 只有同步队列的首节点是写锁才需要排队
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
static final class FairSync extends Sync {
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
// TryAcquireShared 完全版本
// 主要为了处理 CAS 失败和 tryAcquireShared 未处理的重入情况
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
// 部分逻辑与 TryAcquireShared 冗余
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
// 若队列中存在获取读锁的情况, 是会走到此位置
// 检查当前线程是否已持有过读锁
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
// CAS 失败则重新尝试一遍整个逻辑
}
}
解锁过程
AQS.releaseShared 和 AQS.doReleaseShared
public final boolean releaseShared(int arg) {
// 尝试释放共享锁
if (tryReleaseShared(arg)) {
// 共享锁完全释放, 则唤醒队列中的下个节点
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// head state 为 SIGNAL 时唤醒后继节点
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// head state 为 0 时切换状态
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
Sync.tryReleaseShared
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 更新线程缓存中的重入次数
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
// 只有读锁数为 0 时才代表完全释放读锁
return nextc == 0;
}
}
总结
- 由获取读锁的逻辑可见, 同一个线程拥有写锁之后再获取读锁是允许的, 这也被称为锁降级
- 在非公平锁情况下, 允许写锁插队, 也允许读锁插队, 但是读锁插队的前提是队列中的头节点不能是获取写锁的线程, 避免写线程饥饿
Semaphore
-
Semaphore (信号量)
-
信号量 S, 整型变量, 需要初始化值大于0
-
P 操作, 原子减少 S, 如果
S < 0, 则阻塞当前线程 -
V 操作, 原子增加 S, 如果
S <= 0, 则唤醒一个阻塞的线程
-
初始化
public class Semaphore implements java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
Sync(int permits) {
// state 用于储存信号量
setState(permits);
}
......
}
public Semaphore(int permits) {
// 默认非公平锁
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
......
}
加锁过程
static final class NonfairSync extends Sync {
......
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
abstract static class Sync extends AbstractQueuedSynchronizer {
......
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
// 剩余信号量小于 0 直接返回, 线程将进入队列排队
if (remaining < 0 ||
// 剩余信号量不小于 0 则成功获取到锁, 更新信号量
compareAndSetState(available, remaining))
return remaining;
}
}
}
static final class FairSync extends Sync {
......
protected int tryAcquireShared(int acquires) {
for (;;) {
// 公平锁判断
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
解锁过程
abstract static class Sync extends AbstractQueuedSynchronizer {
......
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
// 补回信号量
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
}
CountDownLatch
-
CountDownLatch (计数闭锁)
- 有初始计数值
- 计数值大于 0 时, 获取锁的线程会被阻塞
- 计数值被减到 0 时, 所有被阻塞的线程同时被释放
-
CountDownLatch 是一种闭锁的实现
- 闭锁可以延迟线程的进度直到其到达终止状态
初始化
public class CountDownLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
// state 用于储存计数值
setState(count);
}
......
}
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
......
}
加锁过程
private static final class Sync extends AbstractQueuedSynchronizer {
protected int tryAcquireShared(int acquires) {
// 根据计数是否到 0 返回是否成功
return (getState() == 0) ? 1 : -1;
}
......
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
解锁过程
private static final class Sync extends AbstractQueuedSynchronizer {
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
// 计数减 1
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
......
}
public void countDown() {
sync.releaseShared(1);
}
CyclicBarrier
- 栅栏 (Barrier) 类似于闭锁, 能阻塞一组线程直到某个事件发生
- 主要区别在于必须所有线程到达栅栏位置才能继续执行
- 闭锁用于等待事件, 栅栏用于等待其他线程
初始化
public class CyclicBarrier {
// 用于表示每代栅栏, 并记录该代栅栏是否有被打破
private static class Generation {
boolean broken = false;
}
// 阻塞所需要
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();
// 栅栏的线程数
private final int parties;
// 所有线程到达栅栏后会执行的函数
private final Runnable barrierCommand;
// 当前代
private Generation generation = new Generation();
// 用于计数, 每有一个到达栅栏的线程就减 1
private int count;
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
......
}
到达栅栏
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
if (g.broken)
// 若此代栅栏已经被打破, 则抛出 BrokenBarrierException 异常
throw new BrokenBarrierException();
if (Thread.interrupted()) {
// 处理中断, 打破栅栏
breakBarrier();
throw new InterruptedException();
}
// 计数减 1
int index = --count;
if (index == 0) { // tripped
// 计数到 0 时代表所有线程都已经到达栅栏
boolean ranAction = false;
try {
// 执行栅栏函数
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
// 开始下一代栅栏
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// 计数未到 0, 进行阻塞等待
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
// 等待超时, 打破栅栏
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
读写一致性的一些思考
-
在一个线程写,一个或多个线程读的情况下
-
试想下这样一个场景:一个线程往 HashMap 中写数据,一个线程往 HashMap 中读数据。 这样会有问题吗?
-
内存可见性的问题,HashMap 存储数据的
table并没有用voliate修饰,也就是说读线程可能一直读不到数据的最新值 -
指令重排序的问题,
get的时候可能得到的是一个中间状态的数据,我们看下put方法的部分代码-
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { ... if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = new Node<>(hash, key, value, next); ... } -
在
put操作时,如果table数组的指定位置为null,会创建一个Node对象,并放到table数组上 -
JVM 中
tab[i] = new Node<>(hash, key, value, next);这样的操作不是原子的,并且可能因为指令重排序,导致另一个线程调用get取tab[i]的时候,拿到的是一个还没有调用完构造方法的对象,导致不可预料的问题发生
-
-
-
上述的两个问题可以说都是因为 HashMap 中的内部属性没有被
voliate修饰导致的- 就算给
table加上了volatile应该只是保持了table引用的可见性,对于table中的元素不起作用 - 所以
table加上volatile也不能保证其中元素的可见性 - 在 ConcurrentHashMap (1.8) 中
-
通过
Unsafe类的getObjectVolatile方法保证table里获取到的数据每次都是最新的,而不是缓存 -
而在设置数组元素时, 采用
compareAndSwapObject方法,而不是直接通过下标去操作
-
- 就算给
创建对象的原子性问题
-
对于
Object obj = new Object();这样的操作, 在多线程的情况下可能会拿到一个未初始化的对象 -
以上 Java 语句分为 4 个步骤
- 在栈中分配一片空间给
obj引用 - 在 JVM 堆中创建一个
Object对象,注意这里仅仅是分配空间,没有调用构造方法 - 初始化第 2 步创建的对象,也就是调用其构造方法
- 栈中的
obj指向堆中的对象
- 在栈中分配一片空间给
-
问题在于 JVM 是会对指令进行重排序的,重排之后可能是第 4 步先于第 3 步执行,那这时候另外一个线程读到的就是没有还执行构造方法的对象,导致未知问题
-
JVM 重排只保证重排前和重排后在单线程中的结果一致性
-
注意 Java 中引用的赋值操作一定是原子的,比如说 a 和 b 均是对象的情况下不管是 32 位还是 64 位 JVM,
a=b操作均是原子的- 但如果 a 和 b 是
long或者double原子型数据,那在 32 位 JVM 上a=b不一定是原子的(看 JVM 具体实现),有可能是分成了两个 32 位操作。 但是对于voliate的long,double变量来说,其赋值是原子的
- 但如果 a 和 b 是
数据库中读写一致性
-
跳出 HashMap,在数据库中都是要用 MVCC 机制避免加读写锁
-
也就是说如果不用 MVCC,数据库是要加读写锁的,那为什么数据库要加读写锁呢?
- 原因是写操作不是原子的,如果不加读写锁或 MVCC,可能会读到中间状态的数据
- 以 HBase 为例,Hbase 写流程分为以下几个步骤:
- 获得行锁
- 开启 MVCC
- 写到内存 Buffer
- 写到 Append Log
- 释放行锁
- Flush Log
- MVCC 结束(这时才对读可见)
- 试想,如果没有不走 2,7 也不加读写锁,那在步骤 3 的时候,其他的线程就能读到该数据
- 如果说 3 之后出现了问题,那该条数据其实是写失败的。也就是说其他线程曾经读到过不存在的数据
-
同理,在 MySQL 中,如果不用 MVCC 也不用读写锁,一个事务还没 commit,其中的数据就能被读到,如果用读写锁,一个事务会对中更改的数据加写锁,这时其他读操作会阻塞,直到事务提交,对于性能有很大的影响,所以大多数情况下数据库都采用 MVCC 机制实现非锁定读