轻量级的同步机制----关键字volatile

今天看到关键字volatile,一头雾水,这是啥?
在这里插入图片描述

赶紧学习学习。

在详细了解这个volatile,要先了解一下JMM.

Java内存模型简称JMM(Java Memory Model)

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

示意图:

这里需要解释几个概念:

  • .主内存(Main Memory)

    主内存可以简单理解为计算机当中的内存,但又不完全等同。主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。

  • .工作内存(Working Memory)

    工作内存可以简单理解为计算机当中的CPU高速缓存,但又不完全等同。每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。

线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行。

回到正题:

1,什么是volatile

volatile 是一个类型修饰符。
volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略。

被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象

2, volatile 的特性

  • 实现可见性 (利用java 先行发生原则)

    当一个线程修改了变量的值,新的值会立刻同步到主内存当中。
    而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值
    
  • 实现有序性 (利用内存屏障)

    禁止进行指令重排序。
    

    `volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。

说到这里,不得不提到同步的关键字:synchronized,synchronized是阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁。而volatile就可以说是java虚拟机提供的最轻量级的同步机制,简单来说volatile就是简化版的synchronized,但是volatile不能保证线程安全。在这里插一句:volatile只是轻量级的线城可见方式,并不是轻量级的同步方式,所以说volatile时轻量级的synchonized是片面的
如下案列:希望最后是10000,然而并不是。

package com.example.demo.volatileTest;

public class valatileTest {
    private volatile static int count;

    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();
                            }
                            for (int j = 0; j <1000 ; j++) {
                                count++;
                            }
                        }
                    }
            ).start();
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count:"+count);
    }
}
// 使用volatile修饰的变量,为什么并发自增的时候会出现这样的问题呢?这是因为count++这一行代码本身并不是原子性操作
// 一开始获取主内存数据为0,可是count++时,0++,但主内存可能已经是8了。
  

下面来说说,
可见性

这个特性依赖于java语言的先行发生原则(happens-before)。

先行发生原则:在计算机科学中,先行发生原则是两个事件(各种指令操作,比如读操作、写操作、初始化操作、锁操作等等)的结果之间的关系,
如果一个事件发生在另一个事件之前,结果必须反映,即使这些事件实际上是乱序执行的(通常是优化程序流程)。

指令重排

JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,
对现有的指令顺序进行重新排序。

目的是为了在不改变单线程下的程序执行结果的前提下,优化程序的运行效率。

我们来看看下面的例子(这里java代码的重排只是为了简单示意,真正的指令重排是在字节码指令的层面。):

boolean contextReady = false;
在线程A中执行:
context = loadContext();
contextReady = true;

 

在线程B中执行:

while( ! contextReady ){ 
   sleep(200);
}
doAfterContextReady (context);
以上程序看似没有问题。线程B循环等待上下文context的加载,一旦context加载完成,contextReady == true的时候,才执行doAfterContextReady 方法。
但是,如果线程A执行的代码发生了指令重排,初始化和contextReady的赋值交换了顺序:
boolean contextReady = false;
在线程A中执行:
contextReady = true;
context = loadContext();


在线程B中执行:
while( ! contextReady ){ 
   sleep(200);
}
doAfterContextReady (context);
这个时候,很可能context对象还没有加载完成,变量contextReady 已经为true,
线程B直接跳出了循环等待,开始执行doAfterContextReady 方法,结果自然会出现错误。

了解了优化排序,那volatile如何避免这个优化呢,答案就是内存屏障

  • 什么是内存屏障:

    内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。 
    这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。
    
  • 内存屏障共分为四种类型:

    • LoadLoad屏障:

      抽象场景:Load1; LoadLoad; Load2
      
      Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
      
    • StoreStore屏障:

      抽象场景:Store1; StoreStore; Store2
      	
      Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见
      
    • LoadStore屏障:

      抽象场景:Load1; LoadStore; Store2
      
      在Store2被写入前,保证Load1要读取的数据被读取完毕。
      
    • StoreLoad屏障:

      抽象场景:Store1; StoreLoad; Load2
      
      在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的。
      

在一个变量被volatile修饰后,JVM会为我们做两件事:

1.在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。

2.在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。

使用场景

  • 运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。

synchronized和volatile的区别

  • 作用范围

      volatile仅能使用在变量级别;
       synchronized则可以使用在变量、方法、和类级别的
    
  • 可见性与原子性

      volatile仅能实现变量的修改可见性,并不能保证原子性;
      synchronized则可以保证变量的修改可见性和原子性,线程安全
    
  • 线程堵塞

      volatile不会造成线程的阻塞;
       synchronized可能会造成线程的阻塞。
    
  • 编译器优化

      volatile标记的变量不会被编译器优化();
      synchronized标记的变量可以被编译器优化
    

文献:

参考1

参考2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值