JAVA中Volatile关键字详解

什么是Volatile

Volatile是java虚拟机提供的<轻量级>的同步机制,synchronized太重了

Volatile的3大特性是什么

  1. 保证了JMM的可见性

  2. 不保证JMM的原子性

  3. 禁止指令重排

什么是JMM ( java memory model )

JMM(java memory model),jmm本身是一个抽象的概念,并不真实存在 他描述的是一组规则或规范 (类似于12生肖中的龙并不真实存在 ) , 规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问形式

JMM定义的规范是什么

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

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

  3. 加锁解锁是同一把锁

JMM中工作内存,主内存,共享变量之间的关系

JVM运行程序的实体是线程,而每一个线程创建时JVM都会为其创建一个工作内存(栈),工作内存是每个线程的私有数据区域

  1. java内存模型中规定所有的变量都存储在主内存,主内存是共享内存区域,所有的线程都可以访问,

  2. 但线程对变量的操作(读取赋值等)必须在工作内存中进行,

  3. 首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存

  4. 不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存完成,其简要访问过程如下图

在这里插入图片描述

举个例子:包饺子

背景:A,B,C 一家3口坐在一起包饺子,每个人都有自己的 饺子皮、饺子馅 包自己的饺子,3个人共用同一个砧板(主内存), 砧板上的饺子的数量就是共享变量

  1. 砧板上饺子的数量,每个人数了之后就知道了 是共享变量
  2. 砧板上饺子总数的修改,由每个人来执行
  3. 首先数出砧板上目前有多少个饺子,然后加上自己现在包的这一个饺子,算出总数后,将饺子放回到砧板上,并将砧板上记录饺子总数的值修改成我的值
  4. 每个人都是执行着上述操作,每个人都记录着饺子的总数,且可以修改饺子的总数,但是每2个人之间不能共同包一个饺子(我把皮摊开,你把馅放进来),不能知道对方的饺子一共包了多少个

有了上述知识后,什么是JMM的可见性

在第3步执行完成了以后,其他人现在并不知道饺子的总数是多少,此时 我就需要通知其他人,”来看一眼啊 饺子的数量现在变多了一个“ 如果来得及的话,其他人执行相加操作的时候, 就需要在我操作后的基础上 +1 而不是在原来的基础上 +1

简单的说:A线程修改了共享变量,其他线程要马上就能知道共享变量中的值,立刻收到最新消息这个机制叫可见性

代码示例:

思路:开启2个线程,在使用volatile 和不适用volatile的情况下,A修改 方法中变量的值,并将此值设置为循环的条件,如果循环终结 则说明保证了可见性,如果循环不终结,则说明没有保证可见性

public class Test {

    public static void main(String[] args) {
        OkSee okSee = new OkSee();
        new Thread(() -> {
            System.out.println("thread start");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            okSee.setVal();
            System.out.println("set num val");
        }, "test").start();
        //怎样得到当前线程数量
        while (okSee.getNum() == 0L) {
        }
      
    }
}

@Data
class OkSee{
    private volatile Long num = 0L;
    public void setVal() {
        num = 10L;
    }
}

使用volatile 可以看到程序正常终止了
JAVA中Volatile关键字详解_第1张图片

不使用volatile,可以看到程序设置值之后,不会终止,还是会继续执行
JAVA中Volatile关键字详解_第2张图片

什么是volatile不保证 JMM原子性

在上述例子的基础上: 执行第3步操作的时候,如果A、B, 2个人同时包好了个字的饺子,以同样的速度数出了饺子目前的总数,同时自己把共享变量的值 在原来的基础上 +1, 那么问题来了,

  1. 首先 A 把共享变量中的值添加了1,及时通知了其他线程,
  2. 但是我B线程还没有收到这个通知的时候,就已经把自己计算后得到的总数写入到主内存中了

操作结果: 主内存相较于正常情况,是少了1个的,那么怎么解决呢,juc包下面的 atomic包下面的工具类了 这个后续会详细讲解

案例:使用20个线程,每隔线程跑1000次,那么最终得到的结果理论上应该是2w

//本案例中使用到了lombok包,需要自行导入
public class Test {

    public static void main(String[] args) {
        AtomicTest atomicTest = new AtomicTest();
        for (int i = 1; i <= 20; i++) {
            new Thread(() ->{
                for (int j = 1; j <= 1000; j++) {
                    atomicTest.add();
                }

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

        //判断程序是否执行完成,如果执行完成则终止,如果没有执行完成,则继续执行子线程
        //默认是main线程, 然后后台有一个GC线程
        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(atomicTest.getNum());
    }
}

@Data
class AtomicTest{
    private Integer num = 0;
    public void add(){
        num ++;
    }
}

不管num字段是否添加volatile字段,每次执行的结果都不同,可能会达到2w
JAVA中Volatile关键字详解_第3张图片

那么怎样解决Volatile不保证原子性带来的问题

使用juc atomic包下的工具类,例如对上面案例进行修改

//本案例中使用到了lombok包,需要自行导入
public class Test {

    public static void main(String[] args) {
        AtomicTest atomicTest = new AtomicTest();
        for (int i = 1; i <= 20; i++) {
            new Thread(() ->{
                for (int j = 1; j <= 1000; j++) {
                    atomicTest.add();
                }

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

        //判断程序是否执行完成,如果执行完成则终止,如果没有执行完成,则继续执行子线程
        //默认是main线程, 然后后台有一个GC线程
        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(atomicTest.getNum());
    }
}

@Data
class AtomicTest{
    private AtomicInteger num = new AtomicInteger(0);

    public void add(){
        num.incrementAndGet();
    }
}

什么叫做Volatile禁止指令重排

什么是指令重排(JMM有序性)

计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排

20190421125353566

指令重排的特点
  1. 在单线程环境里面 确保程序最终执行结果和代码顺序执行结果一致 即不管怎么重排序,程序的执行结果不能改变
  2. 处理器在进行指令重排序时必须考虑指令之间的数据依赖性
在多线程中可能引发的问题:

多线程中线程交替执行,由于编译器指令重排的存在,2个线程使用的变量能否保证一致性是无法确认的,结果无法预测

举个例子:

单线程情况下
double pi = 3.14;    // A
double r = 1;        // B
double s= pi * r * r; // C

上面的语句,可以按照 A->B->C执行,结果为 3.14,但是也可以按照 B->A->C的顺序执行, – 指令重排序

因为 A、B是两句独立的语句,而 C则依赖于 AB,所以 AB可以重排序,但是 C却不能排到 AB的前面。 – 数据依赖性

JMM保证了重排序不会影响到单线程的执行,但是在多线程中却容易出问题。

多线程情况下
int a = 0;
boolean flag = false;

public void write() {
  a = 2;              //1
  flag = true;        //2
}

public void multiply() {
  if (flag) {         //3
    int ret = a * a;	//4
  }
}

假如有两个线程执行上述代码段,线程 1先执行 write方法 线程 2再执行 multiply,最后 ret的值一定是 4吗?结果不一定:

JAVA中Volatile关键字详解_第4张图片

如图所示, write方法里的 12做了重排序,线程 1先对 flag赋值为 true,随后执行到线程 2ret直接计算出结果,再到线程 1,这时候 a才赋值为 2,很明显迟了一步。

这时候可以为 flag加上 volatile关键字,禁止重排序,可以确保程序的“有序性”,也可以上重量级的 synchronizedLock来保证有序性,它们能保证那一块区域里的代码都是一次性执行完毕的。

你可能感兴趣的:(多线程)