Java多线程之内存可见性(sync和volatile都可以)和原子性操作


可见性的理论

就说这个线程是可见的

 

 

工作内存是java内存模型提出的概念

JMM

变量是指共享变量

下面的X就是三个线程的共享变量

 

 

 

 共享变量可见性的原理

两个步骤其中任何一个步骤出了差错,都会导致变量不可见。会导致数据的不准确,从而是的线程不安全。所以在编写代码的时候要保证共享变量的可见性

 

 


 满足两点可以保证可见性

 

这里指语言层面,所以不包括concurrent并发包下的高级特性

可以实现互斥锁(原子性),用synchronized

但是他也有另个功能,即实现内存的可见性

Synchronized实现可见性

这六个步骤可以结合刚刚的两条规定来理解。

 

先补充一个概念:

不理解也没关系

看的懂上面的意思即可,有可能执行顺序和代码顺序不一样

as-if-serial

 

 

在单线程下一定遵循这个条件

 

举个例子:

 

 

package mkw.demo.syn;

public class SynchronizedDemo {
    //共享变量
    private boolean ready = false;
    private int result = 0;
    private int number = 1;   
    //写操作
    public void write(){
        ready = true;                           //1.1                
        number = 2;                            //1.2                
    }
    //读操作
    public void read(){                    
        if(ready){                             //2.1
            result = number*3;         //2.2
        }       
        System.out.println("result的值为:" + result);
    }

    //内部线程类
    private class ReadWriteThread extends Thread {
        //根据构造方法中传入的flag参数,确定线程执行读操作还是写操作
        private boolean flag;
        public ReadWriteThread(boolean flag){
            this.flag = flag;
        }
        @Override                                                                    
        public void run() {
            if(flag){
                //构造方法中传入true,执行写操作
                write();
            }else{
                //构造方法中传入false,执行读操作
                read();
            }
        }
    }

    public static void main(String[] args)  {
        SynchronizedDemo synDemo = new SynchronizedDemo();
        //启动线程执行写操作
        synDemo .new ReadWriteThread(true).start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        //启动线程执行读操作
        synDemo.new ReadWriteThread(false).start();
    }
}

 

 

假设在1.1后让出了cpu资源,那么这时候执行2.1,也就是读线程开始执行,

结果是3

先执行1.2说明进行了指令重排序,打印了初始值 0

 还可以2.1和2.2重排序,

 

 

 Volatile实现可见性

加入内存屏障和禁止指令重排序实现可见性。

把内容强制刷新到主内存中去

 

 

在java种一共有八条操作指令,store和load是其中的两条。

通俗理解:

volatile变量在每次被线程访问的时候,都强迫从主内存中重读该变量的值,而当该变量发生变化的时候,又会强迫线程将更新的值刷新到主内存。

这样任何时刻,不同的线程总能看到该变量的最新值。

 

线程写volatile变量的过程:

1.改变线程工作内存中volatile变量副本的值

2.将改变的副本的值从工作内存刷新到主内存

 

线程读volatile变量的过程:

1.从主内存中读取volatile变量的最新值到线程的工作内存中

2.从工作内存中读取volatile的变量的副本

 

volatile不能保持原子性

volatile不能保证volatile变量复合操作的原子性

 

number++可以分解成三个步骤【重】,所以是线程不安全的,所以用sync加上锁后就是原子操作,三个步骤就合成了一个步骤,必须同时执行。

 

但是如果用volatile修饰的话:

 

举个例子:volatile修饰number

有两个线程A和B,

首先a抢到cpu,读取num的值;读完(读操作不会将工作内存的值立刻刷新到主内存,只有对num写才会刷新到主内存中)之后让出cpu,b抢到cpu,a线程阻塞,然后b读取,b执行num+1操作,这时候由于volatile的对内存可见性,会把最新值刷新到主内存中去,所以这时候主内存中num的值为6。

可是刚刚a读取num的值的时候num还是5.这时候如果b让出cpu,a抢到cpu的 时候不会重新读num的值,而是直接执行+1操作了。

 

从第五步开始A执行+1操作,num本来主内存已经是6了,现在又从5+1变成6。可见两个线程分别对num+1,可是看起来,只加了一次。

这是由于volatile不能保持原子性

 禁止指令重排序

https://blog.csdn.net/javazejian/article/details/72772461#理解java内存区域与java内存模型

 

 

volatile禁止重排优化

volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如何实现禁止指令重排优化的。先了解一个概念,内存屏障(Memory Barrier)。 
内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。下面看一个非常典型的禁止重排优化的例子DCL,如下:

/** * Created by zejian on 2017/6/11. * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创] */ public class DoubleCheckLock { private static DoubleCheckLock instance; private DoubleCheckLock(){} public static DoubleCheckLock getInstance(){ //第一次检测 if (instance==null){ //同步 synchronized (DoubleCheckLock.class){ if (instance == null){ //多线程环境下可能会出现问题的地方 instance = new DoubleCheckLock(); } } } return instance; } }

上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码)

memory = allocate(); //1.分配对象内存空间 instance(memory); //2.初始化对象 instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null

由于步骤1和步骤2间可能会重排序,如下:

memory = allocate(); //1.分配对象内存空间 instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成! instance(memory); //2.初始化对象

由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。那么该如何解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可。

  //禁止指令重排优化 private volatile static DoubleCheckLock instance;

ok~,到此相信我们对Java内存模型和volatile应该都有了比较全面的认识,总而言之,我们应该清楚知道,JMM就是一组规则,这组规则意在解决在并发编程可能出现的线程安全问题,并提供了内置解决方案(happen-before原则)及其外部可使用的同步手段(synchronized/volatile等),确保了程序执行在多线程环境中的应有的原子性,可视性及其有序性。

实现原子性的解决方法:

第一种方法:

 

 锁整个方法:

这个方式,使得效率比较低,因为休眠100毫秒,B线程必须等待完A休眠完才行

锁代码块:

这样效率更高

第二种方法(可实现可见性 原子性)

1.

 2.

 

3.

 

 volatile适用场景

 

如果有两个volatile变量,每个volatile变量状态要独立于其他volatile变量

感觉volatile没有sync实用

 sync和volatile的比较

所以在保证volatile适用场景的情况下,实用volatile更加轻量级。

 

注:final也可保证可见性,因为他修饰的变量本身就是不能被改变的。

 

补充一点:

如果想要避免这种情况,就用关键字volatile声明64位的变量,或者把对他们的读写操作锁起来。

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM