Bootstrap

【JavaEE初阶】线程安全之内存可见性->volatile,wait(),notify()

目录

📕 引言 "内存可见性"

🌲 volatile 关键字

🚩 volatile能保证内存可见性

🚩 volatile 不保证原子性

🎋 wait 和 notify

🚩wait()方法

🚩notify()方法

🚩notifyAll()方法

🚩理解notify 和 notifyAll

🎄 wait 和 sleep 的对比


📕 引言 "内存可见性"

我们在最开始讲到线程安全的时候,聊到了关于线程安全问题总共有五种原因,前面我们讲到了三种,还要两种没有涉及到,那么本文章就来聊聊后面两种原因。

由内存可见性引起的线程安全问题

代码:

🌲 volatile 关键字

🚩 volatile能保证内存可见性

volatile修饰的变量, 能够保证 “内存可见性”.也就是说被volatile修饰的变量,都不会触发优化到寄存器的操作。

代码:

volatile是告诉编译器,不要触发上述优化(具体在Java中,是让javac生成字节码的时候产生了"内存屏障"相关指令,然后编译器和JVM自然而然的避免了上述的优化操作)。

🚩 volatile 不保证原子性

注意:volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.

synchronized 保证的原子性没有任何关系的,volatile 是专门针对内存可见性的场景来解决问题的,并不能解决之前两个线程循环 count++ 的问题。

必须之前两个线程循环 count++ 的代码:

我们会发现结果最终 count 的值仍然无法保证是 100000,依然存在线程不安全的问题

所以也就说明了volatile不保证原子性

🎋 wait 和 notify

就比如:

银行的ATM,现在呢滑稽老铁要去ATM里面取钱,此时又不止一位,有好几位滑稽老铁来取钱,所以就需要在门口排队

现在比如说1号滑稽老铁就要去ATM里面去取钱了,由于1号滑稽进入ATM在里面上了锁,其他滑稽老铁就在外面阻塞等待。

1号滑稽老铁在里面发现ATM机里面没有钱,他就要退出来,当1号滑稽老铁出来之后,按理说应该是2号滑稽老铁进去,但是1号滑稽老铁出来之后觉得不对,感觉是自己操作错了,就在2号滑稽老铁进去之前,抢先一步又进去了,然后门又锁上了,那么2号滑稽老铁只能继续等待,1号滑稽老铁进去之后发现确实取不出来,就又退出来了,2号滑稽老铁刚要进去,1号滑稽老铁又抢先一步进去了,1号滑稽老铁就在这里进进出出,此时2号滑稽老铁已经后面的滑稽只能继续阻塞等待。

上述问题称为:"线程饿死",注意不是"死锁",只是因为某个线程频繁获取释放锁,由于获取的太快,以至于其他线程捞不着cpu资源。系统中的线程调度是无序的,上述情况很可能出现(不至于长时间进进出出,进出个几十次,还是有可能的)。不会像死锁那样卡死(就是因为有记账信息才没有卡死),但是可能会卡住一下下。对于程序的效率肯定是有影响的。 

那么还是上述例子,当1号滑稽老铁进去之后发现没钱,就释放锁出来主动阻塞等待,这时候2号滑稽就进去了,若2号滑稽进去正好是要存钱,此时2号滑稽释放锁之后,1号滑稽就可以唤醒,进去取钱了。

完成这个协调工作, 主要涉及到三个方法

  • wait() / wait(long timeout): 让当前线程进入等待状态.

  • notify() / notifyAll(): 唤醒在当前对象上等待的线程

注意: wait, notify, notifyAll 都是 Object 类的方法.

所以任何类都可以使用这三个方法

🚩wait()方法

wait 做的事情:

  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)

  • 释放当前的锁

  • 满足一定条件时被唤醒, 重新尝试获取这个锁

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 ( wait(long timeout) 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出InterruptedException 异常

代码:

加锁:

咱们的wait这样的机制,必须要搭配锁进行使用,一定要先获取到锁,才进行wait,执行wait的时候,他在内部就会先解锁在阻塞等待,此时其他线程是能够获取到object这个锁的。

wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常

通过jconsole查看当前状态:

通过另一个线程,调用 notify 来唤醒阻塞的线程。

🚩notify()方法

notify 方法是唤醒等待的线程.

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁
     

代码示例: 使用notify()方法唤醒线程   

  • 创建 thread1线程,里面调用一次wait
  • 创建 thread2线程,里面调用一次notify
  • 注意,thread1 和 thread2 内部持有同一个 Object locke(同一个锁)thread1和thread2 要想配合就需要搭配同一个 Object(同一个对象)

代码:

输入之前:                                                              输入之后:

代码执行分析:

调整:让代码先执行 notify,将输入注释掉

 

代码分析:

🚩notifyAll()方法

更多线程也是类似的情况,只不过 notify 只能唤醒多个等待线程中的一个。

代码:

此时呢t1被唤醒了,t2还在处于一个等待的状态,但是呢在t3里面进行 notify 两次,就可以把t1和t2线程都唤醒了,这里就不再去实现了.......

注意:notify 唤醒的线程是随机的

那么要想指定线程被唤醒,只能创建多个对象,让 wait 和 notify 对同一对象进行阻塞唤醒。

代码:

使用notifyAll方法可以一次唤醒所有的等待线程

代码:

此时可以看到, 调用 notifyAll 能同时唤醒 2 个wait 中的线程

注意: 虽然是同时唤醒 2 个线程, 但是这 2 个线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行,顺序随机,抢占式执行.

wait 操作也提供了一个带超时时间的版本,默认wait是死等(不太好)

代码(上述部分代码修改):  超过1000还没有被 notify 就自动唤醒。

🚩理解notify 和 notifyAll

notify 只唤醒等待队列中的一个线程. 其他线程还是乖乖等着

notifyAll 一下全都唤醒, 需要这些线程重新竞争锁

🎄 wait 和 sleep 的对比

其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间

不同点在于:

  1. wait 需要搭配 synchronized 使用. sleep 不需要.

  2. wait 是 Object 的方法 sleep 是 Thread 的静态方法

;