深入解析:JUC(2)线程安全
1. 共享带来的问题
临界区 Critical Section
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
2. synchronized解决方案
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
注意虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}
class Room {
int value = 0;
public void increment() {
synchronized (this) {
value++;
}
}
public void decrement() {
synchronized (this) {
value--;
}
}
public int get() {
synchronized (this) {
return value;
}
}
}
@Slf4j
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int j = 0; j {
for (int j = 0; j < 5000; j++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count: {}" , room.get());
}
}
你只需要处理逻辑,对于共享资源的保护,由我对象内部来实现
3. 方法上的synchronized
class Test{
public synchronized void test() {
}
}
等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
一种是加在普通方法上,锁的是this本对象,一种是加在static方法上锁的是.class整个类
synchronized只能锁对象,它加在方法上锁的也是对象,而不是方法
4. 线程安全分析
比如说HashMap的put、get方法其实都是线程安全的,但是组合成代码块之后,就有可能会出现线程安全问题
其实我们看源码之后会发现,其实都是创建了一个新的String,然后把老的String拷贝到新的里面,也就是说,我们调用这些方法之后得到的String,和之前的String已经是两个不同的char数组了
5. 对象头
对象的组成:
- 对象头 + 实例数据 + 对齐填充字节
- Object Header 64位,8个字节
- 其中4个字节呢是Mark Work,里面存放着一些锁的信息和JVM的信息
- 另外4个字节是Class Word,我们知道每个对象都有一个类型,那对象怎么知道自己是什么类型呢,就是用Class Word指针去指向String类,Integer类等等
- 除了上面的之外,还有4个字节的数组的长度
- hashcode:每个对象都有自己的hashcode进行表示的
- age:就是JVM老年代的晋升阈值年龄
- biased_lock是偏向锁的意思
- 后面两位01(无锁)、00(轻量级锁)、10(重量级锁)、11(GC)是它的加锁状态
- State就是对象的状态,比如Normal的意思就是一个正常状态,没加锁,没有被垃圾回收等等
6. Monitor(锁)
我们一直所说的锁,可以理解为就是这个Monitor
Monitor 被翻译为监视器或管程
- 每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其实例存储在堆中,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁
Monitor对象是操作系统提供的一个对象!
每个 Java 对象都“关联”一个 Monitor(监视器),但这个 Monitor 并不是在对象创建时就一定已经分配好的,而是“按需创建”或“延迟分配”的。
- 线程a正在owner时,其他线程也来竞争当前对象的锁,就会去EntryList中等待(处于阻塞状态)
- 在处于owner中正在活跃的线程a比如遇到一些判断条件(一般是不满足条件的情况然后wait()),然后需要先暂时让出这把锁( wait() )
- 会进入waitSet中等待,然后此时EntrySet中等待的线程b就可以进入owner获取到对象的对象锁...然后线程b
- 执行完任务后,可以通过notify()来唤醒在waitSet中处于等待的线程a来继续拿到这把锁!
7. 轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即使用语法仍然是synchronized
轻量级锁的工作过程:
- 代码进入到同步代码块后,先看对象的锁标志位状态.如果是无锁状态,虚拟机会现在当前栈帧中创建一个Lock Record锁记录的空间,
- 该空间用于存储对象头中的Mark Word的拷贝..然后虚拟机常用CAS操作.将共享对象的对象头中Mark Word中前30个bit替换成指向锁记录的指针.
- 如果操作成功,说明当前线程获取到了对象的锁.并将Mark Word中锁标识位的01变为00.也就将锁对象和线程绑定在一起了.
- 如果操作失败了,说明一定有存在竞争该锁对象的其他的线程,会先检查Mark Word中指向的是否是当前线程栈帧中的锁记录,
- 如果是说明是当前线程获取到了锁,就直接执行代码块.
- 如果不是说明当前对象锁是被其他线程获取到了.此时就会进行锁膨胀升级为重量级锁.且与其竞争的线程进入阻塞状态!
注意可能出现锁重入现象!当前线程又对同一个对象尝试加锁!锁重入会再创建一个锁记录作为重入的计数
轻量级锁:
CAS操作尝试将锁对象Mark Word中替换为指向锁记录的指针.失败后有两种情况:
- 如果发现当前锁对象的Mark Word中是存储的是其他线程锁记录的指针,说明CAS失败,则进行锁膨胀为重量级锁
- 如果发现当前锁对象的Mark Word中还是当前线程,那么会添加一个锁记录作为重入的计数.."锁重入"
锁重入:自己线程又给尝试给锁对象尝试加锁,也就是自己又执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数
8. 锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要锁膨胀,将轻量级变为重量级
轻量级锁的CAS失败后进行锁膨胀后由轻量级锁解锁-->重量级锁解锁过程:
- 某个线程2,CAS操作失败之后,当前线程不会干等,会先为当前锁对象申请一个monitor对象(轻量级锁是没有monitor这个概念的,毕竟没有竞争)
- 然后修改锁对象中Mark Word中前30个bit,由原先指向线程锁记录改为指向这个monitor对象
- 后面线程1执行结束后,因为是轻量级锁,那么解锁肯定会失败,然后就要解锁这个重量级锁. 根据锁记录中存储的锁对象Mark Word的拷贝,然后定位到当前的锁
- 对象中前30bit所指向的monitor对象,然后设置monitor对象中owner为null,且唤醒EntryList中等在阻塞
- 等待的线程2来获取锁....此时重量级锁的解锁流程结束
9. 自旋优化
重量级锁竞争的时候,还可以使用自旋(就是不断重试获取锁)来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞(阻塞比较消耗性能)。
是一种对重量级锁的优化策略,避免了等待线程的阻塞.避免了用户到内核态的转变
当前要求等待时间不能太长 否则会cpu空转过长影响性能,适合于并发竞争没有那么激烈和等待时间没有很长的情境下
当前自旋锁也有优化就是"自适应优化",会根据当前锁对象上一次锁的时间和持有者状态决定此次自旋多久
10. 偏向锁
1. 偏向状态

- 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟( 程序刚刚启动之后往往涉及到很多资源初始化,所以锁的竞争会比较大,偏向锁会延迟 )
- 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、 age 都为 0,第一次用到 hashcode 时才会赋值
禁用:在运行时在添加 VM 参数 -XX:-UseBiasedLocking禁用偏向锁,因为在有些项目中,可能上来就是多线程的锁竞争场景了,这时候偏向锁就不合适了,直接关掉反而节省性能
2. 撤销 - 调用对象hashcode
当一个偏向锁的对象调用了它的hashCode方法,偏向锁会被撤销,这是一个很诡异的现象
为什么会出现这个现象呢,我们还是看一下对象头
hashcode需要31位,但是对象的锁状态为偏向锁的时候,可以看到,线程ID就占了54个字节了,剩下的空间hashcode的31位根本不够放。而对象不调用hashCode方法的时候,是没有hashcode的,默认是0000~~~,只有调用hashCode方法的时候,才会生成hashCode,这个时候就会占用线程ID的54位,否则放不下,那么这个时候就会撤销偏向锁的信息
那为什么轻量锁和重量锁不会出现这种情况呢
轻量锁:hashCode存在线程栈帧的锁记录里面
重量锁:hashCode会放入对象的Monitor锁对象里面,将来解锁的时候会还原回来
而偏向锁没有额外的存储空间了
3. 撤销 - 其他线程使用对象
多线程下,两个线程在不同的时刻访问同一个锁对象,锁对象的偏向状态会被撤销!会在第二个线程访问的时候变成轻量级锁
然后第二个线程访问结束后,会默认撤销偏向锁状态!而是成为001了,无锁状态!!!在后面就不能进行偏向了
其实也就是说,A线程执行完代码之后,B线程过来了,虽然没有竞争,但还是撤销了偏向锁
4. 撤销 - 调用wait / notify
wait / notify只有重量锁才有,你想用这种机制,锁就必须升级
5. 批量重偏向
是一种对"偏向撤销"的优化,认为总是偏向撤销会对性能有一定损耗
当撤销偏向锁阈值超过20次后,jvm会觉得是不是偏向错了,于是会将锁对象的Mark Word的threadId重新进行偏向
偏向了线程t1的锁对象重新偏向了线程t2,称为"重偏向"
在“批量重偏向”的特定场景下,当某个类的偏向锁撤销次数达到阈值(如20次)后,后续该类新创建的对象或符合条件的旧对象,其偏向锁并不会直接升级为轻量级锁,而是会进入“批量重偏向”状态,从而避免了立即升级。
一旦一个对象的偏向锁被撤销并成功升级为轻量级锁(或重量级锁),这个对象的锁状态就“固化”了,不会再退化回偏向锁。但是,批量重偏向机制作用的是那些“尚未被访问过”的、仍然处于偏向状态的对象,而不是已经升级的对象。
假设:
- 有一个
ArrayList
,由线程T1创建并填充了大量对象(比如100个),这些对象的锁都偏向于T1。 - 随后,线程T2要遍历并使用这些对象作为锁。
前几次(< 20次):
- T2尝试获取第1个、第2个...直到第19个对象的锁。
- 每次都会触发正常的偏向锁撤销。
- 撤销后,这些对象的锁通过CAS被T2获取,变成轻量级锁。
- 这个过程开销很大,因为每次撤销都需要进入安全点。
第20次(达到阈值
BiasedLockingBulkRebiasThreshold=20
):- 当第20个对象发生偏向锁撤销时,JVM发现该类(比如
MyObject.class
)的撤销计数器达到了20。 - JVM判定:“这个类的锁经常换主人,说明它不适合长期偏向T1。”
- JVM执行批量重偏向操作:
- 将
MyObject.class
的Epoch(纪元)值+1。 - 注意:此时,之前那19个已经被撤销并升级为轻量级锁的对象,不会变回偏向锁。它们已经“升级”了,路径不可逆。
- 将
- 当第20个对象发生偏向锁撤销时,JVM发现该类(比如
第21次及以后(> 20次):
- T2尝试获取第21个对象的锁。
- JVM检查该对象的Mark Word:
- 发现它仍然偏向T1(因为还没被访问过)。
- 但它的Epoch值与类的当前Epoch不匹配(类的Epoch已经+1了,而这个对象的Epoch还是旧的)。
- 关键点来了:JVM知道这是一个“过期”的偏向锁。
- 此时,JVM不会走“撤销 -> 升级为轻量级锁”的流程!
- 而是直接通过一个简单的CAS操作,将这个锁重偏向(Rebias)给T2。
- 这个过程比完整的撤销和升级快得多,因为它避免了进入安全点等重型操作。
6. 批量撤销
当撤销偏向锁的预制超过40次之后,JVM会这样觉得,自己确实偏向错了,根本就不应该偏向,锁的竞争太激烈了,于是把整个类的所有对象都会变成不可偏向,之后新建的对象也是不可偏向的(101 --> 001)
11. 锁消除
锁消除是JIT即时编译器通过"逃逸分析"为依据而做的一项具体的优化
局部变量不会逃离方法的作用范围,当前加锁操作不会存在多线程的情况,所以即时编译器会检测到,然后进行锁消除
12. wait / notify
就是当线程A条件不满足的时候,后面的线程就会一直阻塞(一直拿着锁,但不干事),这时候线程A就会调用wait方法进入WaitSet里面,先把锁给其他线程使用,当线程A条件满足之后,Owner线程会调用notify方法唤醒线程A,这时候线程重新加入EntryList重新抢夺CPU时间片
API:
- wait() 让线程到 waitSet 等待
- notify() 让正在 waitSet 等待的线程中挑一个唤醒
- notifyAll() 让正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
13. wait / notify 的正确姿势
sleep(long n) 和 wait(long n) 的区别
- sleep是Thread的静态方法,wait是Object的方法
- sleep不需要强制和synchronized配合使用,但wait需要和synchronized配合使用
- sleep在睡眠的时候是不会释放锁的,但是wait在等待的时候会释放锁
注意: 关于notify()时,可能会唤醒WaitSet中不想唤醒的线程. ====> 虚假唤醒的问题
进入wait状态的线程被唤醒后,接着从上次wait()方法下面的代码开始执行
所以某个要使用wait的方法最后使用while循环来判断是否满足条件,不然如果当发生了虚假唤醒的情况,那么明明当前线程条件还未满足,但还是会往wait()方法下面的代码块中去执行,这就是虚假等待问题造成的未满足条件却继续向下执行,所以要使用while来判断,而不是if()
14. 同步设计模式-保护性暂停
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
- JDK 中,join 的实现、Future 的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
其实就是t1发现没有我要的值的时候,就使用wait暂停,然后等t2结果出来之后,赋值给response的同时notify唤醒t1线程
相比join方法的优势是:
- join必须等t2结束,但是保护性暂停模式,只需要为response赋值之后唤醒t1线程就可以了,然后t2线程可以继续向下运行
- 你用join,你等待的变量必须要设置为全局的,而保护性暂停模式等待结果的变量都是局部的
15. join原理
join()方法底层是调用了join(0),底层调用了wait(0); 要一直等待,要线程执行完之后主线程才会继续
join(timeout)方法底层调用wait(delay),内部是一个while循环,每次来判断delay(还需要等待的时间),来决定是否还等待
join()底层使用了wait()方法来实现线程同步,join()方法的内部实现是保护性暂停模式的体现
16.异步设计模式-生产者 / 消费者
- 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
- JDK 中各种阻塞队列,采用的就是这种模式
17. Park & Unpark
unpark可以在park之前调用,也可以在park之后调用
原理: 每个线程都会有一个Parker对象,由三部分组成
18. 线程状态转换
19.多把锁
一个房间内,有不同的功能,线程1想学习,线程2想睡觉,两个功能不冲突,但是线程2却要等待线程1完成,才能拿到锁,这样并发度太低了。
这个时候就可以使用多把锁,控制锁的力度,把学习和睡觉两个功能分开锁上
缺点:一个线程获得多把锁会容易导致死锁
20. 活跃性
就是指,你的线程里面的代码本来是有限的,但是因为某些因素,你的线程代码一直执行不完,这就叫线程的活跃性
活跃性有三种现象:
- 活锁
- 死锁
- 饥饿
1. 死锁
两个线程都各自持有一把锁,又想获取对方的锁,就发生了死锁
如何定位死锁?使用jstack和jconsole
2. 活锁
两个线程互相改变对方线程的结束条件,导致谁也无法结束
如何避免活锁?
- 给发生活锁的线程添加一些随机睡眠时间,保证一个线程先执行结束
- ReentrantLock
3. 饥饿
我们可以使用顺序加锁的方式来解决死锁,但是容易出现饥饿
饥饿定义为:一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束
高优先级或频繁访问的线程“霸占”前置锁
- 假设我们有多个线程操作资源 A 和 B,且规定必须先锁 A 再锁 B。
- 如果有一个线程(或多个)频繁地获取锁 A 和 B,它们会不断重复:
- 现在,有一个“慢”线程 T 想要执行,它也需要先获取锁 A。
- 但如果前面那些“快线程”刚释放 A,立刻又有另一个快线程抢到 A 并继续执行。
- 结果:线程 T 一直等不到锁 A,因为它总被其他线程抢先。
这就像食堂打饭:规定必须先拿盘子再打菜。如果有一群人不断重复“拿盘→打饭→还盘→再拿盘”,而你刚想拿盘,总有人比你快一步,你就一直吃不上饭。
21. ReentrantLock(可重入锁)
与synchronized的不同:
- 可中断 (线程b可以中断获取到锁的线程a,synchronized只能去entryList中一直等待) 可避免死锁
- 可设置超时时间 (线程b等待获取到锁的线程a多少时间后 可以不等了 去干自己的事了) 可避免死锁
- 可以设置为公平锁 (防止线程饥饿的情况发生)
- 可以有多个条件变量 (synchronized中条件不满足时只有一个waitSet等待区 而ReentrantLock有多个)
相同之处:都支持锁重入 (同一个线程获取到同一个锁)
synchronized是关键字,ReentrantLock是对象
1. 可重入
// 可重入锁
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
System.out.println("第一获取到锁");
// 如果是不可重入锁,在释放锁之前,是会被阻塞的
m1();
} finally {
lock.unlock();
}
}
private static void m1() {
lock.lock();
try {
System.out.println("m1锁重入");
m2();
} finally {
lock.unlock();
}
}
private static void m2() {
lock.lock();
try {
System.out.println("m2锁重入");
} finally {
lock.unlock();
}
}
2. 可打断
- 这里可打断是调用一个可打断的api,调用.lock()的线程和synchronized的在等待的时候都不可以倍打断
- 如果想让当前线程在等待的过程中可以被其他线程调用interrupt()方法打断的话
- 在加锁的时候通过lock.lockInterruptibly()方法来加锁
- 关键点:在等待期间,如果另一个线程调用了该等待线程的
interrupt()
方法:- 等待会立即被终止。
- 该方法会抛出
InterruptedException
。 - 线程不会获得锁。
- 中断状态在抛出异常时被清除(这是标准行为)。
3. 锁超时
主动的方式避免死等的一种手段,等待多久还没获取到锁就不等了,执行别的事情了,调用lock.tryLock()的api,tryLock()可以带参数,是等待时间
单线程获取到锁的情况
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();
}
多线程获取锁,超时失败
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
log.debug("获取等待 1s 后失败,返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(2);
} finally {
lock.unlock();
}
4. 使用锁超时解决死锁问题
使用层面可以不适用synchronized,而是使用Lock的一些api,提供了响应中断 超时等api,合理的获取 释放锁
5. 公平锁
公平锁就是,按照你进入阻塞队列的顺序,先进入,先获取锁
ReentrantLock默认是不公平锁,但是可以通过构造方法设置其公平性
公平锁一般没必要,会降低并发度
6. 条件变量
static Lock lock = new ReentrantLock();// 锁对象
static Condition condition = lock.newCondition(); // 锁对象其中的一个休息室
static boolean flag = false;
public static void main(String[] args) {
/**
* 让两个线程,输出2在输出1之前 *
*/
// aWait() signal() 版本
Thread t1 = new Thread(() -> {
lock.lock();
try {
// 和wait()套路都是一样的
while (!flag) {
try {
// 会让当前线程去condition中无限等待,直到这个休息室的signal()唤醒,会继续从这里执行!
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(1);
} finally {
lock.unlock();
}
}, "t1");
Thread t2 = new Thread(() -> {
lock.lock();
try {
System.out.println(2);
flag = true;
condition.signal();
} finally {
lock.unlock();
}
}, "t2");
t1.start();
t2.start();
}
22. 线程间顺序控制
两个线程如何控制先打印2,后打印1
1. wait() notify()版
private static final Object lock = new Object();// 对象锁
private volatile static boolean flag = false;
public static void main(String[] args) {
/**
* 让两个线程,输出2在输出1之前 *
*/
// wait() notify() 版本
/**
* 要一把锁
* 要一个标识
*/
Thread t1 = new Thread(() -> {
synchronized (lock) {
// 不满足条件
while (!flag) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 满足条件了
System.out.println(Thread.currentThread().getName() + ": 1");
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + ": 2");
// 获取到锁
flag = true;
lock.notify();
}
}, "t2");
t1.start();
t2.start();
}
2. await() signal()版
static Lock lock = new ReentrantLock();// 锁对象
static Condition condition = lock.newCondition(); // 锁对象其中的一个休息室
static boolean flag = false;
public static void main(String[] args) {
/**
* 让两个线程,输出2在输出1之前 *
*/
// aWait() signal() 版本
Thread t1 = new Thread(() -> {
lock.lock();
try {
// 和wait()套路都是一样的
while (!flag) {
try {
condition.await(); // 会让当前线程去condition中无限等待,直到这个休息室的signal()唤醒,会继续从这里执行!
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(1);
} finally {
lock.unlock();
}
}, "t1");
Thread t2 = new Thread(() -> {
lock.lock();
try {
System.out.println(2);
flag = true;
condition.signal();
} finally {
lock.unlock();
}
}, "t2");
t1.start();
t2.start();
}
3. park() unpark()版
public static void main(String[] args) {
/**
* 让两个线程,输出2在输出1之前 *
*/
Thread t1 = new Thread(() -> {
LockSupport.park();
System.out.println(1);
}, "t1");
Thread t2 = new Thread(() -> {
System.out.println(2);
LockSupport.unpark(t1);
}, "t2");
t1.start();
t2.start();
}
23. 线程间交替输出
24. 总结
互斥: synchronized Lock
- 保障多线程情况下不会由于上下文切换导致指令交错执行,保障临界区代码的原子性
同步: wait() notify() / Lock的条件变量
- 某一个条件不满足时,让当前线程停下来,然后条件满足了,唤醒让其继续运行
Monior的两大作用
- 互斥(synchronized Lock)
- 同步(wait notify / lock的condition)
synchronized实现monitor是在JVM的层面来实现的,通过C++的代码.但在java层面也实现了monitor,就是ReentrantLock!在java级别实现了阻塞队列啊 waitSet等等
- 保护性暂停模式是1对1,一个线程想要获取另一个线程执行后的结果.
- 生产者消费者模式是多对多,一种生产者和消费者的动态平衡