Java多线程之volatile详解

Wesley13
• 阅读 873
目录:
什么是volatile?
JMM内存模型之可见性
volatile三大特性之一:保证可见性
volatile三大特性之二:不保证原子性
volatile三大特性之三: 禁止指令重排
小结
1. 什么是volatile?
答:volatile是java虚拟机提供的轻量级的同步机制(可以理解成乞丐版的synchronized)

特性有:

保证可见性
不保证原子性
禁止指令重排
理解volatile特性之一保证可见性之前要先理解什么是JMM内存模型的可见性
2. JMM内存模型之可见性
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM关于同步的规定:

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

Java多线程之volatile详解

4.图解:

Java多线程之volatile详解

Java多线程之volatile详解

即创建student对象age=25,每个线程自己的工作内存都会拷贝一份age = 25,当线程t1修改age=37后,需要把age=37写回主内存,然后主内存向其他线程分发最新的值。

volatile保证可见性特性也是如此
volatile三大特性之一:保证可见性
1. 结合代码理解volatile的可见性
代码

import java.util.concurrent.TimeUnit;

class MyData{
    int number = 0;

    public void addTo60(){
        this.number = 60;
    }
}

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.addTo60();
            System.out.println(Thread.currentThread().getName()+"\t update number value:"+myData.number);
        },"AAA").start();

        //第2个线程就是我们的main线程
        //如果number==0,那么一直在死循环,下面的输出打印不出来,
        //如果打印了,就是main线程感知到了number已经从0变为了60,可见性被触发
        while (myData.number==0){
            // main线程就一直在这里等待循环,直到number值不再为零。
        }

        System.out.println(Thread.currentThread().getName()+"\t mission is over");
    }
}

编译结果:

Java多线程之volatile详解
AAA线程已经把myData.number从0赋值为60,并且写回了主内存,但是对main线程不可见。所以main线程一直在傻傻的等while(myData.number==0),但实际真实值number=60了,
2. 当我们在number添加volatile修饰符,即volatile int number = 0;
代码:

import java.util.concurrent.TimeUnit;

class MyData{
    //volatile 增强了主内存和各线程之间的可见性,只有有一个线程改了主内存的值,
//    其他线程马上会收到通知。迅速获得最新值。
    volatile int number = 0;

    public void addTo60(){
        this.number = 60;
    }
}

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.addTo60();
            System.out.println(Thread.currentThread().getName()+"\t update number value:"+myData.number);
        },"AAA").start();

        //第2个线程就是我们的main线程
        //如果number==0,那么一直在死循环,下面的输出打印不出来,
        //如果打印了,就是main线程感知到了number已经从0变为了60,可见性被触发
        while (myData.number==0){
            // main线程就一直在这里等待循环,直到number值不再为零。
        }

        System.out.println(Thread.currentThread().getName()+"\t mission is over" +
                ", main get number value:"+myData.number);
    }
}

编译结果:

Java多线程之volatile详解
4. volatile三大特性之二:不保证原子性
1.首先要知道原子性指的是什么意思?
不可分割,完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。
需要整体完整。要么同时成功,要么同时失败。
2.通过代码验证volatile不保证原子性
代码:

class MyData {
    //volatile 增强了主内存和各线程之间的可见性,只有有一个线程改了主内存的值,
//    其他线程马上会收到通知。迅速获得最新值。
    volatile int number = 0;

    public void addTo60() {
        this.number = 60;
    }

    //请注意,此时number前面是加了volatile关键字修饰的,volatile不保证原子性。
    public void addPlusPlus() {
        number++;
    }
}

public class VolatileDemo {
    public static void main(String[] args) {

        MyData myData = new MyData();

        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlusPlus();
                }
            }, String.valueOf(i)).start();
        }


        //需要等待上面20个线程全部计算完成之后,再用main线程取得最终的结果值看是多少
        // >2 是因为后台有两个线程,1是main线程,2是GC线程。
        // 能最好的控制时间
        while (Thread.activeCount()>2){
            Thread.yield();  //礼让线程,退不执行。
        }


        System.out.println(Thread.currentThread().getName()+"\t finally number value:"+myData.number);
    }
}

编译结果:

Java多线程之volatile详解
3.如何解决volatile不保证原子性的问题?
我们可以用java.util.concurrent.atomic包下的 AtomicInteger解决这个问题

Java多线程之volatile详解
具体使用如下:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

class MyData {   //MyData.java ==> MyData.class ==> JVM字节码
    //volatile 增强了主内存和各线程之间的可见性,只有有一个线程改了主内存的值,
//    其他线程马上会收到通知。迅速获得最新值。
    volatile int number = 0;

    public void addTo60() {
        this.number = 60;
    }

    //请注意,此时number前面是加了volatile关键字修饰的,volatile不保证原子性。
    public void addPlusPlus() {
        number++;
    }

    AtomicInteger atomicInteger = new AtomicInteger();

    public void addMyAtomic(){
        atomicInteger.getAndIncrement();
    }
}

public class VolatileDemo {
    public static void main(String[] args) {

        MyData myData = new MyData();

        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlusPlus();
                    myData.addMyAtomic();
                }
            }, String.valueOf(i)).start();
        }

        //需要等待上面20个线程全部计算完成之后,再用main线程取得最终的结果值看是多少
        // >2 是因为后台有两个线程,1是main线程,2是GC线程。
        // 能最好的控制时间
        while (Thread.activeCount()>2){
            Thread.yield();  //礼让线程,退不执行。
        }

        System.out.println(Thread.currentThread().getName()+"\t int type,finally number value:"+myData.number);
        System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type ,finally number value:"+myData.atomicInteger);
    }
}

编译结果:

Java多线程之volatile详解
4.关于volatile数字丢失的简单原理:

Java多线程之volatile详解

上图解释:

比如拿回自己工作空间的时候都是3,+1后写回去的时候,正好被别的线程捷足先登,只能挂起,已经有线程把4写了回去,等再唤醒的时候再把4写回去就会造成丢值
5. number++在多线程下是不安全的,为什么不用synchronized?
因为synchronized是重锁,有更合适的就用更合适的,杀鸡焉用牛刀。

5. volatile三大特性之三: 禁止指令重排
1. 说指令重排之前,我们要知道什么是有序性?

Java多线程之volatile详解

可能会出现问题,如下:

Java多线程之volatile详解
2. volatile特性之三:禁止指令重排
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。我们先了解一个概念:内存屏障
3.内存屏障
内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
保证特定操作的执行顺序,
保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能
和这条MemoryBarrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作
用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

Java多线程之volatile详解

6. 小结
工作内存与主内存同步延迟现象导致的可见性问题

解:可以使用Synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。

对于指令重排导致的可见性问题和有序性问题

解:可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
volatile实现可见性但不保证原子性
   volatile关键字:能够保证volatile变量的可见性不能保证volatile变量复合操作的原子性         volatile如何实现内存可见性:        深入来说:通过加入内存屏障和禁止重排序优化来实现的。对volatile变量执行写操作时,会在写操作后加入一条store屏
Wesley13 Wesley13
3年前
volatile 手摸手带你解析
!(https://oscimg.oschina.net/oscnet/updd725cad8a8b1bffe9ae4c3393eccddac22.JPEG)前言volatile是Java里的一个重要的指令,它是由Java虚拟机里提供的一个轻量级的同步机制。一个共享变量声明为volatile后,特别是在多线程操作时,正确使用
Wesley13 Wesley13
3年前
Volatile关键字
Volatile关键字①volatile的两个特点1保证线程(CPU)之间的可见性;(也就是保证数据一致性)简单解释一下:一个线程将一个值的数值改变时,另一个使用该数值的线程能看到这种改变;2禁止指令重排序(禁止乱序执行);这个和单例
Wesley13 Wesley13
3年前
Java 深入理解volatile关键字
我们知道Java中volatile实现了修饰变量的原子性以及可见性,并且为了实现多线程环境下的线程安全,禁止了指令重排。首先我们先来了解一下happensbefore原则、asifserial语义以及数据依赖性,引用自《Java并发编程的艺术》happensbefore简介从JDK5开始,Java使用新的JSR133内存模型
Wesley13 Wesley13
3年前
JAVA 并发包
Java.Utril.ConcurrentVolatile关键字避免java虚拟机指令重排序,保证共享数据修改同步,数据可见性。volatile相较于synchronized是一种比较轻量级地同步策略,但不具备互斥性,不能成为synchronized的替代,不能保证原子性。
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
3年前
Java多线程(二)
\恢复内容开始一,volatile关键字当多个线程操作共享数据时,可以保证内存中的数据可见性相较于synchronized关键字:1,不具备“互斥性”2,不能保证变量的原子性二,原子变量volatile保证内存可见性CAS(CompareAndSwap)算法保证数据的原子性内存值V预估值A更新值
Wesley13 Wesley13
3年前
Java 多线程:volatile关键字
概念volatile也是多线程的解决方案之一。\\volatile能够保证可见性,但是不能保证原子性。\\它只能作用于变量,不能作用于方法。当一个变量被声明为volatile的时候,任何对该变量的读写都会绕过高速缓存,直接读取主内存的变量的值。如何理解直接读写主内存的值:回到多线程生成的原因(Java内存模型与