volatile——java中稍弱的同步机制
在讲volatile之前,大家先看这样一个例子
public class VolatileTest {
public int i = 0;
public void go() {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
if (i != 0) {
break;
}
}
System.out.println("线程结束");
}
}).start();
}
public static void main(String[] args) {
VolatileTest volatileTest = new VolatileTest();
volatileTest.go();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
volatileTest.i = 1;
}
}
程序的运行结果似乎应该是在运行一段时间后结束并打印出“线程结束”这句话,然而实际运行之后发现程序一直在运行,并且没有任何打印信息,而给定义的变量 i 添加上volatile修饰符之后,就能够得到想要的打印结果了。这一切都是由volatile造成的,我们今天就来讲一讲volatile的作用。
volatile总的来说一共有两个作用,一是保证一个线程对一个变量的修改对另外的线程是可见的,前提是对变量的修改不依赖变量原本的值,二是保证不会发生指令重排序。接下来我们分而析之。
-
确保变量值在线程之间的可见性;
在文章开篇举的例子里,就是由于另一线程改变了变量 i 的值,而go方法中的线程中变量 i 的值还是原来的值,导致线程一直在运行。
总结来说,线程对变量进行修改之后,要立刻回写到主内存中。而其他用到次变量的线程要从主内存中将变量的值再读取到自己的工作内存中。
讲到这里,就要提一下java的内存模型了,称作Java Memory Model ,简称JMM。以此来屏蔽硬件和操作系统对内存访问的差异性。JMM规定了所有的变量都存储在主存中。每条线程还有自己的工作内存,线程的工作内存中保存了被线程使用到的变量和主存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主存中的变量。不同线程之间也无法直接方位对方工作内存中的变量,线程变量值的传递需要通过主内存来完成。
由于线程在执行自己的工作,没时间去再从主内存中将变量值再更新一遍,导致执行结果于预想的出现偏差,而通过volatile可以当变量值改变时强制地让线程去主内存中将变量值更新一次。
但是volatile并不能保证多个线程共同修改同一个变量时所带来的不一致问题,也就是说volatile不能替代synchronized。由于i诸如++此类代码并不是原子性操作,所以导致了多个线程共同修改同一变量时会产生问题,而可以用AtomiInteger等类,这些类的操作是原子性操作。 -
保证不发生指令重排序;
在一个线程中,所有代码按照书写的顺序执行,而在多线程中,一个线程看另外一个线程,则编码是乱序的。通过volatile修饰可以确保在多线程中代码的顺序。这个作用等在以后我会在尽量详细写一下,在这里先不做说明。