volatile是jvm提供的轻量级的同步机制,它具有三大特性分别是:保证可见性、禁止指令重排序、不保证原子性。说到这三大特性还得提到JMM(java内存模型)。
每个线程创建时,jvm都会为其分配一个工作内存,这个工作内存是每个线程私有空间,然而我们定义的所有变量都是存在主内存中,主内存是共享的内存区域,创建的线程都可以访问,但是线程对于变量的操作,必须先将变量从主内存拷贝一份到自己的工作内存,然后才能对其进行操作,操作完毕再将变量写会主内存。各个线程之间不能直接访问对方的工作内存,必须通过主内存间接访问。以下是线程访问主内存的过程:
有两个线程A和B,分别从主内存拷贝了一个变量到自己的工作内存,当线程A对这个变量修改了之后再将此变量写回到主内存,而此时线程B并不知道主内存中的值发生了改变,但是线程B仍然对它之前拷贝的变量旧值操作,也要写回主内存,这时就会发生了写覆盖的线程安全问题,这时就需要一个可见性机制,volatile可以解决这个问题,原理为:当A线程对变量做操作了之后立刻刷新到主内存,并且强制缓存了该值的线程从主内存中重新读取最新的值,以下用代码演示:
public class VolatileTest {
private static int a=0;
public static void main(String[] args) {
new Thread(()->{
System.out.println("线程"+Thread.currentThread().getName()+"开始执行");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
a=1;
System.out.println("线程"+Thread.currentThread().getName()+"修改了值,a="+a);
},"A").start();
while (a==0){
}
System.out.println("线程"+Thread.currentThread().getName()+"结束");
}
}
执行结果:
说明:线程A修改了a的值为1,但是主线程不可见,主线程的工作内存中a的值仍为0,所以一直在while循环中,不会执行下面的代码。
public class VolatileTest {
private static volatile int a=0;
public static void main(String[] args) {
new Thread(()->{
System.out.println("线程"+Thread.currentThread().getName()+"开始执行");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
a=1;
System.out.println("线程"+Thread.currentThread().getName()+"修改了值,a="+a);
},"A").start();
while (a==0){
}
System.out.println("线程"+Thread.currentThread().getName()+"结束");
}
}
运行结果:
说明:此时加上了volatile关键字,线程A修改了a的值,主线程立马可见,a的值为1不走循环体,执行后面的代码主线程结束。
计算机在执行程序的时候,为了提高性能,编译器和处理器会对指令重排序,在多线程环境下可能会出现乱序执行的情况。以单例模式的DCL为例:
class SingleTonTest{
private static SingleTonTest instance=null;
private SingleTonTest(){}
public static SingleTonTest getInstance(){
if (instance == null){
synchronized (SingleTonTest.class){
if (instance == null){
instance=new SingleTonTest();
}
}
}
return instance;
}
}
在对象的创建过程中,其实经历了三个步骤:
1.分配对象内存空间;
2.初始化对象;
3.设置对象指向分配的内存地址,此时instance!=null。
在多线程环境下,由于指令重排,第2和第3可能会调换位置,当刚好执行完第3步时,instance!=null,cpu切换到另一个线程执行,发现instance!=null,于是直接返回,但是此时对象还未初始化,就会发生线程安全问题。
解决这个问题只需要在下面代码中加上volatile即可。
private static volatile SingleTonTest instance=null;
原子性即一个操作不可分割,是一个完整的操作。
代码演示如下:
public class VolatileTest {
private static volatile int a=0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 1000; i1++) {
a++;
}
}).start();
}
//等待操作a的所有线程执行完毕后,再查看a的值
TimeUnit.SECONDS.sleep(1);
System.out.println("a的值为\t"+a);
}
}
执行结果为:
说明:10个线程对a加1,1000次,预期结果应该是10000,但是结果可能小于10000,这是由于volatile不能保证原子性导致的,导致原子性问题是由于a++操作是分步执行的。
下面请看a++的具体实现:
public class Test {
volatile int a=0;
public void add(){
a++;
}
}
底层实现在汇编语言中是这样的
public class com.cmc.springboot.java.Test {
volatile int a;
public com.cmc.springboot.java.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field a:I
9: return
public void add();
Code:
0: aload_0
1: dup
2: getfield #2 // Field a:I
5: iconst_1
6: iadd
7: putfield #2 // Field a:I
10: return
}
我们看add方法,在2这个步骤getfield,是获取变量a的值值0,第6步iadd执行+1操作,第7步putfield,把累加后的值写回,由此可见a++是分3步执行的,如果在putfield执行之前,此线程被阻塞,cpu切换到另外一个线程完成+1,并写回到了主内存中a为1,再回到之前的线程继续执行putfield,把1再写回到主内存,最终结果变成了a的值为1,导致了原子性的问题。
原子问题的解决:
public class VolatileTest {
private static volatile int a=0;
private static AtomicInteger atomicInteger=new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 1000; i1++) {
a++;
atomicInteger.getAndIncrement();
}
}).start();
}
//等待操作a的所有线程执行完毕后,再查看a的值
TimeUnit.SECONDS.sleep(1);
System.out.println("a的值为\t"+a);
System.out.println("atomicInteger的值为:\t"+atomicInteger);
}
}
执行结果:
说明:利用AtomicInteger来解决原子性问题,AtomicInteger是利用CAS算法来实现的,可保证原子性操作,CAS通道
以上是本人学习过程中对volatile的一些理解,如有不对之处请指出。