Java 程序员都该懂的 volatile 关键字
今天,来谈谈 Java 并发编程中的一个基础知识点:volatile 关键字
本篇文章主要从可见性,原子性和有序性进行讲解
一. 主存与工作内存
说 volatile 之前,先来聊聊 Java 的内存模型。
在 Java 内存模型中,规定了所有的变量都是存储在主内存当中,而每个线程都有属于自己的工作内存。线程的工作内存保存了被该内存使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必需在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其余线程的工作内存。
对于单线程的程序,这样的规定没有任何影响;但是对于多线程的程序,便可能导致,某个线程已经改变了主内存中的变量,而另一个线程还在使用其工作内存中的变量,因而造成了数据的不一致。
二. 可见性
volatile 可以保证数据的可见性,前面说到对于多线程的程序可能会造成数据不一致,但是当一个变量加上 volatile 之后,便可以保证,其余线程读取到的该变量都是最新值。
这是由于每当对该变量进行写操作时,都会使得其余线程工作变量中的该变量的拷贝失效,而迫使线程们都重新去主内存读取
我们来看看实例:
public class TestThread extends Thread { private volatile boolean isRunning = true; public void setRunning(boolean running) { isRunning = running; } @Override public void run() { int i = 1; while (isRunning) { i++; } System.out.println(i); }}
public class Test { public static void main(String[] args) throws InterruptedException { TestThread thread = new TestThread (); thread.start(); Thread.sleep(3000); thread.setRunning(false); }}
当 isRunning 变量没有增加 volatile 变量时,该程序会发生死循环,由于setRunning(false)
并没有影响到 thread 所在线程的工作内存(这时该线程看到的值依然是 true)
当我们为变量增加上 volatile 之后,setRunning(false)
执行完毕,thread 所在线程的工作内存的变量拷贝便就此作废,必需去主内存获取最新的值,死循环也因而不会再发生了
值得注意的是,当我们在循环中增加了打印语句,或者者 sleep 方法等,这时无论有没有 volatile,都会中止循环,如:
while (isRunning) { i++; System.out.println(i);}
这是由于,JVM 会尽力保证内存的可见性,本来的代码中,程序一直处于死循环,这时 JVM 没有办法强制要求 CPU 分出时间去保证可见性;但是当加上打印语句之后,CPU 便会分出时间去解决这件事情,并保证了可见性;但是,与之不同的是,volatile 是强制保证可见性的。
三. 原子性
volatile 没有办法保证操作的原子性的
直接上代码:
public class AtomicTest { private static volatile int race = 0; private static void increase() { race++; } private static final int THREADS_COUNT = 20; public static void main(String[] args) { Thread[] threads = new Thread[THREADS_COUNT]; for (int i = 0; i < 10; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < 1000; j++) { increase(); } }); threads[i].start(); } //等待所有累加线程都结束 while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(race); }}
这段代码摘自《深入了解 Java 虚拟机》12 章,当 race 没有 volatile 关键字的加持时,最终的打印结果经常会小于 10000,而有了 volatile,这段程序变不再出现这种情况。
假设两个线程 1 和 2,它们俩先后读取了 race 的值(初始值为 0),因为它们都还没有进行写操作,因而两个线程这时看到的值都是 0,因而便使得之后两次自增操作的结果是 1,而不是 2
刚刚说到 volatile 变量在进行写操作的时候,会让其余线程对应的工作内存中的拷贝失效,使得需要直接去主存中读取变量,而上例中线程 1 在进行写操作之前,线程 2 便已经执行了读操作,因而没办法影响线程 2 的读取,因而也不会升级为最新的数据了
四. 有序性
volatile 可以在肯定程度上禁止指令重排序
//x、y为非volatile变量//flag为volatile变量 x = 2; //语句1y = 0; //语句2flag = true; //语句3x = 4; //语句4y = -1; //语句5
flag 变量增加上 volatile 关键字以后,语句 1,2 不会排在 3 的后面执行,当然 4,5 也不会在 3 的前面执行
但是 1 和 2, 3 和 4 之间的顺序没办法干预,这也是我们说“肯定程度改变”的起因
上个例子:
//线程1:context = loadContext(); //语句1inited = true; //语句2 //线程2:while(!inited){ sleep()}doSomethingwithconfig(context); //出错,context 可能还没有初始化
面对这样的例子的时候,假如 inited 是非 volatile 变量,那么由于重排序的关系,有可能出错;但是加上 volatile 后便不用担心了
五. 使用场景
1. 状态变量:
比方上面给出的可见性的代码例子
while (isRunning) { i++;}
对于这种用于标记状态的变量,volatile 是非常好用的
2. 双重检验:
最经典的就是单例模式的双重检验实现,假如忘了的恰好复习一下:
public class Singleton { private volatile static Singleton singleton; private Singleton() { } public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }}
这里的 volatile,是为了保证 singleton = new Singleton();
操作的有序性,由于 singleton = new Singleton();
并不是原子操作,做了 3 件事
- 给 singleton 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将 singleton 对象指向分配的内存空间(执行完这步 singleton 就为非 null 了)
但是因为重排序的起因,1-2-3 的顺序可能变成 1-3-2,假如是后者,在 singleton 变成非 null 时(即第三步),假如第二个线程开始进入第一个判断 if (singleton == null)
,那么便会直接返回 true,然而事实上 singleton 还没有完成初始化
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Java 程序员都该懂的 volatile 关键字