Java中的volatile关键字

1.内存可见性

      Java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。由于 Java 内存模型( JMM)规定,所有的变量都存放在主内存中,而每个线程都有着自己的工作内存(高速缓存)。线程在工作时,需要将主内存中的数据拷贝到工作内存中。这样对数据的任何操作都是基于工作内存(效率提高),并且不能直接操作主内存以及其他线程工作内存中的数据,之后再将更新之后的数据刷新到主内存中这里所提到的主内存可以简单认为是堆内存,而工作内存则可以认为是栈内存(锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题)。虽然 volatile 保证了内存可见性,每个线程拿到的值都是最新值 ,但volatile 并不能保证线程安全性!

2.volatile的作用

      volatile具备两种特性,第一就是保证共享变量对所有线程的可见性。将一个共享变量声明为volatile后,会有以下效应:其一、当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;其二、这个写会操作会导致其他线程中的缓存无效。当一个变量被 volatile 修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。volatile 修饰之后并不是让线程直接从主内存中获取数据,依然需要将变量拷贝到工作内存中;内存可见性只是 volatile 的其中一个语义它还可以防止JVM 进行指令重排优化。使用 volatile 变量的主要原因是其简易性:在某些情形下,使用 volatile 变量要比使用相应的锁简单得多,使用 volatile 变量次要原因是其性能:某些情况下,volatile 变量同步机制的性能要优于锁。volatile 操作不会像锁一样造成阻塞,因此,在能够安全使用 volatile 的情况下,volatile 可以提供一些优于锁的可伸缩特性,且若读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。

3.正确使用 volatile 变量的条件

       正确使用volatile必须满足两个条件:
1、对变量的写操作不依赖当前值,如多线程下执行a++,是无法通过volatile保证结果准确性的;
2、该变量没有包含在具有其它变量的不变式中;具体代码如下:
import java.util.concurrent.CountDownLatch;
public class Test {
	public static volatile int num = 0;
    //使用CountDownLatch来等待计算线程执行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);
    public static void main(String []args) throws InterruptedException {
        //开启30个线程进行累加操作
        for(int i=0;i<30;i++){
            new Thread(){
                public void run(){
                    for(int j=0;j<10000;j++){
                        num++;//自加操作
                    }
                    countDownLatch.countDown();
                }
            }.start();
        }
        //等待计算线程执行完
        countDownLatch.await();
        System.out.println(num);
    }
}
        运行的结果总是小于:300000一些同学可能会觉得疑惑,如果用volatile修饰的共享变量可以保证可见性,那么结果不应该是300000么?问题就出在num++这个操作上,因为num++不是个原子性的操作,而是个复合操作我们可以简单讲这个操作理解为由这三步组成:1.读取;2.加一;3.赋值。所以,在多线程环境下,有可能线程A将num读取到本地内存中,此时其他线程可能已经将num增大了很多,线程A依然对过期的num进行自加,重新写到主存中,最终导致了num的结果不合预期,而是小于30000。针对num++这类复合类的操作,可以使用java并发包中的原子操作类原子操作类是通过循环CAS的方式来保证其原子性的,如下代码:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class Test {
	//使用原子操作类
    public static AtomicInteger num = new AtomicInteger(0);
    //使用CountDownLatch来等待计算线程执行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);
    public static void main(String []args) throws InterruptedException {
        //开启30个线程进行累加操作
        for(int i=0;i<30;i++){
            new Thread(){
                public void run(){
                    for(int j=0;j<10000;j++){
                        num.incrementAndGet();//原子性的num++,通过循环CAS方式
                    }
                    countDownLatch.countDown();
                }
            }.start();
        }
        //等待计算线程执行完
        countDownLatch.await();
        System.out.println(num);
    }
}

4.禁止指令重排序

        重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。但是重排序也需要遵守一定规则:
       1.重排序操作不会对存在数据依赖关系的操作进行重排序。比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
      2.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下可能就会出现问题,如下代码所示:
public class TestVolatile {
	int a = 1;
	boolean status = false;

	/**
	 * 状态切换为true
	 */
	public void changeStatus() {
		a = 2;// 1
		status = true;
	}

	/**
	 * 若状态为true,则running。
	 */
	public void run() {
		if (status) {
			int b = a + 1;// 3
			System.out.println(b);
		}
	}
}
        假设线程A执行changeStatus后,线程B执行run,我们能保证在4处,b一定等于3么?答案依然是无法保证!也有可能b仍然为2。上面我们提到过,为了提供程序并行度,编译器和处理器可能会对指令进行重排序,而上例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序volatile禁止指令重排序也有一些规则,简单列举一下:1.当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序;2.当地一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序;3.当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序。

猜你喜欢

转载自blog.csdn.net/u011635492/article/details/80371513