在多线程编程中,
我们经常会用到synchronized和Volatile,他们在其中都扮演着重要的角色.下面总结一下volatile的相关内容.
1.Volatile
简介:
1.1 Java
语言规范第三版中对volatile
的定义:java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的.
1.2 Volatile
是轻量级的synchronized
,轻量级体现在一下几个方面:
1.2.1 volatile
变量所需的编码较少
1.2.2
运行时开销也较少
1.2.3
不会引起线程上下文的切换和调度(
线程上下文即线程的运行环境)
1.3
它在多处理器开发中保证了共享变量的“
可见性”(可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值),这就是说线程能够自动发现 volatile 变量的最新值.
1.4 Volatile
变量具有 synchronized
的可见性特性,但是不具备原子特性.
1.5 volatile
变量不会像锁那样造成线程阻塞,
在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于锁的性能优势。
2.Volatile
的原理:
处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存后再进行操作,但操作完之后不知道何时会写到内存,如果对声明了Volatile
变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
3.Volatile
的使用必须满足的条件:
3.1
对变量的写操作不依赖于当前值
例如: volatile
变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性,该变量操作不能依赖其他值。
3.2
该变量没有包含在具有其他变量的不变式中
例如:
下面是一个程序清单:
@NotThreadSafe
public
class NumberRange {
private
int
lower, upper;
public
int
getLower() {
return
lower; }
public
int
getUpper() {
return
upper; }
public
void setLower(
int
value) {
if
(value > upper)
throw
new
IllegalArgumentException(…);
lower = value;
}
public
void setUpper(
int
value) {
if
(value < lower)
throw
new
IllegalArgumentException(…);
upper = value;
}
}
将 lower
和 upper 字段定义为volatile 类型不能够充分实现类的线程安全,如果凑巧两个线程在同一时间使用不一致的值执行 setLower 和 setUpper 的话,则会使范围处于不一致的状态.
4.Volatile
的使用:
4.1
状态标志
作为一个布尔状态标志.
这种类型的状态标记的一个公共特性是:通常只有一种状态转换.
volatile
boolean
shutdownRequested;
…
public
void shutdown() {
shutdownRequested =
true
;
}
public
void doWork() {
while
(!shutdownRequested) {
//
do
stuff
}
}
可能存在一个线程在调用 shutdown()
方法, 因此,需要执行某种同步来确保正确实现 shutdownRequested 变量的可见性。而如果使用 synchronized块编写循环要比使用 volatile 状态标志编写麻烦很多。由于 volatile 简化了编码,并且状态标志并不依赖于程序内任何其他状态,因此此处非常适合使用 volatile。
4.2
一次性安全发布
在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。
//
注意
volatile
!!!!!!!!!!!!!!!!!
private
volatile
static
Singleton instace;
public
static
Singleton getInstance(){
//
第一次
null
检查
if
(instance ==
null
){
synchronized
(Singleton.class) {
//1
//
第二次
null
检查
if
(instance ==
null
){
//2
instance =
new
Singleton();
//3
}
}
}
return
instance;
}
如果不用volatile
,则因为内存模型允许所谓的“无序写入”,可能导致失败。——某个线程可能会获得一个未完全初始化的实例。
设上述代码执行以下事件序列:
1.
线程 1
进入 getInstance() 方法。
2.
由于 instance
为 null,线程 1 在 //1 处进入synchronized块。
3.
线程 1
前进到 //3 处,但在构造函数执行之前,使实例成为非null。
4.
线程 1
被线程 2 预占。
5.
线程 2
检查实例是否为 null。因为实例不为 null,线程 2 将instance引用返回,返回一个构造完整但部分初始化了的Singleton 对象。
6.
线程 2
被线程 1 预占。
7.
线程 1
通过运行 Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。
4.3
独立观察
安全使用 volatile
的另一种简单模式是:定期 “发布” 观察结果供程序内部使用。
例如:
假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。
4.4“volatile bean”
模式
volatile bean
模式的基本原理是:很多框架为易变数据的持有者(例如 HttpSession
)提供了容器,但是放入这些容器中的对象必须是线程安全的。
在 volatile bean
模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通——即不包含约束!
4.5
开销较低的“
读-写锁”策略
如果读操作远远超过写操作,您可以结合使用内部锁和 volatile
变量来减少公共代码路径的开销。
如下显示的线程安全的计数器,使用synchronized
确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。
@ThreadSafe
public
class CheesyCounter {
//Employs the cheap read-write lock trick
//All mutative operations MUST be done with the ‘
this
‘lock held
@GuardedBy(
”
this
”
)
private
volatile
int
value;
//
读操作,没有
synchronized
,提高性能
public
int
getValue() {
return
value;
}
//
写操作,必须
synchronized
。因为
x++
不是原子操作
public
synchronized
int
increment() {
return
value++;
}
}
使用锁进行所有变化的操作,使用 volatile
进行只读操作。其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作