volatile 关键字理解

本文介绍了Java中volatile关键字,它是类型修饰符,可保证变量内存可见性。阐述了其保证可见性、不能确保原子性、保证有序性的特性,并通过可见性和原子性测试代码进行验证,还解释了在while循环加System.out.println使线程停止的原因。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。即 volatile 关键字可以保证其声明的变量内存可见性。

volatile 关键字的特性:
1) 保证可见性
系统总是重新从它所在的内存读取数据

2) volatile不能确保原子性
在Java中,原子性是指:对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

但是,对于64位long 和double类型的变量,若JVM是32位的,32位的JVM会将64位数据的读写操作分为2次32位的读写操作,这就导致了long、double类型的变量在32位虚拟机中是非原子操作,即线程不安全的。

3)保证有序性
会禁止进行指令重排序。

1、可见性测试代码如下:

/**
 * @ClassName: VolatileTest
 * @Description: volatile 内存可见性测试
 * @date: 2018年8月31日 下午10:35:33
 */
public class VolatileTest extends Thread {
  // 停止线程
  private volatile boolean stopThread = false;

  @Override
  public void run() {
    System.out.println(Thread.currentThread().getName() + " -- run()");
    int i = 0;
    while (!stopThread) {
	  // System.out.println(Thread.currentThread().getName()+"运行中:"+i);
      i++;
    }
    // 线程停止后打印i
    System.out.println(i);
  }

  public void stopThread() {
    System.out.println(Thread.currentThread().getName() + " -- stopThread()");
    stopThread = true;
  }

  /**
   * @Title: main
   * @Description: volatile 内存可见性测试
   * @param args
   */
  public static void main(String[] args) {
    VolatileTest thread = new VolatileTest();
    thread.start(); // 线程启动并执行run方法
    try {
      Thread.sleep(500);
      thread.stopThread();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

1)当没有加 volatile 关键字运行的结果是:
Thread-0 – run()
main – stopThread()
没有输出i值,说明自定义线程没有结束,而且一直在执行run方法。

2)当加上 volatile 关键字运行的结果是:
Thread-0 – run()
main – stopThread()
最终结果:1356228826 (每次运行的结果值不固定)
自定义线程读取到了main 线程改变后的 stopThread 变量值,自定义线程结束,说明 volatile 关键字保证了变量的可见性。

3) 当在while循环中加入System.out.println()这句代码时:

while (!stopThread) {
  System.out.println(Thread.currentThread().getName()+"运行中:"+i);
  i++;
}

运行结果为:
Thread-0 – run()
Thread-0运行中:0
Thread-0运行中:1

Thread-0运行中:32265
Thread-0运行中:32266
main – stopThread()
Thread-0运行中:32267
最终结果:32268

结果说明加了System.out.println这条语句后,线程也可以停止,即 变量 i 同步到了主线程,这个问题让我疑惑了很长时间,虽然后面有向高手请教说是 println() 方法有锁(有synchronized关键字)而使变量同步,但还是感觉没解释到根本点上。

后面在网上搜到了这篇博客 《多线程:为什么在while循环中加入System.out.println,线程可以停止》,其中解释了问题产生的原因,下面3段粗体文字为引用的博客内容(有改动):

加了System.out.println()之后,线程能停止了。有人会说,println()方法的源码里面有synchronized关键字,所以会同步变量stopThread的值。这种是很不正确的理解,同步关键字同步的是同步块里面的变量,stopThread变量在这个同步代码块之外。

真正的原因是:JVM会尽力保证内存的可见性,即便这个变量没有加同步关键字。换句话说,只要CPU有时间,JVM会尽力去保证变量值的更新。这种与volatile关键字的不同在于,volatile关键字会强制的保证线程的可见性。而不加这个关键字,JVM也会尽力去保证可见性,但是如果CPU一直有其他的事情在处理,它也没办法。最开始的代码,一直处于循环中,即CPU一直被占用的时候,这个时候CPU没有时间,JVM也不能强制要求CPU分点时间去取最新的变量值。而加了System.out.println之后,由于内部代码的同步关键字的存在,导致CPU的输出其实是比较耗时的。这个时候CPU就有可能有时间去保证内存的可见性,于是while循环可以被终止。

其实,也可以在while循环里面加上sleep,让run方法放弃cpu,但是不放弃锁,这个时候由于CPU有空闲的时候就去按照JVM的要求去保证内存的可见性。如run方法里面加个sleep,cpu有充足的空闲时间去取变量的最新值,所以循环执行一次就停止了。

加上sleep的代码如下:

while (!stopThread) {
    // System.out.println(Thread.currentThread().getName() + "运行中:" + i);
    try {
      Thread.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
      i++;
    }

运行结果为:
Thread-0 – run()
main – stopThread()
最终结果:9

2、原子性测试代码如下:

/**
 * @ClassName: VolatileActomicityTest
 * @Description: volatile 内存原子性测试
 * @date: 2018年8月31日 下午10:35:33
 */
public class VolatileActomicityTest {

  private volatile static int count = 0;

  /**
   * @Title: main
   * @Description: volatile 内存可见性测试
   * @param args
   */
  public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
      Runnable r = () -> {
        for (int j = 0; j < 1000; j++) {
          count++;
        }
      };
      new Thread(r).start();
    }
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println(count);
  }
}

运行3次的结果:
8181
7562
9522

开启10个线程,每个线程实现对count自加1000次,若线程安全结果应该是 100000,但此时多次运行结果都小于10000,说明 volatile 关键字并不能保证原子性。

此处 count++ 并不是一个原子操作,该操作包含了三个步骤:
1.读取变量counter的值;
2.对counter加1;
3.将新值赋值给变量count。
如果线程0读取count到工作内存后,其他线程对这个值已经做了自增操作后,线程0对其他线程修改的count值是不可见的,因此,最终就是小于等于10000的各种可能结果。

附 JMM 图如下:
这里写图片描述



参考资料:

  1. 深入理解Java虚拟机
    三大性质总结:原子性,有序性,可见性
  2. 多线程:为什么在while循环中加入System.out.println,线程可以停止
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值