JUC-Volatile

1.1 volatile的理解

volitile 是 Java 虚拟机提供的轻量级的同步机制,三大特性:

  • 保证可见性。
  • 不保证原子性
  • 禁止指令重排。

1.2 JMM基本概念

JMM 本身是一种抽象的概念,并不真实存在,它描述的是一组规则或者规范。

JMM 同步的规定:

  • 线程解锁前,必须把共享变量的值刷新回主内存。
  • 线程加锁前,必须读取主内存的最新值到自己的工作内存。
  • 加锁解锁是同一把锁。

JMM即为JAVA 内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。

1.2.1 JMM内存模型

JMM规定了内存主要划分为主内存工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存

代码示例

package cn.guardwhy.volatile01;

import java.util.concurrent.TimeUnit;

/*
* JMM内存模型
*/
public class JMMDemo01 {
    private static int num = 0;
    public static void main(String[] args) { // 线程1(main线程)
        new Thread(()->{ // 线程2
            while (num == 0){

            }
        }).start();

        try {
            // 休眠1s
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        num = 1;
        System.out.println("num的值:" + num);
    }
}

线程1感知不到线程2操作了值的变化!如何能够保证线程间可以同步感知这个问题呢?只需要使用Volatile关键字即可!volatile 保证线程间变量的可见性,简单地说就是当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动,更详细地说是要符合以下两个规则 :

  • 线程对变量进行修改之后,要立刻回写到主内存。
  • 线程对变量读取的时候,要从主内存中读,而不是缓存
  • 各线程的工作内存间彼此独立,互不可见,在线程启动的时候,虚拟机为每个内存分配一块工作内存,不仅包含了线程内部定义的局部变量,也包含了线程所需要使用的共享变量(非线程内构造的对象)的副本,即,为了提高执行效率。
1.2.2 内存交互操作

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的。

  • lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态。
  • unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中。
  • use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令。
  • assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中。
  • store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
  • write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

JMM对这八种指令的使用,制定了如下规则

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write。
  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存。
  • 不允许一个线程将没有assign的数据从工作内存同步回主内存。
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作。
  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁。
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存。
1.2.3 结论

JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析,更多的时候,使用java的happen-before规则来进行分析。

happen-before规则

happens-before就是先行发生,在Java内存模型中,happens-before 应该翻译成:前一个操作的结果可以被后续的操作获取。通俗点说就是前面一个操作把变量赋值为3,那后面一个操作肯定知道变量已经变成了3。所以为了解决多线程的可见性问题,就搞出了happens-before原则,让线程之间遵守这些原则。编译器还会优化java语句,所以等于是给了编译器优化的约束。

1.3 保证可见性

代码示例

package cn.guardwhy.volatile01;
import java.util.concurrent.TimeUnit;
/*
 保证可见性
*/
public class VolatileDemo01 {
    // 不加 volatile程序会出现死循环,加上volatile可以保证可见性。
    private volatile static int num = 0;

    public static void main(String[] args) {
        new Thread(()->{ // 线程2对主内存的变化不知道
            while (num == 0){

            }
        }).start();

        try {
            // 休眠1s
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        num = 1;
        System.out.println("num的值:" + num);
    }
}

1.4 不保证原子性

原子性理解:不可分割,完整性,也就是某个线程正在做某个具体的业务的时候,中间不可以被加塞或者被分割,要整体完整,要么同时成功,要么同时失败。

代码示例

package cn.guardwhy.volatile01;
/*
验证volatile 不保证原子性
*/
public class VolatileDemo02 {

    private volatile static int num = 0;

    public static void add(){
        num++;
    }

    public static void main(String[] args) {
        // 1.理论上num的值应该为2w
        for (int i = 1; i <= 20 ; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    // 2.调用方法
                    add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){
            // 礼让
            Thread.yield();
        }

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

执行结果

命令行查看底层字节码代码实现:javap -c VolatileDemo02.class

num++ 在多线程下是非线程安全的

不加Lock锁和synchronized,如何保证原子性

使用原子类, 解决原子性的问题。

代码示例

package cn.guardwhy.volatile01;

import java.util.concurrent.atomic.AtomicInteger;

// volatile 不保证原子性
public class VolatileDemo02 {
    // 原子类的Integer
    private volatile static AtomicInteger num = new AtomicInteger();

    public static void add(){
        num.getAndIncrement(); // AtomicInteger + 1 方法, CAS
    }

    public static void main(String[] args) {
        // 1.理论上num的值应该为2w
        for (int i = 1; i <= 20 ; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    // 2.调用方法
                    add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){
            // 礼让
            Thread.yield();
        }

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

执行结果

源码分析:
这些原子类的底层都直接和操作系统挂钩!!!允许在内存中修改值!!unsafe类是个很特殊的存在。

1.5 指令重排

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

  • 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
  • 处理器在进行重排序时必须要考虑指令之间的数据依赖性
  • 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。
int x = 1; // 1
int y = 2; // 2
x = x + 5; // 3
y = X * x; // 4

期待结果是: 1234 但是可能执行的时候会变成 2134 1324, 但是不可能是 4123!!!

可能造成影响的结果: a b x y 这四个值默认都是 0

线程A线程B
x = ay = b
b = 1a = 2

正常的结果:x = 0; y = 0; 但是可能由于指令重排

线程A线程B
b = 1a = 2
x = ay = b

指令重排导致的诡异结果: x = 2; y = 1;

volitile可以避免指令重排:

内存屏障, CPU指令。基本作用:

1、保证特定的操作。

2、可以保证某些变量的内存可见性(利用这些特性volitile实现了可见性)。

Volitile 是可以保持可见性,不能保证原子性,但是由于内存屏障,可以保证避免指令重排的现象产生!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值