文章目录
synchronized
前置知识
当我们new一个对象后,所产生的对象都有一个对象头。
普通对象的对象头
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
数组对象对象头
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
在对象头中有一个MarkWork,其结构为:
在MarkWork中最后两位是锁标志位
32位虚拟机 MarkWork
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4| biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23|epoch:2 | age:4| biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|
64 位虚拟机 Mark Word
|-----------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|-----------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|-----------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|-----------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|-----------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|-----------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|-----------------|
Monitor
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其实例存储在堆中,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁。
底层原理:
- 刚开始 Monitor 中 Owner 为 null
- 如下图obj对象关联了一个Monitor对象
- 当Thread1线程获取到obj对象的锁后,就会将obj锁关联到Monitor对象中的Owner,也就是锁的持有者指向Thread1线程对象
- 而如果此时还有线程来获取锁就会从运行状态变成阻塞状态,挂在EntryList这个双向链表中,如下图中的Thread2和Thread3线程
- 而WaitSet中的线程Thread2是以前获取过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制)
- 当Thread1线程执行完synchronized代码块中的代码后,就会根据对象头中的MarkWork找到关联的Monitor对象,将Owner设置为空,并唤醒EntryList中的线程,让这些线程来竞争锁
锁升级
轻量级锁
轻量级锁的使用场景:如果一个对象在多线程场景下要加锁,但加锁的时间是错开的(也就是没有存在竞争锁的情况),那么可使用轻量级锁来优化。而synchronized
就是这么做的。
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
-
创建锁记录(Lock Record)对象 ,每个线程都会在栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的MarkWord
-
当调用
method1()
方法的时候,会让Object reference 指向锁对象 ,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录 -
如果 cas 替换成功,对象头中存储了
锁记录地址和状态 00
,表示由该线程给对象加锁,图示如下
-
如果 cas 失败,有两种情况
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
- 下图就是锁重入的时候
-
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
-
当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
-
这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
- 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
- 然后自己进入 Monitor 的 EntryList BLOCKED
-
当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
自旋锁
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞,因为阻塞线程会涉及到线程上下文的切换,会有一定的性能开销。
也就是说当一个线程已经持有锁的时候,另外一个线程来获取锁,发现锁已经被占用,他并不会立马阻塞,而是会自旋尝试获取锁,尝试几次后发现依旧获取失败就会阻塞。
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能
偏向锁
轻量级锁在没有竞争的时候,每次重入仍然需要执行CAS操作。CAS操作是有一定开销的,所以
Java6中引入了偏向锁来对轻量级锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的MarkWord头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就属于当前线程。
如下代码:
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
method2();
}
}
public static void method2() {
synchronized( obj ) {
method3();
}
}
public static void method2() {
synchronized( obj ) {
//...
}
}
-
如果是轻量级锁的话以上代码对于每次锁的重入都会生成一个锁记录,并尝试使用CAS将锁记录替换锁对象的MarkWord,虽然只有第一次会比较交换成功,但每次锁重入都会尝试CAS,而CAS操作都会有一定的性能影响
-
而偏向锁就是对以上问题进行优化
- 如果当前线程进行锁重入的时候没有竞争,那么只会在第一次获取锁的时候使用CAS将当前线程的ID替换锁对象的MarkWord
- 后续的锁重入只需要检查对象头中的线程ID是否是自己的即可
对象头格式:
通过biased_lock
字段来判断是否开启偏向锁
|-----------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|-----------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|-----------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|-----------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|-----------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|-----------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|-----------------|
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的
thread、epoch、age 都为 0 - 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -
XX:BiasedLockingStartupDelay=0 来禁用延迟 - 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、
age 都为 0,第一次用到 hashcode 时才会赋值
**调用对象 hashCode **
正常状态对象一开始是没有 hashCode 的,第一次调用才生成 。
调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被
撤销,因为对象头的二进制位有限,不能同时存储hashCode和锁标记
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
锁消除
我们知道Java是编译+解释型语言,在Java中有一个叫做JIT(即时编译器),在对字节码进行解释的时候,发现一段代码被反复调用,就会进行优化,比如下面这个代码,因为锁的对象是一个局部变量,而JVM会认为局部变量是不会被共享的,所以就会将这个synchronized
锁给消除,也就是优化掉,因为这个锁加上去没有必要,不存在竞争的可能。
public class Test {
public void test() {
Object obj = new Object();
synchronized (obj) {
//同步代码块...
}
}
}
小结
- 首先同步代码块没有任何线程来获取的时候,处于无锁状态
- 当有一个线程来获取锁的时候,会使用CAS将线程ID设置到锁对象的MarkWord头,此时会变成偏向锁,后续的进行锁重入就不需要进行CAS来只需要判断对象头是否是当前线程ID即可
- 如果存在又来了一个线程竞争此时发现当前锁偏向为另外一个线程(此时前一个线程已经释放锁的),此时就会将偏向锁变成轻量级锁,而轻量级锁每次获取锁都会在线程的栈帧中保存锁的对象的MarkWord
- 处于轻量级锁的时候线程门都是通过CAS自旋来获取锁,自旋到一定次数就会失败阻塞并将锁升级为重量级锁
wait¬ify原理
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
join原理
join的实现原理有点类似于保护性暂停模式,通过wait来实现的,源码如下:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();//获取当前时间
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
// 无参等待
if (millis == 0) {
// 判断当前线程是否存活
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
// 等待剩余时间
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
AQS
概述
AQS的全称是AbstractQueuedSynchronizer
,翻译过后就是抽象队列同步器。这是一个抽象类位于java.util.concurrent.locks
包下,是阻塞式锁和相关的同步器工具的框架,有许多并发工具都是基于AQS来实现的。
AQS具有以下特点:
- AQS中用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
- getState - 获取 state 状态
- setState - 设置 state 状态
- compareAndSetState - cas 机制设置 state 状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively
实现不可重入锁
ReentrantLock
ReentrantLock也是可重入锁,相对于 synchronized 它具备如下特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
// 获取锁
lock.lock();
try {
// 临界区
} finally {
// 实发锁
lock.unlock();
}
}
ReentranLock其实就是是基于AQS实现的。
非公平锁实现原理
ReentranLock默认就是非公平锁,在无参构造方法中实例化了一个同步器NonfairSync
,该类继承自AQS。
public ReentrantLock() {
sync = new NonfairSync();
}
加锁成功情况,如下是ReentranLock的lock()
方法的非公平锁实现源码,通过CAS机制将state设置为1,如果设置成功说明获取锁成功,并将当前锁的持有值Owner设置为当前线程。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
当有线程来竞争锁的时候:
-
竞争的线程会先尝试使用CAS将state由0改为1,失败就会走else逻辑
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
-
就会进入
tryAcquire
逻辑再次尝试获取锁,如果上一个线程释放了锁那么就会获取锁成功,如果没有释放就会获取锁失败,就会进入addWaiter
逻辑public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
-
addWaiter
方法会构造Node等待队列,将线程存放到等待队列中- 图中绿色圆形表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
- 其中第一个 Node 为哨兵节点,用来占位,并不关联线程
-
接着线程就会进入
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)) { // 进入if说明加锁成功 // 设置当前节点为头节点 setHead(node); // 将上一个头节点的后继置为null p.next = null; // help GC failed = false; return interrupted; } // 将前驱节点的waitStatus修改为-1,第一次返回false // if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
-
acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
-
如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
-
第一次进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,-1状态表示该节点有义务去唤醒其后继节点的线程,这次返回 false,
-
shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时state 仍为 1,失败
-
当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回true,结束for循环
-
进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)
-
如果有多个下次经历上诉过程竞争失败,最后就会变成如图的样子
-
假设Thread0已经释放了锁,调用unlock()
方法流程如下:
-
在
tryRelease
方法中设置exclusiveOwnerThread
锁的持有者为null,并将state设置为0 -
当前队列不为 null,并且 head 的 waitStatus = -1,进入
unparkSuccessor
流程 ,因为head是哨兵节点,并且如果waitStatus为-1,说明其有义务唤醒他的后继节点public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
-
找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread1
-
如果Thrad1被唤醒就会回到
acquireQueued
流程 -
继续进行逻辑判断,此时Thread1的前驱为head就会继续尝试获取锁,因为Thread0已经释放了锁,假设没有线程和Thread1竞争就会获取到锁
-
如果加锁成功(没有竞争)就会设置
- exclusiveOwnerThread 为 Thread1,state = 1
- head 指向刚刚 Thread1 所在的 Node,该 Node 清空 Thread
- 原本的 head 因为从链表断开,而可被垃圾回收
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)) { // 进入if说明加锁成功 // 设置当前节点为头节点 setHead(node); // 将上一个头节点的后继置为null p.next = null; // help GC failed = false; return interrupted; } // 将前驱节点的waitStatus修改为-1,第一次返回false // if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
此时Thread0虽然释放了锁,假设有新的线程Thread4来和Thread1竞争如果不巧又被 Thread4 占了先
- Thread4 被设置为 exclusiveOwnerThread,state = 1
- Thread1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞
锁重入原理
当同一个线程再次对同一把锁进行加锁的时候,其实就是对state进行了+1操作,表示锁重入的次数。源码如下
abstract static class Sync extends AbstractQueuedSynchronizer {
// ...
// 加锁逻辑
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取锁状态
int c = getState();
if (c == 0) {
// 如果为0说明没有线程持有锁,尝试获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// c不为0说明已经有线程已经持有锁,判断持有锁线程是否是当前线程
else if (current == getExclusiveOwnerThread()) {
// 如果是就对 state++
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 释放锁逻辑
protected final boolean tryRelease(int releases) {
// state--
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 重入锁只有当state为0时才释放锁
if (c == 0) {
free = true;//表示释放锁成功
setExclusiveOwnerThread(null);
}
setState(c);
// 返回值表示为true需要唤醒阻塞线程,false则不需要
return free;
}
}
可打断原理
可打断指的是一个线程在等待其他线程释放锁,也就是没有获取到锁被阻塞,为了防止死等导致死锁,就可以使用打断操作
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t = new Thread(()->{
try {
System.out.println("t线程尝试获取锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("t线程等待过程被打断");
return;
}
try {
System.out.println("t线程获取到锁");
} finally {
lock.unlock();
}
});
lock.lock();
System.out.println("主线程获取到锁");
t.start();
try {
Thread.sleep(2000);
// 打断操作
t.interrupt();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
运行结果
主线程获取到锁
t线程尝试获取锁
t线程等待过程被打断
需要注意的是ReentrantLock只有lockInterruptibly()
和tryLock()
方法能被打断,而lock()
方法不能。
不可打断模式
在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了 。
因为执行打断操作的时候,线程还在AQS的等待队列中等待,此时只是将打断标记interrupted
设置为true,并没有执行任何操作,此时还是在执行acquireQueued
方法中的for循环操作,只有当获取到锁之后才返回打断标记,此时已经没有意义了,因为此时线程已经被唤醒继续执行了。
private final boolean parkAndCheckInterrupt() {
// 如果打断标记已经是 true, 则 park 会失效
LockSupport.park(this);
// interrupted 会清除打断标记
return Thread.interrupted();
}
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);
p.next = null; // help GC
failed = false;
// 还是需要获得锁之后,才能返回打断状态
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 如果是因为 interrupt 被唤醒, 返回打断状态为 true
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果打断状态为 true
selfInterrupt();
}
static void selfInterrupt() {
// 重新产生一次中断
Thread.currentThread().interrupt();
}
可打断模式
可打断模式的区别在于并不是设置打断标记,当有其他线程调用了当前线程的interrupt
方法,直接抛出异常让线程中断停止运行了。
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
// 如果没有获得到锁, 进入
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 这时候抛出异常, 而不会再次进入 for (;;)
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
锁超时
使用tryLock()
方法尝试获取锁,失败就会返回false,但使用lock()
方法就是属于死等了,但有的场景需要指定获取锁的时间。
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(()->{
if (!lock.tryLock()) {
System.out.println("t1线程获取锁失败");
// 失败直接返回
return;
}
try {
System.out.println("t1线程获取锁成功");
} finally {
lock.unlock();
}
});
lock.lock();
t1.start();
try {
System.out.println("主线程获取到锁");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
此时就可以使用tryLock()
的带参数方法,指定尝试获取锁的时间
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(()->{
try {
if (!lock.tryLock(1500, TimeUnit.SECONDS)) {
System.out.println("t1线程等待1.5秒后获取锁失败");
// 失败直接返回
return;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
System.out.println("t1线程获取锁成功");
} finally {
lock.unlock();
}
});
lock.lock();
t1.start();
try {
System.out.println("主线程获取到锁");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
System.out.println("主线程释放锁");
}
}
运行结果
主线程获取到锁
主线程释放锁
t1线程获取锁成功
公平锁
synchronized是非公平锁,而ReentrantLock 默认是不公平锁,但他也支持公平锁。非公平锁是当持有锁的线程释放锁后,阻塞等待获取锁的线程是同时争抢锁的,而公平锁是按照哪个线程先进入阻塞队列谁就先获取锁。
公平锁一般没有必要,会降低并发度。
// 公平锁
ReentrantLock lock = new ReentrantLock(true);
公平锁和非公平锁的实现区别其实就在tryAcquire
方法处,公平锁实现就是如果有线程来竞争锁,先会判断AQS等待队列中是否有等待的线程,有的话,并且队列中有除了哨兵节点的线程就不获取锁,
非公平锁的实现源码
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果当前锁没有线程持有
if (c == 0) {
// 直接尝试获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平锁的实现源码
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果当前锁没有线程持有
if (c == 0) {
// 尝试获取锁之前,判断一下
// 判断AQS队列中是否有节点,
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
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;
// h != t 时表示队列中有 Node
return h != t &&
// (s = h.next) == null 表示队列中还有没有老二,因为有一个哨兵节点
((s = h.next) == null
// 或者队列中老二线程不是此线程
|| s.thread != Thread.currentThread());
}
条件变量
synchronized 中也有条件变量,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
synchronized 是那些不满足条件的线程都在一间休息室(waitSet)等消息
而 ReentrantLock 支持多间休息室,也就是支持多个waitSet,可以让执行不同任务的线程进入不同的waitSet。
使用要点:
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行
public class TestLock {
static boolean flag1 = false;
static boolean flag2 = false;
public static void test() throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
// 条件变量1(休息室1)
Condition condition1 = lock.newCondition();
// 条件变量2(休息室2)
Condition condition2 = lock.newCondition();
new Thread(()->{
try {
lock.lock();
System.out.println("尝试获取资源1");
while (!flag1) {
try {
System.out.println("没有获取到资源1阻塞等待");
condition1.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("成功获取到资源1");
}finally {
lock.unlock();
}
}).start();
new Thread(()->{
try {
lock.lock();
System.out.println("尝试获取资源2");
while (!flag2) {
try {
System.out.println("没有获取到资源2阻塞等待");
condition2.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("成功获取到资源2");
}finally {
lock.unlock();
}
}).start();
Thread.sleep(2000);
lock.lock();
try {
// 通知休息室1的线程资源1已经准备好了
flag1 = true;
condition1.signalAll();
}finally {
lock.unlock();
}
Thread.sleep(2000);
lock.lock();
try {
flag2 = true;
condition2.signalAll();
// 通知休息室2的线程资源2已经准备好了
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
test();
}
}
await 流程
每一个条件变量其实对应着一个等待队列,其实现类是ConditionObject
如图,一开始 Thread0持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程
创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread0,加入等待队列尾部
接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁
unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread1 竞争成功
部分源码
// 等待 - 直到被唤醒或打断
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 添加一个 Node 至等待队列
Node node = addConditionWaiter();
// 释放节点持有的锁
long savedState = fullyRelease(node);
int interruptMode = 0;
// 如果该节点还没有转移至 AQS 队列, 阻塞
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 退出等待队列后, 还需要获得 AQS 队列的锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 所有已取消的 Node 从队列链表删除
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
// 添加一个 Node 至等待队列
private Node addConditionWaiter() {
Node t = lastWaiter;
// 添加一个 Node 至等待队列
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 创建一个关联当前线程的新 Node, 添加至队列尾部
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
// 释放锁。并唤醒等待队列的队首线程
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
- 创建一个节点,关联当前线程,并插入到当前Condition队列的尾部
- 调用
fullRelease
,完全释放同步器中的锁 - 唤醒(unpark)AQS队列中的第一个线程
- 调用park方法,阻塞当前线程。
signal 流程
假设 Thread-1 要来唤醒 Thread-0
进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread0 所在 Node
执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的 waitStatus 改为 -1
源码如下:
有些线程在等待过程中可能被打断或者超时,所以可能会唤醒失败所以源码中使用了while循环找到第一个能唤醒的线程,根据state来判断是否为正在处于awaite的线程。
// 必须持有锁才能唤醒, 因此 doSignal 内无需考虑加锁
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
// 唤醒 - 将没取消的第一个节点转移至 AQS 队列
private void doSignal(Node first) {
do {
// 已经是尾节点了
if ( (firstWaiter = first.nextWaiter) == null) {
lastWaiter = null;
}
first.nextWaiter = null;
} while (
// 将等待队列中的 Node 转移至 AQS 队列, 不成功且还有节点则继续循环 ㈢
!transferForSignal(first) &&
// 队列还有节点
(first = firstWaiter) != null
);
}
- 当前持有锁的线程唤醒等待队列中的线程,调用doSignal或doSignalAll方法,将等待队列中的第一个(或全部)节点插入到AQS队列中的尾部。
- 将插入的节点的状态从Condition设置为0,将插入节点的前一个节点的状态设置为-1(有义务去唤醒后一个节点)
- 接着如果持有锁的线程已经释放了锁,就会unpark释放AQS队列中的第一个线程节点来竞争锁
synchronized和ReentrantLock的区别
- synchronized是Java中提供的一个保证线程安全的关键字,而ReentrantLock是JUC包下Lock的一个实现类
- 接着是锁的粒度不同synchronized可以修饰一个类对象或者静态方法,那么锁的就是整个类对象,如果锁的是一个普通对象,那么或者成员方法那么锁的就是一个普通对象。而ReentrantLock是通过lock和unlock来控制锁的粒度的取决于lock实例的生命周期
- 灵活性不同,synchronized的加锁和解锁都是被动的,进入synchronized代码块加锁代码块执行完毕或者是抛出异常后释放锁,而ReentrantLock可以通过lock和unlock方法手动的加锁和解锁
- 并且ReentrantLock还提供了非阻塞获取锁的方式tryLock该方法返回true和false来表示锁是否释放成功,只获取一次锁。并且ReentrantLock在阻塞等待获取锁的时候是可以被打断的
- 此外synchronized是非公平锁,而ReentrantLock提供了公平锁和非公平锁两种实现方式
- synchronized在代码抛出异常时会自动释放锁着是在字节码层面实现的,而ReentrantLock并不会在抛出异常的时候主动释放锁,所以在使用ReentrantLock的时候要配合try/finally来使用
- synchronized在使用wait进入阻塞队列等待synchronized只有一个阻塞队列,而ReentrantLock可以通过newCondition方法创建多个条件变量,也就是多个等待队列,让处理不同任务的线程进入不同的等待队列,后续可以唤醒指定队列的线程
- synchronized底层的锁是通过C++层面的Monitor对象来实现的,而ReentrantLock是通过AQS队列也就是Java层面来实现的