多线程内存模型
我们来了解下多线程内存模型是怎么工作的
通过图片我们可以看出来 每一个线程都有一个自己的工作空间 而他们是怎么工作的呢?
1.java中所有的变量都是存在主内存里的。
2.各自的线程在工作的时候会自己拿到一块工作内存。里面保存了该线程用到的变量的副本。
3.线程对变量的操作,都是操作自己工作内存中,副本变量,不能操作主内存。
4.线程执行完后,会将工作内存中的数据同步回主内存,来完成主内存中变量的更新的。
通过以上4点,我们发现会出现变量可见性的问题
举个例子吧:
public class test {
public static boolean pd=false;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("检测线程A是否满足需求");
while (!pd){ }
System.out.println("满足需求");
}
},"B").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是线程"+Thread.currentThread().getName()+"正在执行中");
//满足需求改变状态
pd=true;
}
},"A").start();
}
}
检测线程A是否满足需求
我是线程A正在执行中
上面这个例子你多运行几次会发现一直卡在while循环了我明明已经将pd改为true了
你试试加volatile关键字在pd变量上会有奇迹发生
就好比老板安排AB两员工,去找快递公司谈合同 公司只需要一家快递公司
假设A员工顺利和顺丰快递谈好了合作,然后回到公司了,
按照现实来说B员工应该停止继续去找快递公司谈合作, 因为A员工已经谈好了
但是在程序中B员工并不知道 A员工已经谈好了, 所以B员工继续找快递公司谈合作
随后和邮政快递谈好了合作,然后回到公司, 老板看到AB员工都带着一份快递公司的协议回来了瞬间炸毛.
为了解决上面的问题 我们有好多种方式 比如加上同步锁 synchronized 一次只让一个人去谈合作 但是这样会有损性能
我们来看看java内存模型在设计所围绕的三个问题
1、原子性
(1)原子是构成物质的基本单位(当然电子等暂且不论),所以原子的意思代表着——“不可分”;
(2)原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。
2、非原子性
也就是整个过程中会出现线程调度器中断操作的现象,例如:
类似”a += b”这样的操作不具有原子性,在某些JVM中”a += b”可能要经过这样三个步骤:
(1)取出a和b
(2)计算a+b
(3)将计算结果写入内存
如果有两个线程t1,t2在进行这样的操作。t1在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是t2开始执行,t2执行完毕后t1又把没有完成的第三步做完。这个时候就出现了错误,相当于t2的计算结果被无视掉了。所以上面的例子在同步add方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。
类似的,像”a++”这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作。
那么如何保证原子性呢 使用synchronized 或者lock来让它变成一个原子操作,(不可分割)
还可以使用原子数据结构。AtomicInteger、AtomicLong、AtomicReference等。
注意: 原子性包含可见性,因为没有可见性不可能实现原子性,可以自己看Atomic的源码里就使用volatile修饰了成员变量
3、有序性
在当前线程内观察,操作都是有序的;但是如果在一个线程中去观察另外一个线程,可以发现所有的操作都是无序的。
在java内存模型所保证的是,当前线程内,所有的操作都是由上到下的,但是多个线程并行的情况下,则不能保证其操作的有序性。
可以使用synchronized 和volatile 都可以保证有序性 ,也就是防止指令重排
4、可见性
可见性volatile修饰词,可以应对多线程同时访问修改同一变量,由于相互的不可见性所带来的不可预期的结果,存在二义性的现象:
多线程变量不可见 :当一个线程对一变量a修改后,还没有来得及将修改后的a值回写到主存,而被线程调度器中断操作(或收回时间片),然后让另一线程进行对a变量的访问修改,这时候,后来的线程并不知道a值已经修改过,它使用的仍旧是修改之前的a值,这样修改后的a值就被另一线程覆盖掉了。
多线程变量可见: 被volatile修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是能看到某个成员变量都是同一个值,这就是保证了可见性。
volatile使用场景: 在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。
由于使用volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此在必要时才使用此关键字。
什么是指令重排?
指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。
指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。
然而,指令重排是一把双刃剑,虽然优化了程序的执行效率,但是在某些情况下,会影响到多线程的执行结果。我们来看看下面的例子
public class Thread_a extends Thread {
public static boolean pd = false;
public static String str = null;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while( !pd){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(str);
}
},"B").start();
new Thread(new Runnable() {
@Override
public void run() {
str = "值以改";
//模拟线程调度器
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
pd = true;
}
},"A").start();
}
}
结果
值以改
但是如果我把 str = “值以改”; 和 pd = true;调换下位置呢
@Override
public void run() {
pd = true;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
str = "值以改";
}
多次运行后结果就是
null
因为很可能str = "值以改"; 还没有完成赋值的操作,线程调度器就切换到线程B了
线程B直接跳出了循环结果自然会出现null。
而给变量加入volatile后重排序规则
(1)如果第一个操作为volatile读的时候,不管第二个操作是啥,都不能重排序。这条规则确保了volatile读之后的操作不会被编译器重排序到volatile读之前。
(2)如果第二个操作为volatile写的时候,不管第一个操作是啥,都不能重排序。这条规则确保了volatile写的操作不会被编译器重排序带volatile写之后。
(3)如果第一个操作为volatile写,第二个操作为volatile读的时候,不能重排序。
那么针对这一些排序规则,volatile底层到底是怎么实现的呢?这就要靠我们的内存屏障来进行保证了。
内存屏障
(1)在每一个volatile写操作前面插入一个Store屏障。这确保了在进行volatile写之前,前面的所有普通的写操作都已经刷新到了内存。
(2)在每一个volatile写操作后面插入一个StoreLoad屏障。这样可以避免volatile写操作与后面可能存在的读写操作发生重排序。
(3)在每一个volatile读操作后面插入一个Load屏障。这样可以避免volatile读操作和后面普通的读操作进行重排序。
(4)在每一个volatile读操作后面插入一个LoadStore屏障。这样可以避免volatile读操作和后面普通的写操作进行重排序。
这样就有效的解决了 在多线程中 指令重排序问题
还记得单例的DCL写法么?
public class Singleton{
private static volatile Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(instance==null){
synchronized(Singleton.class){
if(instance==null){
instance=new Singleton();
}
}
}
return instance;
}
}
如果不加volatile会有什么问题呢?
当instance=new Singleton(); 引用不会立即刷新到其他线程中所以其他线程instance==null 还是true,导致Singleton不断被创建对象,严重甚至导致宕机
加了volatile之后在第一次对象被创建后引用立即会被刷新到其他线程中,然后其他线程在进行第二次instance==null就会发现结果为false了,这样大大的提升了性能
使用建议
先来看一段代码
public volatile long num=0;
public void add(){
for (int i = 0; i < 1000; i++) {
if (num==4000){
return;
}
num++;
}
}
1.假设我创建100个线程, 看上面代码按理论上来说在第4个线程执行完毕后就满足num==4000其他线程就不会执行了,但是结果不是这样的, 可能会发生同一时间多个线程都超过了num==4000但是还没执行num++,那么这些线程就会永远满足不了num==4000的条件,一直执行到循环结束为止
2.那么我把条件换成num>=4000? 虽然这样可以避免大部分的线程,但是在判断num==4000时同一时间进入的其他线程还会多执行一次才会停下来
所以在使用volatile的时候需要考虑上面的2个问题,虽然能让值立即在其他线程发现,但是在逻辑上是否支持多执行几次循环或者多执行几次方法呢?
所有很多时候建议使用synchronized和lock而不是volatile,因为volatile不容易驾驭,很容易出现意外情况
volatile 修饰对象变量的问题
相信使用java一段时间后应该都知道对象变量是引用类型的,变量里只是存储对象的地址而不是实际的值,那么我们使用volatile去修饰对象变量是不会影响对象内部的东西进行可见,而单纯的只是针对,对象变量存储的引用对象地址可见 ,那么我们来举个例子:
public volatile List listtest=new ArrayList();listtest这个变量的引用地址是对所有线程可见的,当我们对listtest这个变量从新赋值new LinkedList(); 那么其他线程使用listtest变量的时候获取的对象地址就会发生变化,但是当我们操作listtest变量进行get add,remove,set,size等这些方法的时候实际上是操作这个变量地址所对应的对象内部变量,如果这个变量没有被volatile 进行修饰,那么这个内部变量就无法保证多个线程之间的可见。