Java‘volatile conception

一、Java内存模型

Java内存模型简称JMM (Java Memory Model),是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。

主内存(Main Memory):主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。

工作内存(Working Memory):每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。

工作内存类似高速缓存、Cache,而主存类似于内存,直接操纵主内存速度太慢,所以JVM更倾向于在工作内存操纵线程。线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行。

JMM工作流程:定义一个静态变量  static int s = 0;线程A执行操作将s修改为3:s = 3;

1)、Thread A先进行读操作,读取主内存 s 值,此时s = 0 ,并更新工作内存s值, s = 0;

2)、在工作内存进行写操作,将s 由 0 写为 3;

3)、同步更新主内存s值,在主内存进行写操作,将s 由 0 改为 3;

通过一系列内存读写的操作指令,线程A把静态变量 s=0 从主内存读到工作内存,再把 s=3 的更新结果同步到主内存当中。从单线程的角度来看,这个过程没有任何问题。此时引入线程B,则结果将会出现两种状态:① Thread B 读出 s = 0, ②Thread B 读出 s = 3。

出现上述结果原因时在,Thread A 进行同步主内存 s = 3 写操作前 ,Thread B 进行主线程的 s 值读取操作,则此时得到结果为s = 0,而当Thread A 进行同步主内存 s = 3 写操作后,Thread B 进行主线程的 s 值读取操作,则此时得到结果为s = 3。

由于 Synchronized 锁会影响程序性能,所以轻量级锁 volatile 出现了。

二、Volatile

volatile关键字其中最重要的特性就是保证了用 volatile 修饰的变量对所有线程的可见性。可见性指当一个线程修改了变量的值,新的值会立刻同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。volatile 关键字特性得益于java语言的先行发生原则(happens-before)。

先行发生原则:两个事件的结果之间的关系,如果一个事件发生在另一个事件之前,结果必须反映,即使这些事件实际上是乱序执行的(通常是优化程序流程)。这里的事件指各种指令操作,比如读操作、写操作、初始化操作、锁操作等等。先行发生原则作用于很多场景下,包括同步锁、线程启动、线程终止、volatile。我们这里只列举出volatile相关的规则:对于一个volatile变量的写操作先行发生于后面对这个变量的读操作。

回到上述的代码例子,如果在静态变量s之前加上volatile修饰符:volatile static int s = 0;线程A执行如下代码:s = 3;这时候我们引入线程B,执行如下代码:System.out.println("s=" + s); 当线程A先执行的时候,把 s = 3 写入主内存的事件必定会先于读取s的事件。所以线程B的输出一定是 s = 0。

volatile 只保证变量的可见性,并不能保障变量的原子性,请看一下代码:

package com.demo.sync;

public class VolatileTest {
    public volatile static Integer count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e){
                        e.printStackTrace();
                    }

                    //每个线程将count自增100次;
                    for (int j = 0; j < 100; j++) {
                        count++;
                    }
                }
            }).start();
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(count);
    }
}

结果:开启10个线程,每个线程当中让静态变量count自增100次。执行之后会发现,最终count的结果值未必是1000,有可能小于1000,如下图

分析:count++这一行代码本身并不是原子性操作,在字节码层面可以拆分成如下指令:

getstatic        //读取静态变量(count)

iconst_1        //定义常量1

iadd               //count增加1

putstatic        //把count结果同步到主内存

虽然每一次执行 getstatic 的时候,获取到的都是主内存的最新变量值,但是进行iadd的时候,由于并不是原子性操作,其他线程在这过程中很可能让 count 自增了很多次。这样一来本线程所计算更新的是一个陈旧的count值,自然无法做到线程安全:

 

Volatile适用场景:

1.运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。

2.变量不需要与其他的状态变量共同参与不变约束。

volatile static int start = 3;

volatile static int end = 6;

//线程A执行如下代码:

while (start < end){

  //do something

}

//线程B执行如下代码:

start+=3;

end+=3;

这种情况下,一旦在线程A的循环中执行了线程B,start有可能先更新成 6,造成了一瞬间 start == end,从而跳出while循环的可能性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值