详解volatile关键字

一、volatile关键字的两大作用

1.1、保证线程间可见性

JAVA进程中的所有线程是共享堆内存的,线程也有自己的内存,当其中一个线程修改了共享变量时,另一个线程的值可能没有及时更新,volatile关键字标志变量为易变的,CPU会实时监听。保证线程间可见性是基于缓存一致性协议实现的。
当volatile修饰的是引用类型时,引用类型中的变量发生变化是监测不到的,所以volatile一般用于修饰基本数据类型

1.2、防止指令重排

1.2.1、什么是指令重排?

CPU为了提高执行效率,当两条指令没有依赖关系时,执行的顺序可能会发生变化,如CPU在进行读等待时,会去执行下一条指令,前提是指令没有依赖关系。

1.2.2、volatile是如何实现防止指令重排的?

  • 字节码层面:
变量的修饰符access_flags中包含了ACC_VOLATILE标识
  • JVM层面(HotSpot虚拟机):
JVM屏障规范:Store、Load
StoreStoreBarrier - volatile变量的写操作 - StoreLoadBarrie
LoadLoadBarrier - volattile变量的读操作 - LoadStoreBarrier
  • 操作系统层面:
CPU内存屏障
sfence:(save)在sfence指令前的写操作必须在指令后面的写操作前完成。
lfence:(load)在ifence指令前的写操作必须在指令后面的写操作前完成。
mfence:(mix)在mfence指令前的读写操作必须在指令后面的读写操作前完成。
原子指令,lock指令,执行时会锁住内存子系统来确保执行顺序

1.2.3、JVM规范中的8大happen-before原则

  1. 程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!
  2. 管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!
  3. volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
  4. 线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
  5. 线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
  7. 传递规则:这个简单的,就是happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C。
  8. 对象终结规则:这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
    JAVA语言规范文档中有具体说明,jls12.pdf中的第674页有详细介绍happen-before原则,提取码:36sa
    文档链接

二、证明volatile可保证线程间可见性的程序

import java.util.concurrent.TimeUnit;

/**
 * @author IT00ZYQ
 * @date 2021/5/22 20:50
 **/
public class TestVolatile {
    public /*volatile*/ boolean running = true;

    public void m() {
        System.out.println("m() start ...");
        while (running) {

        }
        System.out.println("m() end ...");
    }

    public static void main(String[] args) {
        TestVolatile t = new TestVolatile();

        new Thread(t::m).start();

        try {
            // 主线程睡一秒
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 此时在主线程修改running为false
        // 如果running没有加volatile,此时虽然修改了running
        // 但是由于每个线程会有自己的缓存
        // 没有及时更新,导致running变量的修改对其他线程并不可见
        // volatile可保证线程间可见性
        t.running = false;
    }


}

三、证明指令重排序存在的程序

/**
 * @author IT00ZYQ
 * @Date 2021/3/9 23:17
 **/
public class T10_InstructionRearrangement {
    public static int a = 0, b = 0, x = 0, y = 0;

    public static void main(String[] args) throws InterruptedException {
        int count = 0;
        while(true) {
            a = 0;
            b = 0;
            x = 0;
            y = 0;
            Thread thread1 = new Thread(() -> {
                a = 1;
                x = b;
            });

            Thread thread2 = new Thread(() -> {
                b = 1;
                y = a;
            });

            thread1.start();
            thread2.start();

            thread1.join();
            thread2.join();

            // 如果没有指令重排序,可能的情况有
            // x = 1 y = 1
            // x = 0 y = 1
            // x = 1 y = 0
            // 当出现了x=0,y=0说明发生了指令重排
            count ++;
            if (x == 0 && y == 0){
                System.out.println("运行次数:" + count);
                System.out.printf("x = %d, y = %d\n", x, y);
                break;
            }
        }
    }
}

运行结果:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

it00zyq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值