返回介绍

2.3 volatile 与 Java 内存模型 JMM

发布于 2024-08-21 22:20:21 字数 2637 浏览 0 评论 0 收藏 0

之前已经简单介绍了Java内存模型(JMM),Java内存模型都是围绕着原子性、有序性和可见性展开的。大家可以先回顾一下上一章中的相关内容。为了在适当的场合,确保线程间的有序性、可见性和原子性。Java使用了一些特殊的操作或者关键字来申明、告诉虚拟机,在这个地方,要尤其注意,不能随意变动优化目标指令。关键字volatile就是其中之一。

如果你查阅一下英文字典,有关volatile的解释,你会得到最常用的解释是“易变的,不稳定的”。这也正是使用volatile关键字的语义。

当你用volatile去申明一个变量时,就等于告诉了虚拟机,这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够“看到”这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点。

比如,根据编译器的优化规则,如果不使用volatile申明变量,那么这个变量被修改后,其他线程可能并不会被通知到,甚至在别的线程中,看到变量的修改顺序都会是反的。但一旦使用volatile,虚拟机就会特别小心地处理这种情况。

大家应该对上一章中介绍原子性时,给出的MultiThreadLong案例还记忆犹新吧!我想,没有人愿意就这么把数据“写坏”。那这种情况,应该怎么处理才能保证每次写进去的数据不坏呢?最简单的一种方法就是加入volatile申明,告诉编译器,这个long型数据,你要格外小心,因为他会不断地被修改。

下面的代码片段显示了volatile的使用,限于篇幅,这里不再给出完整代码:

public class MultiThreadLong { public volatile static long t=0; public static class ChangeT implements Runnable{ private long to; ……

从这个案例中,我们可以看到,volatile对于保证操作的原子性是有非常大的帮助的。但是需要注意的是,volatile并不能代替锁,它也无法保证一些复合操作的原子性。比如下面的例子,通过volatile是无法保证i++的原子性操作的:

01 static volatile int i=0;
02 public static class PlusTask implements Runnable{
03   @Override
04   public void run() {
05     for(int k=0;k<10000;k++)
06       i++;
07   }
08 }
09
10 public static void main(String[] args) throws InterruptedException {
11   Thread[] threads=new Thread[10];
12   for(int i=0;i<10;i++){
13     threads[i]=new Thread(new PlusTask());
14     threads[i].start();
15   }
16   for(int i=0;i<10;i++){
17     threads[i].join();
18   }
19
20   System.out.println(i);
21 }

执行上述代码,如果第6行i++是原子性的,那么最终的值应该是100000(10个线程各累加10000次)。但实际上,上述代码的输出总是会小于100000。

此外,volatile也能保证数据的可见性和有序性。下面再来看一个简单的例子:

01 public class NoVisibility {
02   private static boolean ready;
03   private static int number;
04
05   private static class ReaderThread extends Thread {
06     public void run() {
07       while (!ready);
08       System.out.println(number);
09     }
10   }
11
12   public static void main(String[] args) throws InterruptedException {
13     new ReaderThread().start();
14     Thread.sleep(1000);
15     number = 42;
16     ready = true;
17     Thread.sleep(10000);
18   }
19 }

上述代码中,ReaderThread线程只有在数据准备好时(ready为true),才会打印number的值。它通过ready变量判断是否应该打印。在主线程中,开启ReaderThread后,就为number和ready赋值,并期望ReaderThread能够看到这些变化并将数据输出。

在虚拟机的Client模式下,由于JIT并没有做足够的优化,在主线程修改ready变量的状态后,ReaderThread可以发现这个改动,并退出程序。但是在Server模式下,由于系统优化的结果,ReaderThread线程无法“看到”主线程中的修改,导致ReaderThread永远无法退出(因为代码第7行判断永远不会成立),这显然不是我们想看到的结果。这个问题就是一个典型的可见性问题。

注意:可以使用Java虚拟机参数-server切换到Server模式。

和原子性问题一样,我们只要简单地使用volatile来申明ready变量,告诉Java虚拟机,这个变量可能会在不同的线程中修改。这样,就可以顺利解决这个问题了。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文