目录
volatile关键字
内存可见性问题
创建两个线程,t1线程用来重复读取数字,当数字不等于0时,t1循环结束,t2线程用来修改数字,我们预期的输出结果是t2把flag改成非0后,t1的循环结束,但是实际输入1的时候,t1这个循环并没有结束循环,并且同时可以看到t2这个线程已经执行完了,t1线程依然还在继续循环,这个情况,就叫做“内存可见性问题”。
import java.util.Scanner;
class MyCounter{
public int flag = 0;
}
public class ThreadDemo {
public static void main(String[] args) {
MyCounter myCounter = new MyCounter();
Thread t1 = new Thread(() ->{
//t1要循环快速重复读取
while(myCounter.flag == 0){
//循环体
}
System.out.println("t1 循环结束");
});
Thread t2 = new Thread(()->{
//t2进行修改
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数");
myCounter.flag = scanner.nextByte();
});
t1.start();
t2.start();
}
}
t1在进行循环重复读取:1.load把内存中flag的值,读到寄存器里 2.cmp把寄存器的值和0 进行比较,根据比较结果,决定下一步往哪个地方执行,执行这么多次,在t2真正修改之前,load得到的结果都是一样的,另一方面,load操作和cmp操作相比,速度慢非常非常多,CPU针对寄存器的操作,要比内存操作快很多,计算机对于内存的擦欧总,比硬盘快三到四个数量级,由于load执行速度太慢(相比于cmp来说),再加上反复load到的结果都一样。JVM就做出了一个非常大胆的决定,不再重复load操作,判定好像没人改flag值一样,干脆只读一次就好了,所以t2线程在修改flag的值是没用的。
内存可见性问题就是:一个线程针对一个变量进行读取操作,同时另一个线程针对这个变量进行修改,此时读到的值,不一定是修改之后的值,这个线程没有感知到变量的变化,归根结底就是编译器jvm在多线程环境下优化时产生了误判
要改变此问题,只要给flag这个变量就上volatile关键字,意思就是告诉编译器,这个变量时“易变的”,你一定要每次重新读取这个变量的内存内容,不能进行编译器优化 这时候代码就正确了
volatile public int flag = 0;
volatile关键字
只能修饰变量(一个变量在线程中,一个线程读,一个线程写)
volatile 不保证原子性
JMM(Java内存模型)
Java程序中,有主内存,每个线程还有自己的工作内存(t1和t2的工作内存不是同一个东西),t1线程进行读取的时候,只是读取了工作内存的值,t2线程进行修改的时候,先修改的工作内存的值,然后再把工作内存的内容同步到主内存中,但由于编译器优化,导致t1没有重新的从主内存数据到工作内存,读到的结果就是“修改之前”的结果。这里的主存就是内存,工作内存就是指工作存储区 也就是CPU寄存器+CPU缓存 cache。
wait和notify方法
线程最大的问题 是抢占式执行,随机调度,使用join,则必须要t1彻底执行完,t2才能运行,如果是希望t1先干50%的活,就让t2开始行动,join无能为力,使用sleep,指定一个休眠时间,但是t1执行的这些活,到底花了多少时间,不好估计,而wait和notify更好的能控制线程之间的执行顺序,例如有t1和t2两线程,希望t1先干活,干的差不多了,再让t2来干,就可以让t2先wait 阻塞主动放弃CPU 等t1干的差不多了,再通过notify通知t2,把t2唤醒,让t2接着干。
wait方法
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
object.wait();
}
}
这时候这个线程应该处于一个死等状态,但是当我们运行它的时候发现它爆出了IllegalMonitorStateException异常,意为非法的锁状态,这是为什么呢
wait做的事情
1.先释放锁
2.进行阻塞等待
3.收到通知后,重新尝试获取锁,并且在获取锁后,继续往下执行
就好比去银行自助取款机取钱,但是自助取款机里没有钱了,那我们则要先从自助取款机里出来,再等待工作人员在自助取款机里充值完钱以后,我们收到工作人员的可以取款的消息以后,再进入自助取款机(自助取款机就相当于锁)
而这段代码,我们并没有加锁,直接wait,就没办法解锁,所以wait要搭配synchronized使用
synchronized (object) {
object.wait();
}
}
wait方法进行阻塞,某个线程调用wait方法,就会进入阻塞(无论是哪个对象wait的),此处就处在WAITING(只要线程中有一个对象wait了 那么这个线程就处在WAITING),虽然这里wait是阻塞了,阻塞在synchronized代码块里,实际上,这里的阻塞是暂时的释放了锁的,此时其他线程是可以获取到object这个对象的锁的,此时这里的阻塞,就处在WAITING状态
notify 方法(唤醒等待的线程)
public class threadDemo {
public static void main(String[] args) {
Object object = new Object();
Thread t1 = new Thread(() ->{
System.out.println("t1:wait 之前");
try {
synchronized (object) {
object.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1:wait之后");
});
Thread t2 = new Thread(() ->{
System.out.println("t2:notify 之前");
synchronized (object) {
object.notify();
}
System.out.println("t2:notify 之后");
});
t1.start();
try {
Thread.sleep(500);
//因为线程的随机调度 所以不确定t1和t2谁先被调度
//但是notify必须要在取到锁才能进行通知,所以
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
😃 notify使用的对象必须和wait使用的对象一样,如果有多个线程等待,那么线程调度器会随机挑选一个呈wait状态的线程。
😃在notify()方法之后,当前线程不会马上释放该对象锁,要等执行完notify()方法的线程将程序执行完,才会释放对象锁。
如果调用notify,此时没有人wait,那么就无法唤醒wait,相当于notify空喊了一下,没有副作用例如小明去食堂买黄焖鸡,付了款以后又去别的地方转悠,等他的饭好了,食堂师傅喊号取餐,但是没有人去取餐,但是也没有影响,这里的食堂师傅喊号就是notify()唤醒等待线程。
wait和sleep的区别
😀wait的带等待时间版本,看起来和sleep有点像,虽然都能指定等待时间,虽然也能被提前唤醒(wait使用notify唤醒,sleep使用interrupt唤醒)但是这里表示的含义截然不同,notify唤醒外套,这里不会有任何异常,interrupt唤醒sleep则是出异常了,wait是object的方法,sleep是Thread的静态方法
notifyAll
notifyAll和notify非常相似,多个线程wait的时候,notifyAll所有线程都唤醒,这些线程再一起竞争锁