您的位置 首页 java

Java并发之volatile关键字

Java中的volatile关键字用来标记一个变量“被存储在主内存中”,意思是说每次读取 volatile 变量都会从出内存读取而不是 CPU缓存 ,每次对volatile变量的写操作除了会更新相应的CPU缓存还会更新到主内存中。

实际上,从 JDK 1.5以后,volatile关键字还提供其他功能。

可见性问题

Java中的volatile关键字能够保证对变量的值的改变能够在多个线程都可见。

多线程 应用中,当多个线程操作一个非volatile变量时,基于性能考虑(CPU缓存速度高于主内存),每个线程会从主内存中将该变量的值拷贝到CPU的缓存中再进行操作。当程序部署在一个多CPU的计算机上时,每个线程其实是允许在不同的CPU上的,这会导致每个线程会先把该变量拷贝的它运行的CPU的缓存内,如图所示:

CPU缓存、线程、主内存关系图

当使用非volatile变量时,我们是无法保证在多线程情况下JVM对该变量的读写操作能够正常工作的,我们看以下代码:

  private   static  boolean flag;
    
public static  void  main(String[] args) throws  Exception  {
  Thread t1 = new Thread() {
    @ Override 
    public void run() {
      while (true) {
        if (flag) {
          System.out.println("I am thread1, flag is true");
          flag = false;
        }
      }
    }
  };
  t1.start();

  Thread t2 = new Thread() {
    @Override
    public void run() {
      while (true) {
        if (flag == false) {
          System.out.println("I am thread2, flag is false");
          flag = true;
        }
      }
    }
  };
  t2.start();

}  

我们开启了两个线程,希望两个线程能够交替运行打印,但实际上当程序运行一段时间后便不再打印了。原因是每个线程对应的CPU缓存都有一份flag变量的拷贝,并且是基于CPU缓存中的值进行计算的, JVM 并不能保证每个线程对flag的改动都能立刻反应到其他线程中去。

volatile保证可见性

我们对上面的代码加上volatile再看看效果呢:

 private volatile static boolean flag;
    
public static void main(String[] args) throws Exception {
  Thread t1 = new Thread() {
    @Override
    public void run() {
      while (true) {
        if (flag) {
          System.out.println("I am thread1, flag is true");
          flag = false;
        }
      }
    }
  };
  t1.start();

  Thread t2 = new Thread() {
    @Override
    public void run() {
      while (true) {
        if (flag == false) {
          System.out.println("I am thread2, flag is false");
          flag = true;
        }
      }
    }
  };
  t2.start();

}  

这时两个线程就能交替打印了,原因是当变量申明了volatile关键字,某个线程对变量的改动除了会更新该线程所运行的CPU缓存,还会更新主内存,同时让其他CPU缓存内有该变量的拷贝失效从而重新从主内存读取最新的值。

volatile对有序性的保证

基于性能原因,JVM和CPU是允许对代码进行重排的, jdk 1.5之后进行了一些优化,我们来看下面的代码段:

 private static int a;
private static int b;
private static volatile int c;
private static int d;
private static int e;

public static void testOrder() {
  a = 1;
  b = 2;
  c = 3;
  d = 4;
  e = 5;
}  

在jdk1.5之前,针对testOrder方法,系统可能执行的顺序是这样的:

 e = 5;
d = 4;
c = 3;
b = 2;
a = 1;  

在jdk1.5后,做了一些有序性优化,a和b是必须在c之前执行的(a和b顺序可以调换),d和e是不需要在c之后执行(d和e顺序可以调换)。

那这个有什么用呢?我们来看下面这段代码:

 private boolean init = false;
//ServiceA
public void init(){
   // do some init work
  initServiceA()
  init = true;
}

//ConsumeA
if(init){
 // do some work
  ServiceA.doSomething();
}  

如果init不加volatile,实际程序运行是init = true;可以先于initServiceA()执行,这就导致了SerivceA还没有初始化完成已经被 Consumer 调用了。

原子操作

volatile不能保证非原子操作的一致性,请看如下代码:

 private static volatile int value = 0;
    
public static void main(String[] args) {
  Runnable run = new Runnable() {
    @Override
    public void run() {
      for (int i = 0; i < 1000; i++) {
        value++;
      }
    }
  };

  Thread t1 = new Thread(run);
  Thread t2 = new Thread(run);
  t1.start();
  t2.start();

  while(Thread.activeCount() > 1) {
    Thread. yield ();
  }
  System.out.println(value);
}  

这里我们期望value变量经过两个线程的1000次自增操作后能得到2000,但是实际情况是我们只能得到一个小于2000的值,为什么呢?因为value++在CPU执行的时候并不是一个原子操作,他大致会被分成三步:

1.从主内存读取value放入CPU缓存

2.对value加1

3.将value加1后的值赋给value,此时更新主内存

这里在多线程的情况下可能会产生上面代码错误的结果。

volatile和cas(Unsafe对象提供)是构成java并发包(JUC)的基石,后面会陆续介绍JUC的相关内容,欢迎关注和留言。

Demo代码位置

参考:

#:~:text=The%20Java%20volatile%20keyword%20is%20used%20to%20mark,and%20not%20%20just%20to%20the%20CPU%20cache.

文章来源:智云一二三科技

文章标题:Java并发之volatile关键字

文章地址:https://www.zhihuclub.com/183168.shtml

关于作者: 智云科技

热门文章

网站地图