天天看点

Volatile及JMM

参考:

https://blog.csdn.net/suyimin2010/article/details/80722262

https://www.cnblogs.com/yuluoxingkong/p/9236077.html

https://blog.csdn.net/sdr_zd/article/details/81323519

https://www.jianshu.com/p/2ab5e3d7e510

1.valatile的理解

[1].valatile是java虚拟机提供的轻量级的同步机制

①.保证可见性(当有线程改变了主内存中的数据,其他线程马上能收到变动)

package com.w4xj.interview.thread;

import java.util.concurrent.TimeUnit;

public class VolatileTest {

    public static void main(String[] args) {

        Data data = new Data();

        new Thread(() ->{

            System.out.println(Thread.currentThread().getName() + "\t AAA thread begin");

            try {

                TimeUnit.SECONDS.sleep(3);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            data.setNum();

            System.out.println(Thread.currentThread().getName() + "\t updated numer value: " + data.num);

        },"AAA").start();

        while (data.num == 0){}

        System.out.println(Thread.currentThread().getName() + "\t update is over, the num is " + data.num);

    }

}

class Data{

    //volatile 关键字就可以保证可见性,若不加这个关键字,主线程是收不到AAA线程修改num变量成功的通知的

    volatile int num = 0;

    public void setNum(){

        this.num = 66;

    }

}

②.不保证原子性

package com.w4xj.interview.thread;

import java.util.concurrent.TimeUnit;

public class VolatileTest {

    public static void main(String[] args) {

        Data data = new Data();

        for (int i = 0; i < 20; i++) {

            new Thread(() -> {

                for (int j = 0; j < 100; j++) {

                    data.plus();

                }

            }, String.valueOf(i)).start();

        }

        //2即代表gc线程和main线程

        while (Thread.activeCount() > 2){

            //让出线程执行权

            Thread.yield();

        }

        //输出总是小于2000

        System.out.println(Thread.currentThread().getName() +" : "+ data.num);

    }

}

class Data {

    //volatile 关键字就可以保证可见性,若不加这个关键字,主线程是收不到AAA线程修改num变量成功的通知的

    volatile int num = 0;

    public void setNum() {

        this.num = 66;

    }

    public void plus() {

        //这里睡1毫秒,让这个加塞出现的更明显一些,因为自增这个操作太快了,同样也是线程速度过快,会导致可见性来不及通知所以导致写被覆盖

        try {

            Thread.currentThread().sleep(1);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        num++;

    }

}

③.禁止指令重排(保证有序性)

2.JMM

[1].介绍

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

②.JMM关于同步的规定:

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

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

加锁解锁是同一把锁

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

Volatile及JMM

[2].可见性

各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作的工作内存进行读写操作后在写到主内存中的,这就可能存在一个线程AAA修改了共享变量num的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个变量num进行操作,但此时A线程工作内存中共享变量num对线程B来说并不可见,这种工作内存与主存同步延迟的现象就造成了可见性问题

[3].原子性

①.为什么num++在多线程下是非线程安全的

Volatile及JMM

虽然++操作从源码上看是一条命令,但实际从字节码来看是4步,所以加了volatile仍然无法保证原子性

②.解决方式1:synchronized解决(性能低)

③.采用原子类:原子类是juc(java.util.concurrent)下面的保证原子性的包装类

Volatile及JMM

package com.w4xj.interview.thread;

import java.util.concurrent.TimeUnit;

import java.util.concurrent.atomic.AtomicInteger;

public class VolatileTest {

    public static void main(String[] args) {

        Data data = new Data();

        for (int i = 0; i < 20; i++) {

            new Thread(() -> {

                for (int j = 0; j < 100; j++) {

                    data.plus();

                    data.plusAtomic();

                }

            }, String.valueOf(i)).start();

        }

        //2即代表gc线程和main线程

        while (Thread.activeCount() > 2){

            //让出线程执行权

            Thread.yield();

        }

        System.out.println(Thread.currentThread().getName() +" : "+ data.num);

        System.out.println(Thread.currentThread().getName() +" : "+ data.atomicInteger);

    }

}

class Data {

    //volatile 关键字就可以保证可见性,若不加这个关键字,主线程是收不到AAA线程修改num变量成功的通知的

    volatile int num = 0;

    //带原子性的Integer

    AtomicInteger atomicInteger = new AtomicInteger();

    public void setNum() {

        this.num = 66;

    }

    public void plus() {

        //这里睡1毫秒,让这个加塞出现的更明显一些,因为自增这个操作太快了,同样也是线程速度过快,会导致可见性来不及通知所以导致写被覆盖

        try {

            Thread.currentThread().sleep(1);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        num++;

    }

    public void plusAtomic() {

        try {

            Thread.currentThread().sleep(1);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        atomicInteger.getAndIncrement();

    }

}

[4].有序性

①.计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:

Volatile及JMM

a.编译器优化的重排序:

编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。【as-if-serial原则保证,as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。】

b.指令级并行的重排序:

现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。

int x = 1;//步骤1

int y = 2;//步骤2

x = x + 1;//步骤3

y = x + x;//步骤4

重排后可能的顺序:

1234

1324

2134

4***肯定是不行的,必须准守数据依赖

c.内存系统重排序:

由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。两个线程中使用的变量能否保证一致性无法确定。

int x,y,a,b=0;

线程1 线程2
x = a; y = b;
b = 1; a = 2;
x = 0 y = 0

指定重排后

线程1 线程2
b = 1; a = 2;
x = a; y = b;
x = 2  y = 1

②.volatile禁止指令重排原理

a.volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如何实现禁止指令重排优化的。

b.先了解一个概念,内存屏障(Memory Barrier)。内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个

i.一是保证特定操作的执行顺序,

ii.二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

c.硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障

d.java内存屏障主要4种

LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

e.由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。

对Volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量 对Volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存
Volatile及JMM
Volatile及JMM

③.指令重排经典案例之DCL,这里不讨论sysnchronized

a.代码

package com.w4xj.interview.thread;

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;

    }

}

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

memory = allocate(); //1.分配对象内存空间

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

instance(memory);    //2.初始化对象

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

//禁止指令重排优化

private volatile static DoubleCheckLock instance;