内存可见性我们在最开始讲到线程安全的时候聊到了关于线程安全问题总共有五种原因前面我们讲到了三种还要两种没有涉及到那么就来聊聊内存可见性引起的线程安全问题。内存可见性问题指的是在一个线程修改了共享变量的值之后其他线程是否能够立即看到即“看到”最新值这个修改。如果不能就可能出现内存可见性问题。import java.util.*;public class Demo {public static int flag0;public static void main(String[] args) {Thread t1new Thread(()-{while(flag 0) {//等待t1线程输入flag的值只要不为0就能结束System.out.println(t1线程结束);}});Thread t2new Thread(()-{System.out.println(请输入flag的值);Scanner scanner new Scanner(System.in);flag scanner.nextInt();});t1.start();t2.start();}}从之前的内容可知两个线程都写的情况会造成线程安全问题那么这段代码有一个线程在写一个线程在读会不会造成线程安全问题答案是会的内存可见性会导致该问题那么两个线程都进行读会造成线程安全问题这里的答案是不会。这段代码想要表现出来的效果是t1t2线程同时运行通过t2线程中输入的flag的值来控制t1线程是否结束。可是上文我们先后输入了1,0,2......都没能使t1线程结束这是为什么呢我们看whileflag 0{}这条语句其实有两个指令①loadcpu从内存中读取flag的值load到cpu的寄存器上②访问寄存器cpu访问寄存器中存储的flag的值与0进行比较①中load的操作读内存相较于②中访问寄存器的操作开销要大很多。(访问寄存器的速度是读内存的一万倍)上述while循环中①②这两条指令整体看执行的速度非常快等你scanner几秒钟了我while循环中①②可能都执行几亿次了cpu的计算能力非常强此时JVM就会怀疑这个①号load 的操作是否还有存在的必要节省开销于是经过load试探很多次发现都是一样的JVM索性就认为load 的值一直都一样速度太快了等不到我们scanner输入flag的值在load一次后寄存器保存了它的值然后把load这个操作给优化掉只留一个访问寄存器的操作指令访问之前寄存器中保存的值大大提高循环的执行速度。这就是内存可见化问题会出现的本质原因。这是第二种理解方法就是t2先对cpu进行修改再写会内存但是t1读取的时候是直接从内存中加载到CPU中这时候t2对内存的修改还没来的及所以就无法产生影响这是另一种理解那么怎么解决该问题呢我们就用volatile关键词修饰变量。volatile关键词对于JVM的优化都适用于单线程但不适用于多线程可能会出现bug。而volatile关键字是强制性关闭JVM优化开销是变大了但是数据更准了。volatile都是用来修饰变量的功能①解决内存可见性问题每次访问被volatile修饰的变量都要读取内存而不是优化到寄存器或者缓存器当中功能②禁止指令重排序对于被volatile修饰的变量的操作指令是不能被重排序的这个等会会讲对于线程指令是否会发生JVM的优化我们程序员也很难判定是否发生了所以更需要通过volatile去避免这种可能存在的问题。指令重排序指令重排序也是一种在编译器发生的优化过程它改变了程序原有的指令执行顺序使程序变得更好。这在单线程是没问题的但是在多线程可能会导致bug所以在多线程中我们需要解决该问题就要用到volatile修饰重排序操作指令涉及的变量这样就没问题了。举个例子比如要创建一个对象String snew String()可与分为三步1为s分配一份空间出来创建对象2初始化这片空间3使s指向这片空间在这三步中第2、3步有可能会发生指令重排现象创建对象的顺序变为1-3-2后面单例模式细讲这个过程就相当于你买一块房子你可以选择买精装房也可以买毛坯房先装修后买还是先买后装修的问题wait和notify由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知. 但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.完成这个协调工作, 主要涉及到三个方法 wait() / wait(long timeout): 让当前线程进入等待状态. notify() / notifyAll(): 唤醒在当前对象上等待的线程.wait和notify都是Object提供的方法所以说任何类都能使用wait和notifywait和notift是为了解决‘’线程饿死‘’的情况举个例子在ATM机上进行取钱第一位滑稽老铁去取钱但是ATM机器中的钱不够所以他走了但是他走了以后又有可能会回来想再试试有没有可能是刚刚操作失误了这个往复进行后面的几位滑稽老铁就无法取钱了这是线程饿死的形象表述wait ()wait 做的事情:1.使当前执行代码的线程进行等待. (把线程放到等待队列中)2.释放当前的锁释放后就可以允许其他线程用该锁了(所以说要使用wait必须先放在synchronized中有锁后才能释放锁)3.满足一定条件时被唤醒, 重新尝试获取这个锁wait 结束等待的条件:1.其他线程调用该对象的 notify 方法.2.wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).3.其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.所以说wait是先进行解锁然后再进行等待但是要注意的是这俩个步骤是打包原子的这个在wait内部已经实现我们想想如果不是打包原子的会有什么问题如果不是打包原子的那么就会先解锁但是这个线程还没有开始等待会继续执行但是同时其他的锁线程也会开始竞争 如果其他线程在这个时候执行了释放锁操作但是一号线程还没有及时开始等待那么一号线程就会错过这个唤醒信号就会继续等待下去也就是说wait是为了提前唤醒而sleep是为了设定固定时间进行等待不涉及唤醒。虽然interrupt也可以唤醒但是interrupt本质上其实是结束该线程了wait 需要搭配 synchronized 使用 sleep 不需要.wait 是 Object 的方法 sleep 是 Thread 的静态方法notifynotify 方法是唤醒等待的线程.方法notify()也要在synchronized中调用该方法是用来通知那些可能等待该对象的对象锁的 其它线程对其发出通知notify并使它们重新获取该对象的对象锁。如果notify和wait要联动必须要求notify的调用对象notify的锁对象wait的调用对象wait的锁对象都必须相同。如果有多个线程等待则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 先来后到)在notify()方法后当前线程不会马上释放该对象锁要等到执行notify()方法的线程将程序执行完也就是退出synchronized之后才会释放对象锁notifyallnotify方法只是随机唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.notifyAll() 比 notify() 更安全因为它不会随机选择一个线程唤醒而是让所有线程都有机会重新竞争锁从而避免了某些线程被永久忽略的问题。所以在大多数场景中推荐使用 notifyAll()注意虽然是同时唤醒所有线程, 但是这些线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的依次执行.