浅谈volatile变量的理解
2015-10-08 15:51
260 查看
原文 本文为菠萝大象原创,如要转载请注明出处。http://www.blogjava.net/bolo
在Java并发编程里面,volatile是个很重要的概念,大象也来讲讲自己对它的理解。
以前曾经有段时间我一直没搞明白volatile到底怎么用,它是怎样实现的同步,而且对于volatile变量还有一些限制条件。任何技术在没完全弄明白之前,至少在没熟练掌握之前都不太敢放心大胆的用,大象想将自己对它的理解分享出来,给需要的人一些帮助。
volatile是轻量级的锁,它只具备可见性,但没有原子特性。用volatile声明的变量,它的同步特性,简单来讲就是对该变量的单个读/写是同步的。这是什么意思呢?我还是以共享变量i为例,不过在i的前面加上了volatile修饰符。
private volatile int i =
0;
public int get()
{
return i;
}
public void set(int i)
{
this.i =
i;
}
为了与传统的getXXX和setXXX方法区别开来,我将方法名改成了上面这样。对于get和set方法,如果有多个线程同时访问,volatile是可以保证i的原子性的,再简单点讲,对于变量i,get和set方法是同步的。这是通过什么来保证的呢?是通过Java语言规范:如果一个字段被声明为volatile,Java内存模型确保所有线程看到这个变量的值是一致的。
看到这里,可能有的童鞋会想,既然volatile可以保证内存可见性,那不就解决了浅谈Java共享变量这篇文章里面讲到的共享变量的并发问题吗?只要在i的前面加上volatile就可以解决同步问题了,你确定?实践是最好的办法,动手试下,看看结果如何。
事实证明这个办法行不通,为什么呢?原因出在i++上面,增量操作符++不是原子的。这个操作分解开来看是先从堆内存中获得i值的副本放到缓存中,然后对副本值加1,最后再将副本值写回到堆内存的变量i中。从这个过程我们可以看到,从堆内存中获得i(get方法)以及将值写回到i(set方法)这两步都是同步的,但中间的就不能保证是同步的了。
对于volatile修饰的变量,只保证了他的可见性,但不保证原子性。最常用的应该是boolean类型,它用来作为状态标志,因为它只有true和false两个值,不会有非原子性的操作。当然不是说只能用在布尔类型变量上面,其它的基本类型和对象类型都可以用。但一定需要小心谨慎的处理,以免掉进并发陷阱而不知。比如volatile变量就不适合用于不变性条件这种情况,以上下限为例,lower必须小于upper,这就是一种不变性条件,你可以理解为这是一种规则限制。它们都只有set与get方法,但在set方法里面加入了约束条件,这时,volatile的可见性就不能保证并发时,lower与upper之间的不变性条件(lower<upper)一定成立了。
/*
* volatile只保证lower与upper的最后写入一定会被其它读取的线程看到
* 但不能保证在lower或upper写入时,另一个变量的值没有发生变化
*/
private volatile int lower;
private volatile int upper;
public int getLower()
{
return lower;
}
public int getUpper()
{
return upper;
}
public void setLower(int lower)
{
if (lower
> upper)
throw new IllegalArgumentException();
this.lower =
lower;
}
public void setUpper(int upper)
{
if (upper
< lower)
throw new IllegalArgumentException();
this.upper =
upper;
}
如果lower和upper的初始值为0和10,同一时刻,线程1调用setLower(8),线程2调用setUpper(2),执行上完全没问题,但是现在的lower和upper的值就变为了8和2这种无效的数据了,所以volatile只能确保可见性,不能确保原子性。
所以在使用volatile变量时,请考虑是否满足下面这样的要求:
1、对变量的写入操作不依赖变量的当前值(i++这种操作就不行)
2、没有用于其它变量的不变式条件中(lower<upper)
到这里,我们已经明白了:用volatile修饰的变量只具备可见性,那么它是怎么保证可见性的呢?
现在大家用的电脑CPU基本上都是多核的,至少两核,缓存也有很多级(L1、L2、L3)。代码在JVM里面执行的时候,JVM如果发现有CPU在处理volatile变量的写入操作,就会告诉该CPU将当前缓存中的数据写回到堆内存中,但这时其它CPU的缓存值还是旧的,再执行操作就会有问题,所以在处理器的内部实现了缓存一致性协议,当有缓存中的数据写回内存时会引起其它CPU里这个volatile变量的缓存值无效,如果这时候其它CPU要想使用就必须到堆内存中重新读取该值,这样就实现了volatile变量的可见性。我这样讲方便大家理解,实际的情况比这复杂的多。
在Java并发编程里面,volatile是个很重要的概念,大象也来讲讲自己对它的理解。
以前曾经有段时间我一直没搞明白volatile到底怎么用,它是怎样实现的同步,而且对于volatile变量还有一些限制条件。任何技术在没完全弄明白之前,至少在没熟练掌握之前都不太敢放心大胆的用,大象想将自己对它的理解分享出来,给需要的人一些帮助。
volatile是轻量级的锁,它只具备可见性,但没有原子特性。用volatile声明的变量,它的同步特性,简单来讲就是对该变量的单个读/写是同步的。这是什么意思呢?我还是以共享变量i为例,不过在i的前面加上了volatile修饰符。
private volatile int i =
0;
public int get()
{
return i;
}
public void set(int i)
{
this.i =
i;
}
为了与传统的getXXX和setXXX方法区别开来,我将方法名改成了上面这样。对于get和set方法,如果有多个线程同时访问,volatile是可以保证i的原子性的,再简单点讲,对于变量i,get和set方法是同步的。这是通过什么来保证的呢?是通过Java语言规范:如果一个字段被声明为volatile,Java内存模型确保所有线程看到这个变量的值是一致的。
看到这里,可能有的童鞋会想,既然volatile可以保证内存可见性,那不就解决了浅谈Java共享变量这篇文章里面讲到的共享变量的并发问题吗?只要在i的前面加上volatile就可以解决同步问题了,你确定?实践是最好的办法,动手试下,看看结果如何。
事实证明这个办法行不通,为什么呢?原因出在i++上面,增量操作符++不是原子的。这个操作分解开来看是先从堆内存中获得i值的副本放到缓存中,然后对副本值加1,最后再将副本值写回到堆内存的变量i中。从这个过程我们可以看到,从堆内存中获得i(get方法)以及将值写回到i(set方法)这两步都是同步的,但中间的就不能保证是同步的了。
对于volatile修饰的变量,只保证了他的可见性,但不保证原子性。最常用的应该是boolean类型,它用来作为状态标志,因为它只有true和false两个值,不会有非原子性的操作。当然不是说只能用在布尔类型变量上面,其它的基本类型和对象类型都可以用。但一定需要小心谨慎的处理,以免掉进并发陷阱而不知。比如volatile变量就不适合用于不变性条件这种情况,以上下限为例,lower必须小于upper,这就是一种不变性条件,你可以理解为这是一种规则限制。它们都只有set与get方法,但在set方法里面加入了约束条件,这时,volatile的可见性就不能保证并发时,lower与upper之间的不变性条件(lower<upper)一定成立了。
/*
* volatile只保证lower与upper的最后写入一定会被其它读取的线程看到
* 但不能保证在lower或upper写入时,另一个变量的值没有发生变化
*/
private volatile int lower;
private volatile int upper;
public int getLower()
{
return lower;
}
public int getUpper()
{
return upper;
}
public void setLower(int lower)
{
if (lower
> upper)
throw new IllegalArgumentException();
this.lower =
lower;
}
public void setUpper(int upper)
{
if (upper
< lower)
throw new IllegalArgumentException();
this.upper =
upper;
}
如果lower和upper的初始值为0和10,同一时刻,线程1调用setLower(8),线程2调用setUpper(2),执行上完全没问题,但是现在的lower和upper的值就变为了8和2这种无效的数据了,所以volatile只能确保可见性,不能确保原子性。
所以在使用volatile变量时,请考虑是否满足下面这样的要求:
1、对变量的写入操作不依赖变量的当前值(i++这种操作就不行)
2、没有用于其它变量的不变式条件中(lower<upper)
到这里,我们已经明白了:用volatile修饰的变量只具备可见性,那么它是怎么保证可见性的呢?
现在大家用的电脑CPU基本上都是多核的,至少两核,缓存也有很多级(L1、L2、L3)。代码在JVM里面执行的时候,JVM如果发现有CPU在处理volatile变量的写入操作,就会告诉该CPU将当前缓存中的数据写回到堆内存中,但这时其它CPU的缓存值还是旧的,再执行操作就会有问题,所以在处理器的内部实现了缓存一致性协议,当有缓存中的数据写回内存时会引起其它CPU里这个volatile变量的缓存值无效,如果这时候其它CPU要想使用就必须到堆内存中重新读取该值,这样就实现了volatile变量的可见性。我这样讲方便大家理解,实际的情况比这复杂的多。
相关文章推荐
- Python3写爬虫(四)多线程实现数据爬取
- C#实现多线程的同步方法实例分析
- 浅谈chuck-lua中的多线程
- C#简单多线程同步和优先权用法实例
- C#多线程学习之(四)使用线程池进行多线程的自动管理
- C#多线程编程中的锁系统(三)
- C#多线程学习之(六)互斥对象用法实例
- 基于一个应用程序多线程误用的分析详解
- C#多线程学习之(三)生产者和消费者用法分析
- C#多线程学习之(一)多线程的相关概念分析
- C#多线程之Thread中Thread.IsAlive属性用法分析
- C#控制台下测试多线程的方法
- Ruby 多线程的潜力和弱点分析
- 探究在C++程序并发时保护共享数据的问题
- C#中WPF使用多线程调用窗体组件的方法
- C#如何对多线程、多任务管理(demo)
- C#实现多线程的Web代理服务器实例
- c#实现多线程局域网聊天系统
- PHP使用CURL实现多线程抓取网页
- 浅解关于C#多线程的介绍