几乎所有应用程序都将数据读写到计算机的主存中,考虑到性能因素,这些操作不会直接在内存中执行。CPU提供缓存内存系统,应用程序在缓存中写入数据,然后将数据从缓存移到主内存。
在多线程应用中,并发线程在不同的CPU或CPU中不同的内核中运行。当线程修改存储在内存中的变量时,是在缓存或正在运行的CPU或内核中进行修改,但无法保证修改会在何时到达主内存。如果其它线程想要读取数据值,因为此数据不在计算机主存中,所以可能不会读取修改后的值。
为了解决这个问题(还有其它解决方案,例如synchronized关键字),Java语言提供volatile关键字。此关键字为修饰符,指定变量必须始终从主内存而不是CPU的缓存中读取并存储。当其它线程对变量的实际值具有可见性更重要时,应该使用volatile关键字,但访问此变量的顺序并不重要。在此场景中,volatile关键字提供更好的性能,因为它不需要任何监视器或锁来访问变量。与之相反,如果变量的访问顺序很重要,则必须使用另一种同步机制。
本节将学习如何使用易失性关键字及使用效果。
准备工作
本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。
实现过程
通过如下步骤实现范例:
-
创建名为Flag的类,包含名为flag的公有Boolean属性,初始值为true:
public class Flag { public boolean flag = true; }
-
创建名为VolatileFlag的类,包含名为flag的公有Boolean属性,初始值为true。在属性声明上添加volatile修饰符:
public class VolatileFlag { public volatile boolean flag = true; }
-
创建名为Task的类,指定其实现Runnable接口。包含私有Flag属性以及初始化此属性的构造函数:
public class Task implements Runnable{ private Flag flag; public Task(Flag flag) { this.flag = flag; }
-
实现此任务的run()方法。当flag属性值为true时,int变量加1。然后输出变量的最终结果到控制台:
@Override public void run() { int i = 0 ; while (flag.flag) { i++; } System.out.printf("Task: Stopped %d - %s\n", i, new Date()); } }
-
创建名为VolatileTask的类,指定其实现Runnable接口。包含私有VolatileFlag属性以及初始化此属性的构造函数:
public class VolatileTask implements Runnable{ private VolatileFlag flag; public VolatileTask(VolatileFlag flag) { this.flag = flag; }
-
实现此任务的run()方法,代码与Task类相同,不在这里列出:
@Override public void run() { int i = 0 ; while (flag.flag) { i++; } System.out.printf("VolatileTask: Stopped %d - %s\n", i, new Date()); } }
-
实现包含main()方法的Main类。首先,创建VolatileFlag、Flag、VolatileTask和Task类的四个对象:
public class Main { public static void main(String[] args) { VolatileFlag volatileFlag=new VolatileFlag(); Flag flag=new Flag(); VolatileTask vt=new VolatileTask(volatileFlag); Task t=new Task(flag);
-
然后,创建两个线程执行这些任务,启动线程,休眠主线程1秒钟:
Thread thread=new Thread(vt); thread.start(); thread=new Thread(t); thread.start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
-
接下来改变volatileFlag变量值来停止volatileTask执行,并且休眠主线程1秒钟 :
System.out.printf("Main: Going to stop volatile task: %s\n",new Date()); volatileFlag.flag=false; System.out.printf("Main: Volatile task stoped: %s\n", new Date()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
-
最后,改变task对象值来停止任务执行,并且休眠主线出1秒钟:
System.out.printf("Main: Going to stop task: %s\n", new Date()); flag.flag=false; System.out.printf("Main: Volatile stop flag changed: %s\n", new Date()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }
工作原理
下图显示本范例的输出结果:
因为task线程还没有结束,所以此应用并没有完成执行。当改变volatileFlag值时,因为flag属性标记为volatile,所以新值被写入到主内存中,且VolatileTask立即访问这个值并结束执行。与之相反,当改变flag对象值时,因为flag属性未标记为volatile,所以新值存储到主线程的缓存中,并且任务对象无法看到新值,始终不会结束执行。volatile关键字非常重要,不仅因为它需要刷新写操作,而且确保读取不会被缓存,并且从主内存中获取最新的值。volatile非常重要,但也经常被忽视。
考虑到volatile关键字保证修改是在主内存中写入的,但其相反情况并不总是正确的。 例如,处理被多个线程共享且做出大量修改的非易失性整型值,可能会看到其他线程所做的修改,因为它们是在主内存中写入的。但是,不能保证这些更改是从缓存传递到主内存中的。
扩展学习
只有当共享变量的值仅被一个线程修改时,volatile关键字才能正常工作。如果变量被多个线程修改,则volatile关键字无法在数据竞争条件下提供保护。 它也对操作如+或-、原子操作起作用,例如,作用在非易失性变量上的++操作符是线程不安全的。
自从Java 5,Java内存模型用volatile关键字建立了前置保证,包含两个特性:
- 当修改易失性变量时,变量值发送到主内存。也将发送之前由同一线程修改的所有变量值。
- 编译器不能重新排序那些为了优化目的而修改volatile变量的语句。它可以重新排序以前和之后的操作,但是不能修改volatile变量,这些修改之前发生的更改将对这些指令可见。
更多关注
- 本章“使用原子变量”和“使用原子数组”小节