volatile的学习总结

1.volatile是Java虚拟机提供的轻量级的同步机制
  • 保证可见性

  • 不保证原子性

  • 禁止指令重排

2. Java内存模型(JMM)

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过规范定义了程序中的各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。

JMM的同步规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存

  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存

  3. 加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存时每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回到主内存,不能直接操作主内存中的变量,各个线程的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要的访问过程如下图:

volatile的学习总结_第1张图片

JMM的三大特性

JMM是线程安全性获得的保证。因为JMM具有如下特点:

  1. 可见性:从主内存拷贝变量后,如果某一个线程在自己的工作内存中对变量进行了修改,然后写回了主内存,其它线程能第一时间看到,这就叫作可见性。

  2. 原子性:不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割

  3. 有序性:禁止指令重排,按照规定的顺序去执行

综上所述,volatile满足JMM三大特性中的两个,即可见性和有序性,volatile并不满足原子性,所以说volatile是轻量级的同步机制。

3. 代码验证Volatile的可见性

代码示例:

	/**
	 * Created by salmonzhang on 2020/7/4.
	 * 可见性代码实例
	 */
	public class VolatileDemo {
	    public static void main(String[] args) {
	        MyData myData = new MyData();
	        new Thread(() -> {
	            System.out.println(Thread.currentThread().getName() + "\t come in ...");
	            //暂停一会儿线程
	            try{ TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
	            myData.addTo10();
	            System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
	        },"Thread01").start();
	
	        while (myData.number == 0) {
	            //main线程一直在这里等待,直到number的值不再等于零
	        }
	        System.out.println(Thread.currentThread().getName()+"\t mission is over , number updated ...");
	    }
	}
	
	class MyData{
	//    int number = 0; // 这里没有加volatile
	    volatile int number = 0; // 这里加了volatile
	    public void addTo10() {
	        this.number = 10;
	    }
	}

没有加volatile的运行结果:

volatile的学习总结_第2张图片

加了volatile的运行结果:

volatile的学习总结_第3张图片

总结:如果不加volatile关键字,则主线程会进入死循环,加了volatile时主线程运行正常,可以正常退出,说明加了volatile关键字后,当有一个线程修改了变量的值,其它线程会在第一时间知道,当前值作废,重新从主内存中获取值。这种修改变量的值,让其它线程第一时间知道,就叫作可见性。

4. 代码验证Volatile不保证原子性

代码示例:

	/**
	 * Created by salmonzhang on 2020/7/4.
	 * 验证volatile不保证原子性
	 * 原子性是什么意思:
	 * 不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。
	 * 需要整体完整,要么同时成功,要么同时失败。保证数据的原子一致性
	 */
	public class VolatileDemo2 {
	    public static void main(String[] args) {
	        MyData2 myData2 = new MyData2();
	        for (int i = 1; i <= 20; i++){
	            new Thread(() -> {
	                for (int j = 0; j < 1000; j++) {
	                    myData2.addPlusPuls();
	                }
	            },String.valueOf(i)).start();
	        }
	
	        //需要等待上面20个线程全部执行完成后,再用main线程取得最终的结果值看看是多少?
	        while (Thread.activeCount() > 2) { //后台默认有两个线程:GC线程和main线程
	            Thread.yield();
	        }
	        System.out.println(Thread.currentThread().getName() + "finally number value = " + myData2.number);
	    }
	}
	
	class MyData2{
	    volatile int number = 0; // 这里加了volatile
	    public void addPlusPuls() {
	        number++;
	    }
	}

运行结果:

volatile的学习总结_第4张图片

从代码的运行结果会发现:会出现number最终的结果有可能出现不是20000的时候,这就证明了volatile不能保证原子性。

5. volatile不能保证原子性的原因和解决方案
  1. 为什么volatile不能保证原子性?

    由于多线程进程调度的关系,在某一时间段出现了丢失写值的情况。因为线程切换太快,会出现后面的线程会把前面的线程的值刚好覆盖。

    例如:Thread1和Thread2同时从主内存中读取number的值1到自己的工作内存,并同时进行了+1的动作,当Thread1将2写会主内存的时候,由于线程的调度原因,Thread2并没有第一时间知道Thread1已经将number的值改为了2,而是直接将Thread1改的number值进行覆盖,这样就会导致数据丢失。

  2. 解决方案:

    2.1. 直接在addPlusPuls前面加上synchronized

    class MyData2{
        volatile int number = 0; // 这里加了volatile
        public synchronized void addPlusPuls() {
            number++;
        } 	
    }
    

    但是为了保证一个number++的原子性直接用synchronized,感觉有点重,类似于“杀鸡用牛刀”

    2.2 用atomic

    class MyData2{
        AtomicInteger number = new AtomicInteger();
        public void addPlusPuls() {
            number.getAndIncrement();
        } 	
    }
    
7. 有序性
  1. 计算机在执行程序时,为了提高性能,编译器的处理器通常会对指令做重排,一般有三种重排:

    • 编译器的重排

    • 指令并行的重排

    • 内存系统的重排

在这里插入图片描述

  1. 单线程环境里确保程序最终执行的结果和代码执行的结果一致

  2. 处理器在进行重排序时,必须考虑指令之间的数据依懒性

  3. 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证用的变量能否一致性是无法确定的,结果也是无法预测的

重排案例一:

public void mySort(){
    int x=11;//语句1
    int y=12;//语句2
    x=x+5;//语句3
    y=x*x;//语句4
}

计算机执行的顺序可能是:

1234

2134

1324

问题:
请问语句4可以重排后变成第一条码?
存在数据的依赖性,所以没办法排到第一个

重排案例二:
volatile的学习总结_第5张图片

指令重排代码示例:

public class ReSortSeqDemo {
    int a = 0;
    boolean flag = false;
    
    public void method01() {
        a = 1;           // 这里的a和flag没有禁止指令重排,所以在多线程环境中就有可能出现问题
        flag = true;
    }

    public void method02() {
        if (flag) {
            a = a + 3;
            System.out.println("a = " + a);
        }
    }
}

这里的a和flag没有禁止指令重排,所以在多线程环境中就有可能出现问题,例如指令重排后,method01中的flag=true先被Thread1执行了,此时Thread2又抢占到了线程资源去执行method02()时,此时的运行结果就是有问题的。运行结果就是a = 3,而不是正常情况下的a = 4

7. 单例模式下可能存在线程不安全

代码示例:

public class SingletonDemo {
    private static SingletonDemo instance = null;
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName() + "\t 我是SingletonDemo的构造方法");
    };

    //synchronized 解决单例的多线程问题,会显得比较重,整个方法都被锁住了,不建议这么写
    public static SingletonDemo getInstance(){
        if (instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }

    public static void main(String[] args) {
        //并发多线程后,会出现构造函数多次执行的情况
        for (int i = 1; i <= 10; i++){
            new Thread(() -> {
                SingletonDemo.getInstance();
            },String.valueOf(i)).start();
        }
    }
}

运行结果:

volatile的学习总结_第6张图片

8. 单例模式下的volatile分析

1.代码示例:

public class SingletonDemo {
    private static volatile SingletonDemo instance = null; //加上volatile,禁止编译器指令重排
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName() + "\t 我是SingletonDemo的构造方法");
    };

    /**
     * DCL (double check Lock 双端检索机制)
     */
    public static SingletonDemo getInstance(){
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        //并发多线程后,会出现构造函数多次执行的情况
        for (int i = 1; i <= 10; i++){
            new Thread(() -> {
                SingletonDemo.getInstance();
            },String.valueOf(i)).start();
        }
    }
}

总结:

  • 如果没有加 volatile 就不一定是线程安全的,原因是指令重排序的存在,加入 volatile 可以禁止指令重排。

  • 原因是在于某一个线程执行到第一次检测,读取到的 instance 不为 null 时,instance 的引用对象可能还没有完成初始化。

  • instance = new Singleton() 可以分为以下三步完成

     memory = allocate();  // 1.分配对象空间 	
     instance(memory);     // 2.初始化对象
     instance = memory;    // 3.设置instance指向刚分配的内存地址,此时instance != null
    
  • 步骤 2 和步骤 3 不存在依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种优化是允许的。

  • 发生重排

     memory = allocate();  // 1.分配对象空间 	
     instance = memory;    //3.设置instance指向刚分配的内存地址,此时instance != null,但对象还没有初始化完成 
     instance(memory);     // 2.初始化对象
    
  • 所以不加 volatile 返回的实例不为空,但可能是未初始化的实例

非常感谢您的耐心阅读,希望我的文章对您有帮助。欢迎点评、转发或分享给您的朋友或技术群。

你可能感兴趣的:(java并发编程)