JAVA 多线程之volatile的介绍
volatile的介绍
volatile的主要作用是:提示编译器该对象的值有可能在编译器未监测的情况下被改变。
volatile类似于大家所熟知的const也是一个类型修饰符。volatile是给编译器的指示来说明对它所修饰的对象不应该执行优化。volatile的作用就是用来进行多线程编程。在单线程中那就是只能起到限制编译器优化的作用。所以单线程的童鞋们就不用浪费精力看下面的了。
volatile让变量每次在使用的时候,都从主存中取。而不是从各个线程的“工作内存”。
volatile具有synchronized关键字的“可见性”,但是没有synchronized关键字的“并发正确性”,也就是说不保证线程执行的有序性。
也就是说,volatile变量对于每次使用,线程都能得到当前volatile变量的最新值。但是volatile变量并不保证并发的正确性。
=========================分割线1=================================
在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。
一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中。
没有volatile的结果
如果没有volatile,你将无法在多线程中并行使用到基本变量。下面举一个我开发项目的实例(这个实例采用的是C#语言但不妨碍我们讨论C++)。在学校的一个.Net项目的开发中,我曾经在多线程监控中用到过一个基本变量Int32型的,我用它来控制多线程中监控的一个条件。考虑到基本变量是编译器自带的而且无法用lock锁上,我想当然的以为是原子操作不会有多线程的问题,可实际运行后发现程序的运行有时正常有时异常,改为用Dictionary对象处理并加锁以后才彻底正常。现在想来应该是多线程同时操作该变量了,具体的将在下面说清。
volatile的作用
如果一个基本变量被volatile修饰,编译器将不会把它保存到寄存器中,而是每一次都去访问内存中实际保存该变量的位置上。这一点就避免了没有volatile修饰的变量在多线程的读写中所产生的由于编译器优化所导致的灾难性问题。所以多线程中必须要共享的基本变量一定要加上volatile修饰符。当然了,volatile还能让你在编译时期捕捉到非线程安全的代码。我在下面还会介绍一位大牛使用智能指针来顺序化共享区代码的方法,在此对其表示感谢。
泛型编程中曾经说过编写异常安全的代码是很困难的,可是相比起多线程编程的困难来说这就太小儿科了。多线程编程中你需要证明它正确,需要去反复地枯燥地调试并修复,当然了,资源竞争也是必须注意的,最可恨的是,有时候编译器也会给你点颜色看看。。。
class Student { public: void Wait() //在北航排队等吃饭实在是很痛苦的事情。。。 { while (!flag) { Sleep(1000); // sleeps for 1000 milliseconds } } void eat() { flag = true; } ... private: bool flag; };
好吧,多线程中你就等着吃饭吧,可在这个地方估计你是永远等不到了,因为flag被编译器放到寄存器中去了,哪怕在你前面的那位童鞋告诉你flag=true了,可你就好像瞎了眼看不到这些了。这么诡异的情况的发生时因为你所用到的判断值是之前保存到寄存器中的,这样原来的地址上的flag值更改了你也没有获取。该怎么办呢?对了,改成volatile就解决了。
volatile对基本类型和对用户自定义类型的使用与const有区别,比如你可以把基本类型的non-volatile赋值给volatile,但不能把用户自定义类型的non-volatile赋值给volatile,而const都是可以的。还有一个区别就是编译器自动合成的复制控制不适用于volatile对象,因为合成的复制控制成员接收const形参,而这些形参又是对类类型的const引用,但是不能将volatile对象传递给普通引用或const引用。
如何在多线程中使用好volatile
在多线程中,我们可以利用锁的机制来保护好资源临界区。在临界区的外面操作共享变量则需要volatile,在临界区的里面则non-volatile了。我们需要一个工具类LockingPtr来保存mutex的采集和volatile的利用const_cast的转换(通过const_cast来进行volatile的转换)。
首先我们声明一个LockingPtr中要用到的Mutex类的框架:
class Mutex { public: void Acquire(); void Release(); ... };
接着声明最重要的LockingPtr模板类:
template <typename T> class LockingPtr { public: // Constructors/destructors LockingPtr(volatile T& obj, Mutex& mtx) : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx) { mtx.Lock(); } ~LockingPtr() { pMtx_->Unlock(); } // Pointer behavior T& operator*() { return *pObj_; } T* operator->() { return pObj_; } private: T* pObj_; Mutex* pMtx_; LockingPtr(const LockingPtr&); LockingPtr& operator=(const LockingPtr&); };
尽管这个类看起来简单,但是它在编写争取的多线程程序中非常的有用。你可以通过对它的使用来使得对多线程中共享的对象的操作就好像对volatile修饰的基本变量一样简单而且从不会使用到const_cast。下面来给一个例子:
假设有两个线程共享一个vector<char>对象:
class SyncBuf { public: void Thread1(); void Thread2(); private: typedef vector<char> BufT; volatile BufT buffer_; Mutex mtx_; // controls access to buffer_ };
在函数Thread1中,你通过lockingPtr<BufT>来控制访问buffer_成员变量:
void SyncBuf::Thread1() { LockingPtr<BufT> lpBuf(buffer_, mtx_); BufT::iterator i = lpBuf->begin(); for (; i != lpBuf->end(); ++i) { ... use *i ... } }
这个代码很容易编写和理解。只要你需要用到buffer_你必须创建一个lockingPtr<BufT>指针来指向它,并且一旦你这么做了,你就获得了容器vector的整个接口。而且你一旦犯错,编译器就会指出来:
void SyncBuf::Thread2() { // Error! Cannot access 'begin' for a volatile object BufT::iterator i = buffer_.begin(); // Error! Cannot access 'end' for a volatile object for (; i != lpBuf->end(); ++i) { ... use *i ... } }
这样的话你就只有通过const_cast或LockingPtr来访问成员函数和变量了。这两个方法的不同之处在于后者提供了顺序的方法来实现而前者是通过转换为volatile来实现。LockingPtr是相当好理解的,如果你需要调用一个函数,你就创建一个未命名的暂时的LockingPtr对象并直接使用:
unsigned int SyncBuf::Size() { return LockingPtr<BufT>(buffer_, mtx_)->size(); }
LockingPtr在基本类型中的使用
在上面我们分别介绍了使用volatile来保护对象的意外访问和使用LockingPtr来提供简单高效的多线程代码。现在来讨论比较常见的多线程处理共享基本类型的一种情况:
class Counter { public: ... void Increment() { ++ctr_; } void Decrement() { —-ctr_; } private: int ctr_; };
这个时候可能大家都能看出来问题所在了。1.ctr_需要是volatile型。2.即便是++ctr_或--ctr_,这在处理中仍是需要三个原子操作的(Read-Modify-Write)。基于上述两点,这个类在多线程中会有问题。现在我们就来利用LockingPtr来解决:
class Counter { public: ... void Increment() { ++*LockingPtr<int>(ctr_, mtx_); } void Decrement() { —?*LockingPtr<int>(ctr_, mtx_); } private: volatile int ctr_; Mutex mtx_; };
volatile成员函数
关于类的话,首先如果类是volatile则里面的成员都是volatile的。其次要将成员函数声明为volatile则同const一样在函数最后声明即可。当你设计一个类的时候,你声明的那些volatile成员函数是线程安全的,所以那些随时可能被调用的函数应该声明为volatile。考虑到volatile等于线程安全代码和非临界区;non-volatile等于单线程场景和在临界区之中。我们可以利用这个做一个函数的volatile的重载来在线程安全和速度优先中做一个取舍。具体的实现此处就略去了。
在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,只需要像在本程序中的这样,把该变量声明为volatile(不稳定的)即可,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般说来,多任务环境下各任务间共享的标志都应该加volatile修饰。
Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
用volatile和不用volatile的区别,运行一下,就知道了。
不用volatile:
- package com.keyword;
- public class TestWithoutVolatile {
- private static boolean bChanged;
- public static void main(String[] args) throws InterruptedException {
- new Thread() {
- @Override
- public void run() {
- for (;;) {
- if (bChanged == !bChanged) {
- System.out.println("!=");
- System.exit(0);
- }
- }
- }
- }.start();
- Thread.sleep(1);
- new Thread() {
- @Override
- public void run() {
- for (;;) {
- bChanged = !bChanged;
- }
- }
- }.start();
- }
- }
运行后,程序进入死循环了,一直在运行。
用volatile:
package com.keyword;
- public class TestWithVolatile {
- private static volatile boolean bChanged;
- public static void main(String[] args) throws InterruptedException {
- new Thread() {
- @Override
- public void run() {
- for (;;) {
- if (bChanged == !bChanged) {
- System.out.println("!=");
- System.exit(0);
- }
- }
- }
- }.start();
- Thread.sleep(1);
- new Thread() {
- @Override
- public void run() {
- for (;;) {
- bChanged = !bChanged;
- }
- }
- }.start();
- }
- }
程序输出!=,然后马上退出。
但是,很多情况下,用不用volatile,感觉不出什么区别,什么时候要用volatile呢?看看JDK里使用volatile的类。
比如java.util.regex.Pattern里的变量:
- private transient volatile boolean compiled = false;
还有,java.lang.System的变量:
- private static volatile Console cons = null;
一般就是初始化的时候,需要用到volatile。
java.util.Scanner里的变量,如:
- private static volatile Pattern boolPattern;
- private static volatile Pattern separatorPattern;
- private static volatile Pattern linePattern;
初始化boolPattern的代码:
- private static Pattern boolPattern() {
- Pattern bp = boolPattern;
- if (bp == null)
- boolPattern = bp = Pattern.compile(BOOLEAN_PATTERN,
- Pattern.CASE_INSENSITIVE);
- return bp;
- }
上面的情况,可以使用synchronized来对boolPattern加锁,但是synchronized开销比volatile大,volatile能够胜任上面的工作。
volatile不保证原子操作,所以,很容易读到脏数据。
使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。
总结
在编写多线程程序中使用volatile的关键四点:
1.将所有的共享对象声明为volatile;
2.不要将volatile直接作用于基本类型;
3.当定义了共享类的时候,用volatile成员函数来保证线程安全;
4.多多理解和使用volatile和LockingPtr!(强烈建议)
转自:http://blog.csdn.net/jingxuewang110/article/details/6759044
JAVA 多线程之volatile的介绍的更多相关文章
- JAVA多线程之volatile 与 synchronized 的比较
一,volatile关键字的可见性 要想理解volatile关键字,得先了解下JAVA的内存模型,Java内存模型的抽象示意图如下: 从图中可以看出: ①每个线程都有一个自己的本地内存空间--线程栈空 ...
- Java多线程之volatile详解
本文目录 从多线程交替打印A和B开始 Java 内存模型中的可见性.原子性和有序性 Volatile原理 volatile的特性 volatile happens-before规则 volatile ...
- Java多线程之volatile与synchronized比较
可见性: JAVA内存模型: java为了加快程序的运行效率,对一些变量的操作是在寄存器或者CPU缓存上进行的,后面再同步到主存中 看上图,线程在运行的过程中,会从主内存里面去去变量,读到自己的空间内 ...
- Java多线程之volatile关键字《一》
关键字volatile的主要作用是使变量在多个线程间可见. 1.关键字volatile与死循环 如果不是在多继承的情况下,使用继承Thread类和实现Runnable接口在取得程序运行的结果上并没有什 ...
- java多线程之volatile关键字
public class ThreadVolatile extends Thread { public boolean flag=true; @Override public void run() { ...
- Java多线程之ConcurrentSkipListMap深入分析(转)
Java多线程之ConcurrentSkipListMap深入分析 一.前言 concurrentHashMap与ConcurrentSkipListMap性能测试 在4线程1.6万数据的条件下, ...
- JAVA多线程之wait/notify
本文主要学习JAVA多线程中的 wait()方法 与 notify()/notifyAll()方法的用法. ①wait() 与 notify/notifyAll 方法必须在同步代码块中使用 ②wait ...
- Java多线程之Runnable与Thread
Java多线程之Thread与Runnable 一.Thread VS Runnable 在java中可有两种方式实现多线程,一种是继承Thread类,一种是实现Runnable接口:Thread类和 ...
- JAVA多线程之UncaughtExceptionHandler——处理非正常的线程中止
JAVA多线程之UncaughtExceptionHandler——处理非正常的线程中止 背景 当单线程的程序发生一个未捕获的异常时我们可以采用try....catch进行异常的捕获,但是在多线程环境 ...
随机推荐
- 【转】WPF Template模版之DataTemplate与ControlTemplate(一)
WPF系统不但支持传统的Winfrom编程的用户界面和用户体验设计,更支持使用专门的设计工具Blend进行专业设计,同时还推出了以模板为核心的新一代设计理念. 1. 模板的内涵 作为表现形式,每个控件 ...
- R options scipen 控制科学计数法的显示
当数字过长,R语言会自动采用科学计数法显示,测试如下 > a <- > a [] > a <- > a <- > a [] > a <- & ...
- 蜕变成蝶~Linux设备驱动中的阻塞和非阻塞I/O
今天意外收到一个消息,真是惊呆我了,博客轩给我发了信息,说是俺的博客文章有特色可以出本书,,这简直让我受宠若惊,俺只是个大三的技术宅,写的博客也是自己所学的一些见解和在网上看到我一些博文以及帖子里综合 ...
- VS2013-2017 舒服的字体设置和背景色
使用字体:Fixedsys Excelsior 3.01 1.如果没有安装字体的话,首先下载字体:http://www.fixedsysexcelsior.com/ 2.安装字体:控制面板 -> ...
- POI导出Excel发现不可读取的内容
环境说明:MyEclipse Tomcat7.0 通过后台查询数据,导出Excel在打开时会出现以下提示: 点击否,则不显示任何内容,点击是,弹出 查看修改记录为: 通过WPS打开不会出现任何提示,可 ...
- VS Code打开使用IDEA搭建的Spring Boot项目运行提示"snakeyaml was not found on the classpath"错误
今天用VS Code打开之前基于IDEA搭建并开发的Spring Boot项目,启动调试后出现如下错误: 17:43:05.214 [restartedMain] ERROR org.springfr ...
- Mesos和Docker的集成
摘要: 众所周知,Mesos全面支持Docker.但是这意味着什么呢?在命令行里运行docker run...就可以使用Docker了.还需要做什么?让我们一起研究下Mesos的高级特性——和Dock ...
- mock---前端搭建模拟服务
在做前端开发接口的时候,往往接口都是从后端来的,这让前端很苦恼,但是有了 MockServer ,前端也可以搭建API服务了. server-mock是什么? 是一款nodejs的应用,用于搭建web ...
- CString数组和CStringArray
CStringArray是编译器定义的类型!可以进行一些(如:访问.增.删.改)等操作. 集中单个字符串的操作使用Cstring,集中一批字符串的管理使用CstringArray. 一个是动态,CSt ...
- 2018-2019-2 20175320实验一《Java开发环境的熟悉》实验报告
2018-2019-2 20175320实验一<Java开发环境的熟悉>实验报告 一.实验步骤及内容 (一)带包程序的编译运行 1.使用mkdir命令创建如图所示目录 2.进入exp1下的 ...