volatile
volatile关键字的重要功能:保证内存可见性
在多线程中常常会出现的线程安全问题:
1.内存可见性
计算机运行的程序代码中,经常要访问数据,这些依赖的数据往往会存储在内存中(也就是会定义一个变量,变量中存放的数据就是经常要访问的数据,而这个变量就在内存中)。CPU使用这个变量时,就会把这个内存中的数据先读出来,放在CPU寄存器中,再参与运算。
CPU读取内存这个操作时非常慢的(慢只是相对而言),对CPU而言,它的大部分操作都是很快的,一旦操作涉及读/写内存,就导致速度降下来。CPU内部操作 > 读/写寄存器 > 读取高速缓存Cache > 读/写内存 > 读硬盘。
为了解决读取内存速度慢这个问题,编译器会对代码做出优化,把一些本来要读内存的操作优化程读取寄存器,这样一来就可以减少读内存的次数,提高效率,这个就是内存的可见性。
我们用一个代码详细的解释:
public class Demo01 {
private static int Q = 0;//全局变量
public static void main(String[] args) {
Thread t1 = new Thread(()->{
//该线程不断的判断Q的值是否有变化
while (Q == 0){
}
System.out.println("t1结束");
});
Thread t2 = new Thread(()->{
System.out.println("请输入Q的值:");
Scanner s = new Scanner(System.in);
Q = s.nextInt();
});
t1.start();
t2.start();
}
}
解释以上代码:代码想表达的意思是线程一在不断的判断此时此刻的Q是否为0,而线程二在修改Q的值。正常情况下如果我们将Q的值修改成非零那么线程一就不再循环判断,并且结束线程一。如下图我们运行的结果并非是我们所预期的效果,当预期于结果不同时就是一个线程安全的问题。
也就是说一个线程读,一个线程写也会出现线程安全问题,这就是内存可见性引起的。那么我们深入分析一下每一步过程:
在此之前我们也得知道这个线程一是分两步的,第一步:load读取内存中的Q值到寄存器中;第二步:通过cmp指令比较寄存器的值是否为0,决定是否继续循环;
由于这个循环的速度非常快,短时间内就会进行大量循环,也就是进行大量的load和cmp。此时,编译器就发现进行这么多次的load,结果都是一样的,读取内存速度慢费时间(完成一次load等价于上万次cmp),于是,编译器就做了一个决定,只读一次内存中的数据,后面不再重复读,直接从寄存器中取Q的值(这样就感知不到Q在变化)。以至于线程二修改了Q的值,线程一也没有结束的原因,于是就出现了以上的bug。

解决内存可见性问题
在多线程环境下,编译器对于是否进行这样的优化判断不一定准确,就需要我们通过volatile关键字告诉编译器不要优化,不管是否优化都可以通过volatile预防这种优化(也叫保证内存可见性),并且volatile不能保证原子性。也就是开头所说的保证内存可见性。同样的,我们可以也在循环中添加sheep休眠为了减缓速度也可以达到volatile的效果,只是原因不同了减缓速度使开销变小了,编译器就没有必要进行优化(这种方案不可靠,在编写代码时,我们无法确定编译器是否对我们的程序进行了优化),最好的方案就是是由volatile关键字。

wait 和 notify
wait 和 notify 执行时所做的事:
1.释放当前的锁
2.让线程进入阻塞
3.当线程被唤醒时,重新获得锁
如果在一个线程中没有锁就直接调用 wait 会抛出异常(非法监视),所以在调用 wait/notify 前该线程需要加锁
wait 和 notify 是相互使用,wait 是阻塞状态,而 notify 则是唤醒 其中一个被阻塞的线程。
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread t1 = new Thread(()->{
synchronized (lock){
System.out.println("开始");
try {
lock.wait();//此时的锁被释放,目前为阻塞状态,等待notify唤醒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("结束");
}
});
Thread t2 = new Thread(()->{
try {
Thread.sleep(3000);//先让t1线程获得锁
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock){
lock.notify();
}
});
t1.start();
t2.start();
}
}
notigy 是一次唤醒一个,而 notifyAll 则是唤醒当前所有被阻塞状态的线程。但是就算线程被唤醒,需要重新获得锁,如果是同一把锁还是存在锁竞争。
并且wait 和 notify 可以避免线程饿死,正是它的功能 "释放当前锁"。(线程饿死:一个或多个线程因为无法获取到必要的资源(通常是 CPU 时间片 ,或者是锁资源),而长时间无法执行,处于饥饿等待状态。)
wait 也可以添加超时限制,也就是当阻塞时间超过该限制时,线程不再阻塞而是继续往后执行。

join的用法:哪个线程调用join,就等那个线程结束后再往后执行代码。
