目录
📕 引言 "内存可见性"
我们在最开始讲到线程安全的时候,聊到了关于线程安全问题总共有五种原因,前面我们讲到了三种,还要两种没有涉及到,那么本文章就来聊聊后面两种原因。
由内存可见性引起的线程安全问题
代码:
🌲 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 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间
不同点在于:
-
wait 需要搭配 synchronized 使用. sleep 不需要.
-
wait 是 Object 的方法 sleep 是 Thread 的静态方法