- synchronized
- wait-notify
- park-unpark
- ReentrantLock
锁
★synchronized
**在成员方法上加锁等于锁住当前对象
this
**:1
2
3
4
5
6
7
8
9
10
11
12
13class Test {
public synchronized void test() {
}
}
等价于
class Test {
public void test() {
synchronized(this) {
}
}
}**在静态方法上加锁等于锁住类对象
clazz
**:1
2
3
4
5
6
7
8
9
10
11
12
13class Test {
public synchronized static void test() {
}
}
等价于
class Test {
public static void test() {
synchronized(Test.class) {
}
}
}
Monitor(重量级锁)
Monitor 被翻译为监视器或管程。
每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其实例存储在堆中,如果使用 synchronized 给对象上锁(重量级)之后,该对象的对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁(Heavyweight Locked)。
Mark Word 结构:最后两位是锁标志位。
64 位虚拟机 Mark Word:
synchronized 锁的原理:
开始时 Monitor 中 Owner 为 null。
当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner,obj 对象的 Mark Word 指向 Monitor,把对象原有的 MarkWord 内容存入线程栈中的锁记录中(轻量级锁部分详解)。
在 Thread-2 上锁后,Thread-1、Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),它们发现 obj 的 Mark Work 已经指向了一个 Monitor,并进一步发现该 Monitor 的 Owner 已经被 Thread-2 占用了,那这些线程就会进入 EntryList 等待,状态为 BLOCKED。
Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置回 Mark Word。
唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞。
WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制,后面再说)。
- 注意:
- synchronized 必须是进入同一个对象的 Monitor 才有上述的效果
- 不加 synchronized 的对象不会关联监视器,不遵从以上规则
- 注意:
synchronized 是可重入、不公平的重量级锁,所以可以对其进行优化。
1
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 // 随着竞争的增加,只能锁升级,不能降级
轻量级锁
一个对象有多个线程要加锁,但加锁的时间是错开的(没有竞争),可以使用轻量级锁来优化,轻量级锁对使用者是透明的(不可见)
- 轻量级锁在没有竞争时(锁重入时),每次重入仍然需要执行 CAS 操作,Java 6 才引入的偏向锁来优化
轻量级锁加锁流程:
创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,存储锁定对象的 Mark Word。
让锁记录中 Object reference 指向锁住的对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入线程栈中的锁记录。
如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁。
如果 CAS 失败,有两种情况:
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程。
- 如果是线程自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数。
当退出 synchronized 代码块(解锁时):
- 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1。
- 如果锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀(轻量级锁->重量级锁)
在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁。
Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,通过 Object 对象头获取到持锁线程,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED。
当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。
自旋锁(重量级锁竞争时)
重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用自旋(默认 10 次)来进行优化,采用循环的方式去尝试获取锁。如果当前线程自旋成功,即此时持锁线程已经退出了同步块,释放了锁,这时当前线程就可以避免阻塞。
注意:
- 自旋占用 CPU 时间,单核 CPU 自旋就是浪费时间,因为同一时刻只能运行一个线程,多核 CPU 自旋才能发挥优势。
- 自旋失败的线程会进入阻塞状态。
优点:不会进入阻塞状态,减少线程上下文切换的消耗。
缺点:当自旋的线程越来越多时,会不断的消耗 CPU 资源。
自旋锁情况:
自旋成功的情况:
自旋失败的情况:
注意:
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能
- Java 7 之后不能控制是否开启自旋功能,由 JVM 控制
偏向锁
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作:
当锁对象第一次被线程获得的时候进入偏向状态,标记为 101,同时使用 CAS 操作将线程 ID 记录到 Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定或轻量级锁状态
一个对象创建时:
如果开启了偏向锁(默认开启),那么对象创建后,MarkWord 值为 0x05 即最后 3 位为 101,thread、epoch、age 都为 0。
如果没有开启偏向锁,那么对象创建后,mark word 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为0,第一次用到 hashcode 时才会赋值。
- 当一个对象已经计算过 hashCode,就再也无法进入偏向状态了,从对象头的格式可以看出来,如果计算过 hashCode,那 54 位的线程 ID 已经存不下了,所以该对象不能再用偏向锁了。
偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数
-XX:BiasedLockingStartupDelay=0
来禁用延迟。JDK 8 延迟 4s 开启偏向锁原因:在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低添加 VM 参数
-XX:-UseBiasedLocking
禁用偏向锁
撤销偏向锁
- 撤销偏向锁的状态:
- 调用对象的 hashCode:偏向锁的对象 MarkWord 中存储的是线程 id,调用 hashCode 将导致偏向锁被撤销,偏向锁升级为轻量级锁
- 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
- 调用 wait/notify,需要申请 Monitor,进入 WaitSet,偏向锁升级为重量级锁
- 批量撤销:如果对象被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
- 批量重偏向:当撤销偏向锁阈值超过 20 次后,JVM 会觉得是不是偏向错了,于是在给这些对象加锁时重新偏向至加锁线程
- 批量撤销:当撤销偏向锁阈值超过 40 次后,JVM 会觉得自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
锁消除
- 锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除,这是 JVM 即时编译器的优化
- 锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM 逃逸分析)
wait-notify
基本使用
需要获取对象锁后才可以调用
锁对象.wait()
,notify 随机唤醒一个线程,notifyAll 唤醒所有线程去竞争 CPU。- 说明:wait 是挂起线程,需要唤醒的都是挂起操作,阻塞线程可以自己去争抢锁,挂起的线程需要唤醒后再去争抢锁。
Object 类 API:
1
2
3
4public final void notify():唤醒正在等待对象监视器的单个线程。
public final void notifyAll():唤醒正在等待对象监视器的所有线程。
public final void wait():导致当前线程等待,直到另一个线程调用该对象的 notify() 方法或 notifyAll()方法。
public final native void wait(long timeout):有时限的等待, 到n毫秒后结束等待,或是被唤醒- 它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法,也就是说线程必须成为 Monitor 的 Owner 之后,上面的方法调用才有效。
对比 sleep():
- 原理不同:sleep() 方法是属于 Thread 类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait() 方法属于 Object 类,用于线程间通信
- 对锁的处理机制不同:调用 sleep() 方法的过程中,线程不会释放对象锁,当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池(不释放锁其他线程怎么抢占到锁执行唤醒操作),但是都会释放 CPU
- 使用区域不同:wait() 方法必须放在同步控制方法和同步代码块(先获取锁)中使用,即需要和 synchronized 一起使用,而 sleep() 方法可以放在任何地方使用
底层原理
Owner 线程发现当前继续向下执行的条件不满足,则调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,唤醒后并不意味者立刻获得锁,需要进入 EntryList 重新竞争
虚假唤醒
notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程
解决方法:采用 notifyAll
notifyAll 仅解决某个线程的唤醒问题,使用 if + wait 判断仅有一次机会,一旦条件不成立,无法重新判断
解决方法:用 while + wait,当条件不成立,再次 wait
1
2
3
4
5
6
7
8
9
10
11
12private static final Object lock = new Object();
synchronized(lock) {
while (条件不成立) {
lock.wait();
}
// 向下执行代码...
}
// 另一个线程
synchronized(lock) {
lock.notifyAll();
}
park-unpark
基本使用
LockSupport
是用来创建锁和其他同步类的线程原语。LockSupport
类方法:LockSupport.park()
:暂停当前线程,挂起原语LockSupport.unpark(暂停的线程对象)
:恢复某个线程的运行
LockSupport
出现就是为了增强 wait & notify 的功能:- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park、unpark 不需要
- park & unpark 以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify。类比生产消费,先消费发现有产品就消费,没有就等待;先生产就直接产生商品,然后线程直接消费
- wait 会释放锁资源进入等待队列,park 不会释放锁资源,只负责阻塞当前线程,会释放 CPU
底层原理
先 park 再 unpark:
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 0,这时获得 _mutex 互斥锁
- 线程进入 _cond 条件变量挂起
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 唤醒 _cond 条件变量中的 Thread_0,Thread_0 恢复运行,设置 _counter 为 0
先 unpark 再 park:
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 1,这时线程无需挂起,继续运行,设置 _counter 为 0
线程状态转换

- 假设有线程 t
情况 1 NEW —> RUNNABLE
- 当调用 t.start() 方法时,由 NEW —> RUNNABLE
情况 2 RUNNABLE —> WAITING
- t 线程用
synchronized(obj)
获取了对象锁后- 调用
obj.wait()
方法时,t 线程从 RUNNABLE —> WAITING - 调用
obj.notify()
,obj.notifyAll()
,t.interrupt()
时- 竞争锁成功,t 线程从 WAITING —> RANNABLE
- 竞争锁失败,t 线程从 WAITING —> BLOCKED
- 调用
情况 3 RUNNABLE —> WAITING
- 当前线程调用
t.join()
方法时,当前线程从 RUNNABLE —> WAITING- 注意是当前线程在 t 线程对象的监视器上等待
- t 线程运行结束,或调用了当前线程的
interrupt()
时,当前线程从 WAITING —> RUNNABLE
情况 4 RUNNABLE —> WAITING
- 当前线程调用
LockSupport.park()
方法会让当前线程从 RUNNABLE —> WAITING - 调用
LockSupport.unpark(目标线程)
或调用了线程的interrupt()
,会让目标线程从 WAITING —> RUNNABLE
情况 5 RUNNABLE —> TIMED_WAITING
- t 线程用
synchronized(obj)
获取了对象锁后- 调用
obj.wait(long n)
方法时,t 线程从 RUNNABLE —> TIMED_WAITING - t 线程等待时间超过了 n 毫秒,或者调用
obj.notify()
,obj.notifyAll()
,t.interrupt()
时- 竞争锁成功,t 线程从 TIMED_WAITING —> RANNABLE
- 竞争锁失败,t 线程从 TIMED_WAITING —> BLOCKED
- 调用
情况 6 RUNNABLE —> TIMED_WAITING
- 当前线程调用
t.join(long n)
方法时,当前线程从 RUNNABLE —> TIMED_WAITING- 注意是当前线程在 t 线程对象的监视器上等待
- 当前线程等待时间超过了 n 毫秒,或者 t 线程运行结束,或调用了当前线程的
interrupt()
时,当前线程从 TIMED_WAITING —> RUNNABLE
情况 7 RUNNABLE —> TIMED_WAITING
- 当前线程调用
Thread.sleep(long n)
,当前线程从 RUNNABLE —> TIMED_WAITING - 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING —> RUNNABLE
情况 8 RUNNABLE —> TIMED_WAITING
- 当前线程调用
LockSupport.parkNanos(long nanos)
或LockSupport.parkUntil(long millis)
时,当前线程从 RUNNABLE —> TIMED_WAITING - 调用
LockSupport.unpark(目标线程)
或调用了线程的interrupt()
,或是等待超时,会让目标线程从 TIMED_WAITING —> RUNNABLE
情况 9 RUNNABLE —> BLOCKED
- t 线程用
synchronized(obj)
获取对象锁时如果竞争失败,则从 RUNNABLE —> BLOCKED - 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,则从 BLOCKED —> RUNNABLE,其它失败的线程仍然 BLOCKED
情况 10 RUNNABLE —> TERMINATED
- 当前线程所有代码运行完毕,进入TERMINATED
★ReentrantLock
- ReentrantLock 相对于 synchronized 具备如下特点:
- 锁的实现:synchronized 是 JVM 实现的,使用关键字
synchronized
来使用。而 ReentrantLock 是 JDK 实现的,要创建ReentrantLock
对象来使用。 - 性能:新版本 Java 对 synchronized 进行了很多优化,synchronized 与 ReentrantLock 大致相同
- 使用:ReentrantLock 需要手动解锁,synchronized 执行完代码块自动解锁
- 可中断:ReentrantLock 可中断,而 synchronized 不行
- 公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
- ReentrantLock 可以设置公平锁,synchronized 中的锁是非公平的
- 不公平锁的含义是阻塞队列内公平,队列外非公平
- 锁超时:尝试获取锁,超时获取不到直接放弃,不进入阻塞队列
- ReentrantLock 可以设置超时时间,synchronized 会一直等待
- 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象,更细粒度的唤醒线程
- 两者都是可重入锁
- 锁的实现:synchronized 是 JVM 实现的,使用关键字
基本使用
构造方法:
ReentrantLock lock = new ReentrantLock();
ReentrantLock 类 API:
public void lock()
:获得锁- 如果锁没有被另一个线程占用,则将锁定计数设置为 1
- 如果当前线程已经保持锁定,则保持计数增加 1
- 如果锁被另一个线程保持,则当前线程被禁用线程调度,并且在锁定已被获取之前处于休眠状态
public void unlock()
:尝试释放锁- 如果当前线程是该锁的持有者,则保持计数递减
- 如果保持计数现在为零,则锁定被释放
- 如果当前线程不是该锁的持有者,则抛出异常
1
2
3
4
5
6
7
8
9ReentrantLock reentrantLock = new ReentrantLock();
try {
// 获取锁
reentrantLock.lock();
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
可重入
- 可重入是指同一个线程如果首次获得了这把锁,那么它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住,直接造成死锁。
- 用 lock 方法加两把锁会是什么情况呢?
- 加锁两次解锁两次:正常执行
- 加锁两次解锁一次:程序直接卡死,线程不能出来,也就说明申请几把锁,最后需要解除几把锁
- 加锁一次解锁两次:运行程序会直接报错
可打断
public void lockInterruptibly()
:获得可打断的锁- 如果没有锁竞争,此方法就会获取 lock 对象锁
- 如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 打断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
System.out.println("尝试获取锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("没有获取到锁,被打断,直接返回");
return;
}
try {
System.out.println("获取到锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
Thread.sleep(2000);
System.out.println("主线程进行打断锁");
t1.interrupt();
}- 注意:如果是不可中断模式
lock.lock()
,那么即使使用了 interrupt 也不会让等待状态中的线程中断
锁超时
public boolean tryLock()
:尝试获取锁,获取到返回 true,获取不到直接放弃,不进入阻塞队列public boolean tryLock(long timeout, TimeUnit unit)
:在给定时间内获取锁,获取不到就退出1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
if (!lock.tryLock(2, TimeUnit.SECONDS)) {
System.out.println("获取不到锁");
return;
}
} catch (InterruptedException e) {
System.out.println("被打断,获取不到锁");
return;
}
try {
log.debug("获取到锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
System.out.println("主线程获取到锁");
t1.start();
Thread.sleep(1000);
try {
System.out.println("主线程释放了锁");
} finally {
lock.unlock();
}
}- 注意:tryLock 期间也可以被打断
公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
- ReentrantLock 可以设置公平锁,synchronized 中的锁是非公平的
- 不公平锁的含义是阻塞队列内公平,队列外非公平
构造方法:
ReentrantLock lock = new ReentrantLock(true)
1
2
3public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}ReentrantLock 默认是不公平的:
1
2
3public ReentrantLock() {
sync = new NonfairSync();
}说明:公平锁一般没有必要,会降低并发度。
条件变量
synchronized 的条件变量,是当条件不满足时进入 WaitSet 等待;ReentrantLock 的条件变量比 synchronized 强大之处在于支持多个条件变量。
ReentrantLock 类获取 Condition 对象:
public Condition newCondition()
Condition 类 API:
void await()
:当前线程从运行状态进入等待状态,释放锁void signal()
:唤醒一个等待在 Condition 上的线程,但是必须获得与该 Condition 相关的锁
使用流程:
- await / signal 前需要获得锁
- await 执行后,会释放锁进入 ConditionObject 等待
- await 的线程被唤醒(或打断,或超时)去重新竞争 lock 锁
- 线程在条件队列被打断会抛出中断异常
- 竞争 lock 锁成功后,从 await 后继续执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
//创建一个新的条件变量
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
new Thread(() -> {
try {
lock.lock();
System.out.println("进入等待");
//进入休息室等待
condition1.await();
System.out.println("被唤醒了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
Thread.sleep(1000);
//叫醒
new Thread(() -> {
try {
lock.lock();
//唤醒
condition1.signal();
// condition2.signal(); // 用condition2是不能唤醒的,因为之前的线程是在condition1中等待
} finally {
lock.unlock();
}
}).start();
}