目录
1.内存可见性问题-引入
2.volatile关键字
3.从java内存模型的角度内存可见性问题
4.wait()和notify()方法
4.1 wait()方法
4.2 notify()方法
wait与sleep的区别(面试题)
4.3 notifyAll()方法
4.4 多个线程使用wait和notify方法
1.内存可见性问题-引入
构造一个myCounter类,成员flag,让t1线程中的循环条件为新创建的对象flag,让t2通过输入的整型值,控制flag的值,若非0,则t1循环应该终止!
class myCounter{
public int flag = 0;
}
public class ThreadDemo16 {
public static void main(String[] args) {
myCounter mycounter = new myCounter();
Thread t1 = new Thread(()->{
while(mycounter.flag==0){
}
System.out.println("t1循环执行结束");
});
Thread t2 = new Thread(()->{
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个整数:");
mycounter.flag = sc.nextInt();
});
t1.start();
t2.start();
}
}
我们先来看两个线程的状态:
t1线程是RUNNABLE状态
因为还未输入,t2其实还是阻塞状态
当我们输入一个非0值后:
此时t2线程已经执行结束,t1线程还在RUNNABLE状态!!
可以看到并没有打印"t1循环执行结束".我们预期的是t2改动了flag值,t1就应该结束循环了,但此时t1明显没有结束循环
这个问题就是"内存可见性问题"!因为这里的结果并不是我们预期的,所以也是一个Bug,这也是一个线程安全问题!
这里使用汇编指令理解,大致分为两步操作:
1.load,把内存中的flag的值读取到寄存器中
2.cmp,把寄存器的值和0进行比较,根据比较结果,决定下一步的跳转(这两步是一个循环,执行速度极快)循环执行次数这么多,t2真正修改flag之前,load得到的结果都是一样的,另一方面,load的操作和cmp操作相比,速度则非常慢!!(CPU针对寄存器的操作,要比内存操作快很多,快3~4个数量级,计算机对于内存的操作,比硬盘快3~4个数量级)
由于load执行的速度相对于cmp比较慢,再加上反复load的结果相同,JVM就做出了一个优化,不再重复load,判定没有人修改flag的值,于是只读一次就好了
不再重复load是编译器的优化 的一种方式,但是实际上是有人修改的,因此由于编译器的判断失误导致出现了bug
内存可见性问题
一个线程针对一个变量进行读取操作,另一个线程针对这个变量进行修改,此时线程读到的值,不一定是修改过后的值!!读线程没有感受到线程的改动!!归根结底还是编译器/JVM在多线程环境下优化时产生了误判!
此时就需要程序员手动干预了
2.volatile关键字
如何手动干预编译器/JVM的优化呢?
此时,给flag变量加上volatile 关键字就可以了,这个单词意思是可变的,容易失去的.它表示这个变量时"可变的",编译器每一次都要重新读取这个变量的内存内容,任意时间这个变量的值可能改变,编译器不能对它进行优化!!!
给flag变量加上volatile 关键字
结果
优化虽然能让速度提升起来,但是容易引发各种各样的问题!
这个关键字只能修饰变量,
不能修饰方法里的局部变量,局部变量出了方法就没了,只能在线程里边使用,不能多线程之间同时读取/修改,天然的规避了线程安全问题!
每个线程都有自己的"栈空间",方法内部的变量在"栈"这样的内存空间上(栈就是记录方法之间的调用关系),即使是同一个方法不同的线程调用,方法内的局部变量也在不同的栈空间中,本质上还是不同的变量,那么也不会涉及到多个线程读取/修改同一个变量的情况,不会出现内存可见性问题
上面的内存可见性问题也不是始终会出现,就是可能会误判,如果加个sleep,我们看结果
结果
这里结果正确了,sleep控制了循环的速度,编译器错误的优化也消失了,但是我们不知道编译器什么时候会优化,在应用程序方面无法感知,最稳妥的方法还是加上volatile
3.从java内存模型的角度内存可见性问题
java程序里,内存,每个线程还有自己的"CPU和寄存器"都是不同的,t1线程进行读取的时候,只是读取了t1线程的"CPU 寄存器"的值,t2线程进行修改的时候,先修改的是"CPU 寄存器"中的值,然后再把这个值同步到内存中,但是由于编译器优化,t1没有重新从内存中同步数据到"CPU 寄存器"中,读到的结果就是"修改之前的值"
主内存:main memory 贮存,也叫做内存
工作内存:work memory 工作存储区,不是内存,而是cpu上存储数据的单元(这里是存储器,还有其他东西(cache等))
这里的工作内存还不光指寄存器,还可能是高速缓冲器,cpu读取寄存器,速度比读取内存快太多了,为了减小这个差距,引入了cache,cache是指可以进行高速数据交换的存储器,它先于内存与CPU交换数据,因此速率很快
缓存的工作原理
当CPU要读取一个数据时,首先从CPU缓存中查找,找到就立即读取并送给CPU处理;没有找到,就从速率相对较慢的内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。正是这样的读取机制使CPU读取缓存的命中率非常高(大多数CPU可达90%左右),也就是说CPU下一次要读取的数据90%都在CPU缓存中,只有大约10%需要从内存读取。这大大节省了CPU直接读取内存的时间,也使CPU读取数据时基本无需等待。总的来说,CPU读取数据的顺序是先缓存后内存
相比于cpu和内存,它的存储空间居中,读写速度居中,成本居中,当CPU需要读到一个内存数据的时候,可能直接从内存读,也能从cache中的缓存,也可能读寄存器中的数据
因此硬件结构更复杂了,工作存储区=cpu寄存器+cpu cache,为了表述简单,直接就用"工作内存"代替了!
4.wait()和notify()方法
由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序
多线程引起的不安全问题往往是因为抢占式执行,随即调度,因此需要程序员来控制线程之间的执行顺序,虽然线程在内核里的调度是随机的,但是可以让线程阻塞,主动给放弃CPU,让别的线程先执行
例如,t1线程和t2线程,先让t1线程执行,t2线程先wait(阻塞),等待t1执行一部分,然后通过notify通知t2,唤醒t2,让t2执行(使用sleep和join也可以,但是wait和notify能更好地解决问题)
使用join,必须t1先执行完,t2才能执行.如果想让t1执行一般,t2就执行,join做不到!
使用sleep,是指定一个休眠时间,但是无法知道t1具体得花费多少时间不好估计,容易出现偏差
wait,notify,notifyAll这几个方法都是Object类的方法,java中的所有类都是继承于这个类的,因此任意对象都有这三个方法!
4.1 wait()方法
看一个案例
public class ThreadDemo17 {
public static void main(String[] args) throws InterruptedException {
Object ob = new Object();
ob.wait();
}
}
这里出发了不合法的监视器(synchronized)状态异常,锁的状态就分为加锁和解锁状态,非法就是预期的是什么状态,结果是另一个状态,产生异常就要直到wait干了什么事
wait的功能
释放当前的锁
使当前执行代码的线程进行等待. (把线程放到等待队列中)
满足一定条件时被唤醒, 重新尝试获取这个锁
当前ob对象没有加锁,因此wait还无法释放锁,就会产生非法的锁状态异常
public static void main(String[] args) throws InterruptedException {
Object ob = new Object();
System.out.println("wait之前");
synchronized (ob){
ob.wait();
}
System.out.println("wait之后");
}
我们看到wait之后线程就阻塞了,不会再执行,没有打印"wait之后"
注意:虽然这里wait之后主线程阻塞在synchronized代码块中,此时处于WAITING状态,但是这里的阻塞释放了锁,其它线程是可以获取到object这个对象的锁
wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常
wait 结束等待的条件
其他线程调用该对象的 notify 方法
wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间)
其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常
4.2 notify()方法
notify 方法是唤醒等待的线程
方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的 其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁
如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程(并没有 "先来后到")
在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行 完,也就是退出同步代码块之后才会释放对象锁
notify也要搭配 synchronized 来使用
观察一个notify唤醒线程的案例
public class ThreadDemo18 {
public static void main(String[] args) {
Object object = new Object();
//t1线程用来进行wait
Thread t1 = new Thread(()->{
synchronized (object){
System.out.println("wait之前");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait之后");
}
});
Thread t2 = new Thread(()->{
System.out.println("notify之前");
synchronized (object){
//notify必须获取到锁才能进行通知
object.notify();
}
System.out.println("notify之后");
});
t1.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
此处先执行了wait,然后wait操作阻塞了.没有看到紧接着wait之后的打印,接下来执行t2,进行了notify之后,才会唤醒t1的wait,t1才继续执行,打印wait之后.线程start之间添加sleep是保证大概率情况下,t1先于t2执行,否则t2先执行,notify时,没有对应的wait响应,那么t2就是进行了无效的通知,不过也没有别的影响,就是相当于白通知了!
如果t2不进行notify,那么t1就会一直阻塞,等待其它线程的唤醒,这样死等容易出问题
因此还提供了一个带参的wait(),参数为指定的等待的最大时间,等待最大时间还没有被唤醒,就直接自动唤醒,继续执行
wait与sleep的区别(面试题)
和wait比较相似,sleep也是休眠指定时间,也都能被提前唤醒,sleep是通过interrupt唤醒,wait是通过notify唤醒.但是表示的含义不同,notify是正常唤醒,逻辑是正常的,sleep被提前唤醒则是出现了异常,是不正常的逻辑
wait需要搭配synchronized使用,sleep不需要
wait是Object的方法,sleep是Thread的静态方法
4.3 notifyAll()方法
notify 方法是唤醒等待的线程,使用notifyAll方法可以一次唤醒所有的等待线程
看一个案例:设置多个线程,先使用notify方法,看唤醒了几个线程,再将notify替换为notifyAll,观察唤醒了几个线程
public class WaitAndNotify {
public static void main(String[] args) {
Object co = new Object();
System.out.println(co);
for (int i = 0; i < 5; i++) {
MyThread t = new MyThread("Thread" + i, co);
t.start();
}
try {
Thread.sleep(2000);
System.out.println("-----Main Thread notify-----");
synchronized (co) {
co.notify();
}
Thread.sleep(2000);
System.out.println("Main Thread is end.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class MyThread extends Thread {
private String name;
private Object co;
public MyThread(String name, Object o) {
this.name = name;
this.co = o;
}
@Override
public void run() {
System.out.println(name + " is waiting.");
try {
synchronized (co) {
co.wait();
}
System.out.println(name + " has been notified.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
5个线程都在阻塞中,使用notify方法,只唤醒了一个等待的线程
我们将notify换成notifyAll后:
所有阻塞等待的线程都被唤醒
注意: 虽然是同时唤醒这些线程, 但是这些线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行.
4.4 多个线程使用wait和notify方法
案例:有三个线程,分别只能打印ABC,通过使用wait和notify方法控制三个线程按固定的顺序打印ABC
public class ThreadDemo19 {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(()->{
System.out.println("A");
synchronized (locker1){
locker1.notify();
}
});
Thread t2 = new Thread(()->{
synchronized (locker1){
try {
locker1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("B");
synchronized (locker2){
locker2.notify();
}
});
Thread t3 = new Thread(()->{
synchronized (locker2){
try {
locker2.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("C");
});
t2.start();
t3.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.start();
}
}
程序中如果先执行t2的wait,后执行t1的notify, 是没问题的,但是可能存在这种情况:如果调度顺序是先t1中的notify,那么就不会唤醒t2了.程序就僵持在这里了.解决办法就是让t1执行慢点,让其他线程先执行,这样就万无一失了
结果