👏作者简介:大家好,我是笙一X,java大二练习生,喜欢算法和Java相关知识。
📕正进行的系列:算法 、JUC从入门到成神
目录
前言
本文知识较为基础,很多来源于网络,经作者总结归纳,只做学习用途!!!
理解Monitor
我们在学习了 synchronized 之后,有没有考虑过,synchronized 底层是如何控制线程安全的?
在Java中,Monitor通常通过关键字 synchronized 实现,它可以用来修饰方法或代码块。当一个线程进入一个被 synchronized 修饰的方法或代码块时,它会尝试获取对象的锁。如果这个锁已经被其他线程获取了,那么当前线程就会被阻塞,直到获得锁的线程释放锁为止。这样就确保了同一时间只有一个线程可以执行被 synchronized 修饰的代码,从而避免了竞态条件的发生。
我们在详细讲解 Monitor 前,先来介绍一个概念:
Java对象头
在Java虚拟机中,每个对象都有一个对象头(Object Header),它是对象在内存中的一部分,用于存储对象的元数据信息。对象头包含了一些重要的信息,例如对象的哈希码、锁状态、垃圾回收信息等。
Java对象头的具体结构在不同的Java虚拟机实现中可能有所不同,但通常包括以下几个部分:
-
标记字(Mark Word):标记字是对象头的主要部分,用于存储对象的元数据信息,如锁状态、垃圾回收信息等。其中包括了锁信息(存储对象是否被锁定以及锁的类型)、GC相关信息(如对象的分代信息、是否被标记为可回收等)等。标记字的具体结构可以根据具体的Java虚拟机实现而变化。
-
类型指针(Class Metadata Address / Klass Word):指向对象所属类的指针,用于确定对象的类型信息。这个指针指向对象的类元数据,包括类的方法、字段、父类等信息。
对象头的大小和内容会受到Java虚拟机的具体实现和配置的影响,通常在不同的虚拟机版本和不同的JVM参数下可能会有所不同。对象头的存在使得Java虚拟机能够对对象进行有效管理和操作,如垃圾回收、同步等。
以32位虚拟机为例
普通对象
|------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------|-----------------------|
| Mark Word(32 bits) | Klass Word(32 bits) |
|------------------------------|-----------------------|
数组对象
|--------------------------------------------------------------------------------|
| Object Header (96 bits) |
|------------------------------|-------------------------|-----------------------|
| Mark Word(32 bits) | Klass Word(32 bits) | array length(32 bits) |
|------------------------------|-------------------------|-----------------------|
其中 Mark Word 结构为:
注:比较陌生的词(只考虑 Normal 情况下):
age - 分代年龄(GC,垃圾回收中使用,年龄超过一定次数会从幸存区到老年代“JVM知识”)
biased_lock - 偏向锁(后面再讲)
01 / 00 / 10 / 11 - 加锁状态(
-
01:表示对象处于可偏向状态(Biased Locking),此时对象头中存储的是偏向线程的 ID。这表示这个对象的锁被某个线程偏向于此线程,因此没有与 Monitor 关联。
-
00:表示对象处于轻量级锁状态(Lightweight Locking),此时对象头中存储的是指向锁记录(Lock Record)的指针。这表示与该对象关联了一个轻量级锁。
-
10:表示对象处于重量级锁状态(Heavyweight Locking),此时对象头中存储的是指向互斥量(Mutex)的指针。这表示与该对象关联了一个重量级锁。
-
11:表示对象处于 GC 标记状态(GC Marking),此时对象头中存储的是用于标记对象是否被 GC 标记的相关信息。
)
|---------------------------------------------------------|-------------------------------|
| 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 |
|---------------------------------------------------------|-------------------------------|
例如,对于 Integer 对象来说,原本 int 类型占 4 个字节, 而装箱为 Integer 后,多了 对象头 ,所以再多加 8 个字节(32位虚拟机),一共12个字节。
Monitor(锁)
Monitor 被翻译为监视器或管程
注:(很多文章和视频中说Monit由操作系统提供,这并不完全正确。在软件工程中,特别是在并发编程中,"monitor" 是一种同步机制,用于控制对共享资源的访问。在这种情况下,Monitor 是一种高级抽象,通常由编程语言或库提供。操作系统提供的是基本的同步原语,如互斥锁和信号量,而不是直接提供 Monitor。倒不如说是JVM创建的Monitor,JVM(Java虚拟机)在执行Java程序时会使用Monitor。在Java中,Monitor是一种同步机制,用于实现线程之间的互斥访问共享资源。当你在Java代码中使用synchronized
关键字或者使用ReentrantLock
等锁机制时,JVM会创建Monitor来确保线程安全。)
每个Java对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该 对象头的Mark Word中就被设置指向 Monitor 对象的指针
Monitor 结构如下
Owner:当前锁的所有者
EntryList:等待队列 / 阻塞队列
-
刚开始 Monitor 中 Owner 为 null
-
当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个Owner
-
在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED
-
Thread-2执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时候是非公平的(根据JDK底层实现来看)
-
图中 WaitSet 中的 Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING 状态的线程,后面讲 wait-notify 时会分析
注意:
-
synchronized 必须是进入同一个对象的 moniter 才有上述的效果
-
不加 synchronized 的对象不会关联监视器,不遵从以上规则
synchronized 原理进阶
轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块 A
method2();
}
}
public static void method2(){
synchronized(obj){
//同步块 B
}
}
-
创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
-
让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
-
如果 cas 替换成功,对象头中存储了锁记录地址和状态 00,表示由该线程给对象加锁,这时图示如下
-
如果 cas 失败,有两种情况
-
如果是其它线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
-
如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
-
-
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
-
当退出 synchronized 代码块(解锁时),锁记录的值不为null,这时使用 cas 将 Mark Word 的值恢复给对象头
-
成功,则解锁成功
-
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
-
锁膨胀
如果在尝试轻量级加锁的过程中,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 线程
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自选重试成功的情况
线程1(cpu 1上) 对象Mark 线程2(cpu 2上)
— 10(重量锁) —
访问同步块,获取 monitor 10(重量锁)重量锁指针 —
成功(加锁) 10(重量锁)重量锁指针 —
执行同步块 10(重量锁)重量锁指针 —
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 monitor
执行完毕 10(重量锁)重量锁指针 自旋重试
成功(解锁) 01(无锁) 自旋重试
— 10(重量锁)重量锁指针 成功(加锁)
— 10(重量锁)重量锁指针 执行同步块
— ... ...
自选重试失败的情况
线程1(cpu 1上) 对象Mark 线程2(cpu 2上)
— 10(重量锁) —
访问同步块,获取 monitor 10(重量锁)重量锁指针 —
成功(加锁) 10(重量锁)重量锁指针 —
执行同步块 10(重量锁)重量锁指针 —
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 阻塞
... ... ...
-
在 Java 6之后的自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
-
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
-
Java 7 之后不能控制是否开启自旋功能
偏向锁
|---------------------------------------------------------|-------------------------------|
| 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 |
|---------------------------------------------------------|-------------------------------|
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word头,之后发现这个线程 ID(图中的thread) 是自己的就表示没有竞争,不用重新 CAS 。以后只要不发生竞争,这个对象就归该线程所有
例如:
static final Object obj = new Object();
public static void m1(){
synchronized(obj){
//同步块 A
m2();
}
}
public static void m2(){
synchronized(obj){
//同步块 B
m3();
}
}
public static void m3(){
synchronized(obj){
//同步块 C
}
}
偏向状态
回忆一下对象头格式
|----------------------------------------------------------------|-----------------------|
| 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 时才会赋值
注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中。这就是偏向锁的优势,等到下次重入发现Thread相同,就不做cas尝试
测试禁用:
在测试代码运行时再添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁
撤销-调用对象 hashCode
通过测试:
对象.hashCode();
调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,此时hashcode存放后,会无处存放线程id(空间不足),所以如果调用 hashCode 会导致偏向锁被撤销
-
轻量级锁会在锁记录中记录 hashCode
-
重量级锁会在 Monitor 中记录 hashCode
在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking
我们可以思考一下为什么轻量级锁和重量级锁调用hashCode不会有这样的问题?
因为轻量级锁的 hashcode 存在于线程栈帧的锁记录里,重量级锁的 hashcode 存在 monitor 里。
撤销-其它线程使用对象
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
private static void test2() throws InterruptedException{
Dog d = new dog();
Thread t1 = new Thread(() -> {
synchronized(d){
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}
synchronized(TestBiased.class){
TestBiased.class.notify();
}
// 如果不用 wait/notify 使用 join 必须打开下面的注释
// 因为:t1 线程不能结束,否则底层线程可能被 jvm 重用作为 t2 线程,底层线程 id 是一样的
/*try{
System.in.read();
}catch(IOException e){
}*/
}, "t1");
t1.start();
}
撤销-调用 wait / notify
这个我们先不予理会,讲到此方法再做认识
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
当撤销偏向锁阈值超过20次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
private static void test3() throws InterruptedException{
Vector<Dog> list = new Vector<>();
Thread t1 = new Thread(() -> {
for(int i = 0; i < 30; i++){
Dog d = new Dog();
list.add(d);
synchronized(d){
log.debug(i + "t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}
synchronized(list){
list.notify();
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized(list){
try{
list.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
log.debug("=============>");
for(int i = 0; i < 30; i++){
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized(d){
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t2");
t2.start();
}
批量撤销
当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
有无锁的性能差异
比较下面两个方法的性能相差
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iteration=3)
@Measurement(iteration=5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark{
static int x = 0;
@Benchmark
public void a() throws Exception{
x++;
}
@Benchmark
public void b() throws Exception{
Object o = new Object();
synchronized(o){
x++;
}
}
}
因为 JIT 即时编译器的存在,Object o 因为离不开该方法范围,所以syn操作会被优化掉
先打包再运行
java -jar benchmarks.jar
Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 1.542 0.056 ns/op
c.i.MyBenchmark.b avgt 5 1.518 0.091 ns/op
可以看到几乎没有区别,这就是JIT所导致的,syn操作被优化。所以关个开关:不使用锁消除
java -XX:-EliminateLocks -jar benchmarks.jar
Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 1.542 0.056 ns/op
c.i.MyBenchmark.b avgt 5 16.976 1.572 ns/op
可以看到差了十几倍,所以知道加了syn性能会差很多。
wait/notify
原理
-
Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
-
BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用CPU时间片
-
BLOCKED 线程会在 Owner 线程释放锁时唤醒
-
WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入 EntryList重新竞争
API
-
obj.wait() 让进入 object 监视器的线程到waitSet 等待(可带long参数 timeout,表示多少毫秒之后,停止等待,重新进入EntrySet)。其实wait()内部实现的是一个wait(0),表示无期限等待。
还有一个有趣的地方,wait()方法里还可填入除 timeout 参数外另一个int参数 nanos,表示纳秒,但他其实不做纳秒计时,只要其大于0且在一定范围内,作timeout++ 操作。
-
obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒(随机)
-
obj.notifyAll() 让 object上正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
代码展示:
如果不先获取到锁
public class Test3 {
private final static Object OBJECT = new Object();
public static void main(String[] args) {
try {
OBJECT.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
报错
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.sheng1.jucBase.Test3.main(Test3.java:8)
需要获得当前锁
public class Test3 {
private final static Object OBJECT = new Object();
public static void main(String[] args) {
synchronized (OBJECT) {
try {
OBJECT.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
sleep(long n)和 wait(long n)的区别
1)sleep 是Thread 静态方法,而 wait 是 Object 的方法
2)sleep 不需要强制和 synchronized 配合使用,而 wait 需要和 synchronized 一起使用
3)sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁。
4)sleep(long)和wait(long)进入TIMED_WAITING,但wait()会进入WAITING状态
synchronized(lock){
while(条件不成立){
lock.wait();
}
//干活
}
//另一个线程
synchronized(lock){
lock.notifyAll();
}
Park & Unpark
基本使用
它们是 LockSupport 类中的方法
LockSupprot 用来阻塞和唤醒线程,底层实现依赖于 Unsafe 类
//暂停当前线程
LockSupport.park();
//恢复某个线程的运行
LockSupport.unpark(暂停线程对象);
先 park 再 unpark
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(1);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);
与wait/notify很大的区别是unpark可以在park后调用,也可以在park前调用,具体就是改变两个方法运行顺序,resume的日志也能记录
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(2);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
特点
与 Object 的 wait¬ify相比
-
wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 unpark 不必
-
park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
-
park & unpark 可以先 unpark,而wait & notify 不能先 notify
原理
首先要知道,每个线程都有自己的一个Parker对象,内部封装了_counter,_cond,_mutex。
1.当前线程调用 Unsafe.park() 方法
2.检查 _counter,本情况为0,这时,获得 _mutex 互斥锁
3.线程进入 _cond 条件变量阻塞
4.设置 _counter = 0
1.调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2.唤醒 _cond 条件变量中的 Thread_0
3.Thread_0 恢复运行
4.设置 _counter 为 0
1.调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2.当前线程调用 Unsafe.park() 方法
3.检查 _counter,本情况为1,这时线程无需阻塞,继续运行
4.设置 _counter 为 0
多把锁
多把不相干的锁
如果业务间没有关联,可以做粒度锁的细分(分成多把不相干的锁)
将锁的粒度细分
-
好处,是可以增强并发度
-
坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
活跃性
死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1 线程获得 A对象 锁,接下来想获取 B对象 锁
t2 线程获得 B对象 锁,接下来想获取 A对象 锁
例如:
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized(A){
log.debug("lock A");
sleep(1);
sychronized(B){
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized(B){
log.debug("lock B");
sleep(0.5);
sychronized(A){
log.debug("lock A");
log.debug("操作...");
}
}
},"t2");
t1.start();
t2.start();
定位死锁
-
检测死锁可以使用 jconsole 工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:
活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如
public class TestLiveLock
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args){
new Thread(() -> {
//期望减到 0 退出循环
while(count > 0){
sleep(0.2);
count--;
log.debug("count:{}", count);
}
}, "t1").start();
new Thread(() -> {
//期望超过 20 就退出循环
while(count < 20){
sleep(0.2);
count++;
log.debug("count:{}", count);
}
}, "t2").start();
}
饥饿
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
这是一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题
顺序加锁的解决方案
ReentrantLock
相对于 synchronized 它具备如下特点
-
可中断
-
可以设置超时时间
-
可以设置为公平锁
-
支持多个条件变量
与 synchronized 一样,都支持可重入
基本语法
//获取锁
reentrantLock.lock();
try {
//临界区
} finally {
//释放锁
reentrantLock.unlock();
}
可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权力再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1(){
lock.lock();
try {
log.debug("execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2(){
lock.lock();
try {
log.debug("execute method2");
method3();
} finally {
lock.unlock();
}
}
public static void method3(){
lock.lock();
try {
log.debug("execute method3");
} finally {
lock.unlock();
}
}
可打断
在等待锁的过程中,其它线程可以用interrupt()打断
需要用 lockInterruptibly() 锁住
public class Test22 {
private static ReentrantLock lock = new RentrantLock();
private static void main(String[] args) {
new Thread(() -> {
try {
// 如果没有竞争,那么此方法就会获取 lock 对象锁
// 如果有竞争就进入阻塞队列,可以被其它线程用 interrupt() 方法打断
log.debug("尝试获得锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("没有获得锁,返回");
return;
}
try {
log.debug("获取到锁");
} finally {
lock.unlock();
}
}, "t1").start();
}
lock.lock();
t1.start();
sleep(1);
log.debug("打断 t1");
t1.interrupt();
}
锁超时
立即失败
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动...");
if(! lock.tryLock()) {
log.debug("获取立刻失败,返回");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(2);
} finally {
lock.unlock();
}
tryLock()可以设置等待时间
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
if(! lock.tryLock(2, TimeUnit.SECONDS)) {
log.debug("获取不到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("获取不到锁");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
sleep(1);
log.debug("释放了锁");
lock.unlock();
公平锁
ReentrantLock 默认是不公平的
ReentrantLock lock = new ReentrantLock(false); //false 不公平,true 公平
lock.lock();
for(int i = 0; i < 500; i++){
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "running...");
} finally {
lock.unlock();
}
}, "t" + 1).start();
}
// 1s 之后去争抢锁
Thread.sleep(1000);
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "start...");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "running...");
} finally {
lock.unlock();
}
}, "强行插入").start();
公平锁一般没有必要,会降低开发度,后面分析原理时会讲解
条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
-
synchronized 是那些不满足条件的线程都在一间休息室等消息
-
而 ReentrantLock 支持多间休息室,唤醒时也是按休息室来唤醒
使用流程
-
await 前需要获得锁
-
await 执行后,会释放锁,进入 conditionObject 等待
-
await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
-
竞争 lock 锁成功后,从 await 后继续执行
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
//创建一个新的条件变量(休息室)
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
lock.lock();
//进入休息室等待
condition1.await();
condition1.signal();
condition1.signalAll();
}
总结
前期学习的 JUC “锁” 知识大概就是这些,勤加记忆与巩固。我是笙一X,一个正在努力拼搏的人,感谢你的支持,同时期待交流和指导。