在做多线程并发处理时,经常需要对资源进行可见性访问和互斥同步操作。有时候,我们可能从前辈那里得知我们需要对资源进行 volatile 或是synchronized 关键字修饰处理。可是,我们却不知道这两者之间的区别,我们无法分辨在什么时候应该使用哪一个关键字。本文就针对这个问题,展开讨论。
happens-before 模型简介如果你单从字面上的意思来理解 happens-before 模型,你可能会觉得这是在说某一个操作在另一个操作之前执行。不过,学习完 happens-before 之后,你就不会还这样理解了。以下是《Java 并发编程的艺术》书上对 happens-before 的定义:
volatile 的内存语义在 JMM(Java Memory Model) 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以在一个线程之内,也可以是在不同的线程之间。
对于多线程编程来说,每个线程是可以拥有共享内存中变量的一个拷贝,这一点在后面还是会讲到,这里就不作过多说明。如果一个变量被 volatile 关键字修饰时,那么对这的变量的写是将本地内存中的拷贝刷新到共享内存中;对这个变量的读会有一些不同,读的时候是无视他的本地内存的拷贝的,只是从共享变量中去读取数据。
synchronized 的内存语义我们说 synchronized 实际上是对变量进行加锁处理。那么不管是读也好,写也好都是基于对这个变量的加锁操作。如果一个变量被 synchronized 关键字修饰,那么对这的变量的写是将本地内存中的拷贝刷新到共享内存中;对这个变量的读就是将共享内存中的值刷新到本地内存,再从本地内存中读取数据。因为全过程中变量是加锁的,其他线程无法对这个变量进行读写操作。所以可以理解成对这个变量的任何操作具有原子性,即线程是安全的。
上面的一些说明或是定义可能会有一些乏味枯燥,也不太好理解。这里我们就列举一些例子来说明,这样比较具体和形象一些。
volatile 可见性测试RunThread.java
public class RunThread extends Thread{
private boolean isRunning =true;
public boolean isRunning {
return isRunning;
}
public void setRunFlag(booleanflag) {
isRunning = flag;
}
@Override public void run {
System.out.println("I'm come in...");
booleanfirst =true;
while(isRunning) {
if(first) {
System.out.println("I'm in while...");
first =false;
}
}
System.out.println("I'll go out.");
}
}
MyRun.java
publicclassMyRun{publicstaticvoidmain(String args)throwsInterruptedException { RunThread thread =newRunThread; thread.start; Thread.sleep(100); thread.setRunFlag(false); System.out.println("flag is reseted: "+ thread.isRunning); } }
对于上面的例子只是一个很普通的多线程操作,这里我们很容易就得到了 RunThread 线程在 while 中进入了死循环。
privatevolatilebooleanisRunning =true;
这样一来, volatile 修改了 isRunning 的可见性,使得主线程的
thread.setRunFlag(false)
将会 happens-before 子线程中的 while 。最终,使得子线程从 while 的循环中跳出,问题解决。
volatile 确实有很多优点,可是它却有一个致命的缺点,那就是 volatile 并不是原子操作。也就是在多线程的情况,仍然是不安全的。
public class DemoNoProtected{
staticclass MyThread extends Thread {staticintcount =0;privatestaticvoidaddCount{for(inti =0; i <100; i++) { count++; }
System.out.println("count = "+ count); }
@Override public void run { addCount; } }
public static void main(String args) {
MyThread threads =newMyThread[100];for(inti =0; i <100; i++) { threads[i] =newMyThread; }
for(int i =0; i <100; i++) { threads[i].start; } } }
count =300count =300count =300count =400......count =7618count =7518count =9918
这是一个未经任何处理的,很直白的过程。可是它的结果,也很直白。其实这个结果并不让人意外,从我们学习Java的时候,就知道Java的多线程并不安全。是不是从上面的学习中,你感觉这个可以通过 volatile 关键字解决?既然你这么说,那么我们就来试一试,给 count 变量添加 volatile 关键字,如下:
publicclassDemoVolatile{staticclass MyThread extends Thread {staticvolatileintcount =0; ... ... }publicstaticvoidmain(String args) { ... ... } }
count =100count =300count =400count =200......count =9852count =9752count =9652......count =8154count =8054
不知道这个结果是不是会让你感觉到意外。对于 count 的混乱的数字倒是好理解一些,应该多个线程同时修改时就发生这样的事情。可是我们在结果为根本找不到逻辑上的最大值“10000”,这就有一些奇怪了。因为从逻辑上来说, volatile修改了 count 的可见性,对于线程 A 来说,它是可见线程 B 对 count 的修改的。只是从结果中并没有体现这一点。
inttmp = count; tmp = tmp +1; count = tmp;
可见,count++ 并非原子操作。任何两个线程都有可能将上面的代码分离进行,安全性便无从谈起了。
synchronized 同步测试
上面说到 volatile 不能解决线程的安全性问题,这是因为 volatile 不能构建原子操作。而在多线程编程中有一个很方便的同步处理,就是 synchronized 关键字。下面来看看 synchronized 是如何处理多线程同步的吧,代码如下:
publicclassDemoSynchronized{staticclass MyThread extendsThread {staticintcount =0;privatesynchronizedstaticvoidaddCount {for(inti =0; i <100; i++) { count++; } System.out.println("count = "+ count); }@Overridepublicvoidrun { addCount; } }publicstaticvoidmain(String args) { MyThread threads =newMyThread[100];for(inti =0; i <100; i++) { threads[i] =newMyThread; }for(inti =0; i <100; i++) { threads[i].start; } } }
count =100count =200count =300......count =9800count =9900count =10000
通过 synchronized 我们可以很容易就获得了理想的结果。而关于 synchronized 关键字的内存模型可以这样来表示: