volatile可以看成是轻量级的低配版的Synchronized,他主要是作用于共享变量,保证共享变量的可见性。确保共享变量在主内存中一致地准确的更新通知到各个线程,这是Volatile的可见性,同时由于它是低配版的Synchronized,所以他也没有了Synchronized的一些功能,比如原子性。

Java内存模型

在理解有关Java并发编程的时候,我们是非常有必要的先了解一下Java内存模型的。正如图上所示,Java内存模型规定了所有的变量都存储在主内存中,线程之间的工作并不是直接去读取操作主内存的,而是每个工作线程首先会在主内存中拷贝这些共享变量下来线程对变量的操作都是自己的工作线程中完成的,对于不同线程之间是具有不具备可见性的(各做各的),线程间变量值的传递均需要通过主内存来完成。Volatile正是为了解决以上的问题而存在的。

工作线程操作变量与主内存的交互

• read:从主内存中读取变量
• load:复制变量到工作线程本地内存作为副本
• use:运算副本
• assign:给副本赋值
• store:副本写入工作线程的内存中
• write:线程内存中的共享副本刷新主内存中的共享变量

可见性

可见性是指线程之间变量的可见性,及时得到变量状态变化的通知,一个线程修改了变量的状态另一个就及时的知道变量的最新状态。举个例子:A、B线程在主内存中拷贝同一个Volatile修饰变量,A线程把这个变量的状态由false改为了true,紧跟着B线程就会收到通知他刚刚拷贝的变量已经过期失效,B线程就会更新这个变量,得到最新的状态true,而不再是过期失效的状态。

Volatile修饰的变量不允许线程内部缓存和重排序,也就是说直接操作主内存,这样就对其他线程可见了。但是,但是,但是,Volatile修饰的变量只能保证变量的可见性,而不能保证变量的原子性,这样就导致一些非原子性的操作仍然存在线程安全的问题。

普通的共享变量在进行操作之后,写入主内存的时机是不确定的,该线程可能在操作完变量并且还没写入主内存的时候就去干别的事情了,这样就导致在其他线程获取这个变量的时候并不是最新的值,无法保证可见性,真是这样的线程安全问题也就导致了程序运行结果并不是我们所期望的结果。

Volatile并不能保证原子性,而他的高配版——Synchronized完全应付了这些问题。Synchronized既能保证可见性又能保证原子性。Synchronized在工作时只能有一个线程获取锁执行同步代码,并在释放锁的时候把变量写入主内存中。

public class MyThread extends Thread {
    public boolean exit = false; 
        public void run() { 
        while (!exit){
            //do something
        }
    } 
}

上面的代码使用退出标志终止线程关闭的代码。看上去似乎是没有问题,只要其他线程吧exit复制为true就能够终止线程。但是这样写仍然会存在风险,有可能不是我们所期望的效果。上面说过,工作线程会各自在主线程中拷贝变量,然后自顾自的工作。以实例来说,A、B线程会在主线程中各自拷贝exit变量到自己的工作内存中,当B需要终止A线程的时候,B便会修改自己工作内存中的exit变量,但是由于不确定性B在修改本地exit变量的时候可能还没把修改后的变量exit写入主内存中就去了干别的事情了,导致A线程没有终止。

给exit变量增加Volatile修饰后,B线程把本地变量exit赋值为true的时候,Volatile会强制把最新值写到主内存中并且会通知A线程告知其本地exit变量已过期失效,立即到主内存中更新exit变量,这样子便会使A线程的exit变量及时更新。也体现了Volatile在线程之间的可见性。

原子性

从Volatile可见性的问题中我们带出了原子性这一名词。原子性是指:一个操作或者多个操作(可以把它看成事务)要么全执行而且不会被中断,要么全不执行。在Java中,对基本数据类型的变量的读取和赋值操作是就原子性操作。原子就是不能再细分的意思。

举个例子:int a = 8; 把8赋值给a这个操作已经不能再细分或分割了,那么类似于这种操作就称之为原子操作。

再举个例子:i++; 这个操作就可以分解为 i = i + 1 ,那么类似于这从操作就称之为非原子操作

非原子操作带来的是线程安全问题,使用同步技术Synchronized来把这堆操作变成一个原子操作。

public class Test {    

    public volatile int inc = 0; 
    
    public void increase() {
        inc++;    
    }     

    public static void main(String[] args) {    
    
        final Test test = new Test();    
    
        for(int i=0;i<10;i++){            
            new Thread(){
                @Override                
                public void run() {                    
                    for(int j=0;j<1000;j++)                        
                        test.increase();                
                    };            
            }.start();        
        }         

        while(Thread.activeCount()>1){
            System.out.println(test.inc);   
        }  //保证前面的线程都执行完 
           
        Thread.yield();         
    }
}

上面代码,我们直观的认为新建了10个线程,每个线程都对inc变量自增1,那么10个线程最后输出的结果自然是1000*10=10000,这是我们所期望的。但是通过输出我们发现结果并不是我们所想要的。

前面提到过,Volatile只能保证变量的可见性,而不能保证原子性。

还是按实例来说,A、B线程创建后各自把inc拷贝到自己的本地内存中。此时A、B都在自己本地线程中对inc++自增,然后A、B线程把运算后的结果写入主内存中。这样,尽管A、B线程都进行了自增的运算,我们的期望是等于3,尽管进行了两次自增,但是此时主内存中的inc变量只是2。

例子也体现了Volatile并不能保证非原子操作,仍然会存在线程安全问题。

解决方案:可以通过synchronized或lock,进行加锁,来保证操作的原子性。也可以通过AtomicInteger。(不在本文范围)

有序性

有序性就是程序执行的顺序按照代码的先后顺序执行。Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。处理器的重排序不会影响单个线程,但是面临并发编程的时候就不能保证正确性了。

volatile关键字可以保证一定的“有序性”。synchronized既保证有序性同样保证原子性。

Volatile原理

在对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。

为了保证各个处理器的工作线程一致,每个处理会通过嗅探在总线上传播的数据来检查自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。

小结

synchronized是防止多个线程同时执行一段代码,这样同步就会影响程序执行效率,而volatile在某些情况下性能要优于synchronized。Volatile只能保证变量的可见性,并不能保证变量的原子性。对于由于非原子操作而产生的线程安全问题,还是请使用synchronized。

最后使用Volatile的必备两个条件:

  • 对变量的操作不依赖于当前值,也就是原子操作
  • 该变量没有包含在具有其他变量的不变式中