volatile与JMM
synchronized是一种重量级同步机制,volatile是一种轻量级、弱弱同步机制。在进一步描述volatitle之前我们先了解一下JMM(Java Memory Model).
Java线程之间的通信由JMM控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见,定义了线程和主内存之间的抽象关系。共享的变量存储在主内存上,而每一个线程处理器都有自己的Cache(多级),为了提供线程处理效率,共享变量一般都会在自己的cache中保留一份副本,然后按照一定策略和主内存同步。Java内存模型的抽象示意图如下:
一般我们申明变量方式,如下代码片段:
private boolean flag = true;
public setFlag(boolean flag){
this.flag = flag;
}
public void execute(){
while(flag){//等待flag变成false退出
dosomething();//
}
}
如果线程A执行execute while循环里内容,等待其他线程修改flag为false退出循环。此时假设线程B执行了setFlag(false),试问,线程A是否会退出while循环?答案是:不一定。这和我们期望有出入,怎么回事?看了上面的JMM简单描述,我想你可能会明白了。
那如果把setFlag/execute申明为synchronized方法,结果又是什么?结果线程A会在处理完dosomething之后退出循环。但synchronized是互斥锁,这样在同时只能有一个线程执行execute方法,这势必会降低系统的吞吐量,还有更好的方法吗?如果仅考虑变量线程的可见性,那就是采用volatile,把flag申明为:
private volatile boolean flag = true;
volatile确保对申明的变量修改会预知地传递给其他线程。当一个变量被申明为volatile时,编译器和运行时会知道这个变量被共享,所以这个变量不应该在寄存器或工作内存中缓存,而应该直接存储在主内存中,这样一个线程对它修改后,其他线程读取这个变量时会得到最新的值。这个特性也被成为volatile的线程可见性(visibility),也是volatile最主要特性。
除了volatile变量的线程可见性外,还有
1)禁止重排序
就是对它的内存操作不允许和其他内存操作进行重排序。而JVM(不仅仅是JVM)对执行执行的重排序主要是为了利用物理机的特性提高执行效率,只要最后输出的结果是对的。
2)可见性扩展
volitile变量可见性的影响已经超出了自己内容的可见性。所有在修改volitile变量之前修改其他共享变量,在volitile变量对其他线程可见时,其他共享变量也是可见的。这是依据现行发生原则(happens-before)第(3)条:对volatile字段的写入操作happens-before于每一个后续的同一个字段的读操作。
将设有一个共享变量b是volitile,而另一个a不是volitile,这是一个线程执行一个操作C在字段b的写操作之后,那么volatile b写操作之前的所有操作都对此操作C可见。所以修改a总是在修改b之前,也就是说如果其他线程读取到了一个b的值,那么在b变化之前的a也就能够读取到,换句话说就是如果看到了b值的变化,那么就一定看到了a值的变化。而如果上面两条语句交换下顺序就无法保证这个结果一定存在了。在ConcurrentHashMap(jdk1.6.0.45).containsValue中,可以看到每次遍历segments时都会执行int c = segments[i].count;,但是接下来的语句中又不用此变量c,尽管如此JVM仍然不能将此语句优化掉,因为这是一个volatile字段的读取操作,它保证了一些列操作的happens-before顺序,所以是至关重要的。在这里可以看到:ConcurrentHashMap将volatile发挥到了极致!
volatitle与synchronized、atomic变量
volatile是Java语言的高级特性,能用他来做什么,不能用它来做什么一定要清楚。现在网上有文章说不要轻易使用volatile,还说如果对于多线程掌握不够深入的话,尽可能用synchronized。
volatile能完成的事情,synchronized也能完成,但反过来不成立。被synchronized的代码块内的共享变量具有原子性,而volatile修饰的变量没有,如下代码所示:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileTest {
private volatile int vi = 0;
public int getVi() {
try {
Thread.sleep(1); //模拟耗时操作
} catch (InterruptedException ex) {
Logger.getLogger(Volatile.class.getName()).log(Level.SEVERE, null, ex);
}
return vi;
}
public void incrVi(){
vi++;
}
public void execute() throws InterruptedException {
ExecutorService es = Executors.newCachedThreadPool();
for (int j = 0; j < 10; j++) {
es.execute(new Runnable() {
public void run() {
int j = 0;
while (j++ < 5000) {
incrVi();
}
}
});
}
es.shutdown();
es.awaitTermination(1, TimeUnit.HOURS);
}
public static void main(String[] args) throws InterruptedException {
VolatileTest app = new VolatileTest ();
app.execute();
System.out.println("最后结果:" + app.getVi());
}
}
请问上面的输出结果是50000?输出不固定,大多数情况小于50000。虽然volatile可以让其修饰的变量值对其他线程马上可见,但多个线程同时读取了最新值,比如10个线程同时读到300,最后都修改为301.volatile是轻量级同步机制,不能排他访问共享变量。
如果把incrVi方法用synchronized修饰,结果正确了。
但是这样的话,incrVi同时只能被一个线程访问了,所有的并行操作到了incrVi都变成了串行。如果incrVi是一个耗时操作的话,整个性能会严重受损。
多线程会产生资源争用,但不是所有时刻都会产生,如果是100%都会产生,synchronized也许是个最佳选择,但实际情况真正在CPU执行发生争用的概率很低。既然这样,我们不要完全采用synchronized机制,让所有线程并发运行起来,只有真正发生争用时在额外处理一下即可,这就是CAS的粗话描述。不恰当量化分析一下,执行synchronized方法花费时为1ms,50000执行是50000ms,50s,而如果换成CAS操作,一次执行只有0.01ms,50000执行争用发生10次,每次处理2ms,这样最后执行时间为500+20=520ms,0.52s。
JUC(java并发套件)中的并发套件主要就是采用CAS了实现的。上面程序的改进如下:
public class CASTester{
private final AtomicInteger atomicI = new AtomicInteger(0);
public void incrVi(){
try {
Thread.sleep(1);//模拟耗时操作
} catch (InterruptedException ex) {
Logger.getLogger(Volatile.class.getName()).log(Level.SEVERE, null, ex);
}
atomicI.incrementAndGet();
}
public int getAtomici() {
return atomicI.get();
}
public void execute() throws InterruptedException {
ExecutorService es = Executors.newCachedThreadPool();
for (int j = 0; j < 10; j++) {
es.execute(new Runnable() {
public void run() {
int j = 0;
while (j++ < 5000) {
incrVi();
}
}
});
}
es.shutdown();
es.awaitTermination(1, TimeUnit.HOURS);
}
public static void main(String[] args) throws InterruptedException {
CASTester app = new CASTester();
app.execute();
System.out.println("最后结果:" + app.getAtomici());//50000
}
}
不用synchronized修饰,输出结果一样正确,但执行效率可是比synchronized高出很多。简单测试synchronized花费了50s,而CAS操作仅7s。
volatitle与不变模式
不变对象是线程安全的,不用增加额外的机制可以安全地被多线程并发访问。那这又和volatile有半毛关系?本来没有,但是两者结合起来有很多妙处。
volatile变量是不和其他状态变量一起纳入不变条件中,但是借助不变模式则可以变向实现,请看下面的例子:
本例子的逻辑就是,一个提供计算耗时(通过sleep模仿)服务,为了局部提高性能需要保留最近一次的计算原值和计算结果,如果当前需要计算的值和保留的计算值相同则直接返回结果,而不用耗时计算。此外要求本服务线程安全。
/**
* 示例服务接口
*/
interface Service {
/**
* @param 计算输入
* @return
*/
int service(int i);
}
/**
* 示例服务抽象类
*/
abstract class ServiceTemplate implements Service {
/**
* 示例计算方法,模仿耗时操作
*
* @param i
* @return
*/
protected Integer compute(Integer i) {
try {
Thread.sleep(1);
} catch (InterruptedException ex) {
Logger.getLogger(App.class.getName()).log(Level.SEVERE, null, ex);
}
return ((Double) Math.sqrt(i)).intValue();
}
}
实现一:粗粒度synchronized
/**
* 性能不高的线程安全缓存服务
*/
class SynchronizedCacheService extends ServiceTemplate {
private Integer lastNumber = new Integer(-1);
private Integer lastResult = new Integer(-1);
public synchronized int service(int req) {
if (req == lastNumber) {
return lastResult;
} else {
int result = compute(req);
lastNumber = req;
lastResult = result;
return result;
}
}
}
实现二:细粒度synchronized
public class FineSynchronizedCacheService extends ServiceTemplate {
private Integer lastNumber = new Integer(-1);
private Integer lastResult = new Integer(-1);
public int service(int req) {
Integer i = req;
synchronized (this) {
if (i.equals(lastNumber)) {
return lastResult;
}
}
Integer result = compute(i);
synchronized (this) {
lastNumber = i;
lastResult = result;
}
return result;
}
}
虽然实现方式二相比一只是一个小小的改动,然而却是service方法尽可能同时被多个线程访问,提高了吞吐量。但不论如何细粒度,只要是被synchronized修饰的代码块,某一个时刻只能被一个线程访问执行。
实现三:不变模式和volatile
class OneValueCache {
private final Integer lastNumber;
private final Integer lastResult;
public OneValueCache(Integer i, Integer result) {
lastNumber = i;
if (factors != null) {
lastResult = result .intValue();
} else {
lastResult = null;
}
}
public Integer getResult(Integer i) {
if (lastNumber == null || !lastNumber.equals(i)) {
return null;
} else {
return lastResult;
}
}
}
public class VolatileCachedService extends ServiceTemplate {
private volatile OneValueCache cache = new OneValueCache(null, null);
public int service(int req) {
Integer i = req;
Integer result= cache.getResult (i);
if (factors == null) {
factors = compute(i);
cache = new OneValueCache(i, factors);
}
return result;
}
}
如果在VolatileCachedService中分别用两个volatile变量保存原值和结果值,那么因为volatile变量不能其他状态变量参与不变约束,即两个volatile变量不能共同保持原子型,必然有线程会读取到不一致数据。
通过不变OneValueCache对象内部用了两个final数据,封装了两个volatile变量,这样在初始化时,两个数据必然被同时初始化,并且一经init不允许update。同时因为cache被修饰为volatile,保证新的值可以马上被其他线程获取。
总结-volatile使用约束
思维比较随便,零零散散记录了volatile的一些内容,现在总结一下volatile使用的约束:
1、更新操作不能依赖于当前值,如Volatile例子所反映。如果需要依赖的话,必须每一次更新只有一个线程在进行。(注,这个在Java并发编程实践中文版翻译中存在错误)
2、volatile变量不能和其他状态变量一同参与到不变约束中,但是和不变模式的结合应用
3、对volatile变量的操作是不需要的锁操作
4、不要轻易使用volatile,但是使用好了会有非常的结果,JUC中很多实现就是例证