深入解析:JUC(2)线程安全

1. 共享带来的问题

临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

2. synchronized解决方案

为了避免临界区的竞态条件发生,有多种手段可以达到目的
  • 阻塞式的解决方案:synchronizedLock
  • 非阻塞式的解决方案:原子变量
我们使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一
时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁
的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
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

轻量级锁的工作过程:

  1. 代码进入到同步代码块后,先看对象的锁标志位状态.如果是无锁状态,虚拟机会现在当前栈帧中创建一个Lock Record锁记录的空间,
  2. 该空间用于存储对象头中的Mark Word的拷贝..然后虚拟机常用CAS操作.将共享对象的对象头中Mark Word中前30个bit替换成指向锁记录的指针.
  3. 如果操作成功,说明当前线程获取到了对象的锁.并将Mark Word中锁标识位的01变为00.也就将锁对象和线程绑定在一起了.
  4. 如果操作失败了,说明一定有存在竞争该锁对象的其他的线程,会先检查Mark Word中指向的是否是当前线程栈帧中的锁记录,
  5. 如果是说明是当前线程获取到了锁,就直接执行代码块.
  6. 如果不是说明当前对象锁是被其他线程获取到了.此时就会进行锁膨胀升级为重量级锁.且与其竞争的线程进入阻塞状态!    

注意可能出现锁重入现象!当前线程又对同一个对象尝试加锁!锁重入会再创建一个锁记录作为重入的计数

轻量级锁

CAS操作尝试将锁对象Mark Word中替换为指向锁记录的指针.失败后有两种情况:

  1. 如果发现当前锁对象的Mark Word中是存储的是其他线程锁记录的指针,说明CAS失败,则进行锁膨胀为重量级锁
  2. 如果发现当前锁对象的Mark Word中还是当前线程,那么会添加一个锁记录作为重入的计数.."锁重入"

锁重入:自己线程又给尝试给锁对象尝试加锁,也就是自己又执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数

8. 锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要锁膨胀,将轻量级变为重量级

轻量级锁的CAS失败后进行锁膨胀后由轻量级锁解锁-->重量级锁解锁过程:

  1. 某个线程2,CAS操作失败之后,当前线程不会干等,会先为当前锁对象申请一个monitor对象(轻量级锁是没有monitor这个概念的,毕竟没有竞争)
  2. 然后修改锁对象中Mark Word中前30个bit,由原先指向线程锁记录改为指向这个monitor对象
  3. 后面线程1执行结束后,因为是轻量级锁,那么解锁肯定会失败,然后就要解锁这个重量级锁. 根据锁记录中存储的锁对象Mark Word的拷贝,然后定位到当前的锁
  4. 对象中前30bit所指向的monitor对象,然后设置monitor对象中owner为null,且唤醒EntryList中等在阻塞
  5. 等待的线程2来获取锁....此时重量级锁的解锁流程结束

9. 自旋优化

重量级锁竞争的时候,还可以使用自旋(就是不断重试获取锁)来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞(阻塞比较消耗性能)。

是一种对重量级锁的优化策略,避免了等待线程的阻塞.避免了用户到内核态的转变
当前要求等待时间不能太长 否则会cpu空转过长影响性能,适合于并发竞争没有那么激烈和等待时间没有很长的情境下
当前自旋锁也有优化就是"自适应优化",会根据当前锁对象上一次锁的时间和持有者状态决定此次自旋多久

10. 偏向锁

1. 偏向状态

轻量级锁: 每次 锁重入都需要CAS操作.每次锁重入时都会产生一个锁记录,然后尝试用CAS将Mark Word替换成指向锁记录的指针
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS线程 ID设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

一个对象创建时:
  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epochage 都为 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要遍历并使用这些对象作为锁。
  1. 前几次(< 20次)

    • T2尝试获取第1个、第2个...直到第19个对象的锁。
    • 每次都会触发正常的偏向锁撤销
    • 撤销后,这些对象的锁通过CAS被T2获取,变成轻量级锁
    • 这个过程开销很大,因为每次撤销都需要进入安全点。
  2. 第20次(达到阈值 BiasedLockingBulkRebiasThreshold=20

    • 当第20个对象发生偏向锁撤销时,JVM发现该类(比如MyObject.class)的撤销计数器达到了20。
    • JVM判定:“这个类的锁经常换主人,说明它不适合长期偏向T1。”
    • JVM执行批量重偏向操作:
      • MyObject.classEpoch(纪元)值+1。
      • 注意:此时,之前那19个已经被撤销并升级为轻量级锁的对象,不会变回偏向锁。它们已经“升级”了,路径不可逆。
  3. 第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 对象的方法。必须获得此对象的锁,才能调用这几个方法

wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止
wait(long n)有时限的等待, n 毫秒后结束等待,或是被 notify

13. wait / notify 的正确姿势

sleep(long n) 和 wait(long n) 的区别

  1. sleep是Thread的静态方法,wait是Object的方法
  2. sleep不需要强制和synchronized配合使用,但wait需要和synchronized配合使用
  3. sleep在睡眠的时候是不会释放锁的,但是wait在等待的时候会释放锁

注意: 关于notify()时,可能会唤醒WaitSet中不想唤醒的线程.   ====>   虚假唤醒的问题

进入wait状态的线程被唤醒后,接着从上次wait()方法下面的代码开始执行

所以某个要使用wait的方法最后使用while循环来判断是否满足条件,不然如果当发生了虚假唤醒的情况,那么明明当前线程条件还未满足,但还是会往wait()方法下面的代码块中去执行,这就是虚假等待问题造成的未满足条件却继续向下执行,所以要使用while来判断,而不是if()

14. 同步设计模式-保护性暂停

Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点:
  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
  • JDK 中,join 的实现、Future 的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式

​​​​

其实就是t1发现没有我要的值的时候,就使用wait暂停,然后等t2结果出来之后,赋值给response的同时notify唤醒t1线程

相比join方法的优势是:

  1. join必须等t2结束,但是保护性暂停模式,只需要为response赋值之后唤醒t1线程就可以了,然后t2线程可以继续向下运行
  2. 你用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. 活锁
  2. 死锁
  3. 饥饿

1. 死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁
t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁

两个线程都各自持有一把锁,又想获取对方的锁,就发生了死锁

如何定位死锁?使用jstackjconsole

2. 活锁

两个线程互相改变对方线程的结束条件,导致谁也无法结束

如何避免活锁?

  1. 给发生活锁的线程添加一些随机睡眠时间,保证一个线程先执行结束
  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,一个线程想要获取另一个线程执行后的结果.
  • 生产者消费者模式是多对多,一种生产者和消费者的动态平衡

posted @ 2025-09-14 10:25  yjbjingcha  阅读(5)  评论(0)    收藏  举报