关于 Java中的volatile(2)

package test; import java.util.concurrent.CountDownLatch; /** * Created by chengxiao on 2017/3/18. */ public class Counter { 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); } }

执行结果:

224291

针对这个示例,一些同学可能会觉得疑惑,如果用volatile修饰的共享变量可以保证可见性,那么结果不应该是300000么?

问题就出在num++这个操作上,因为num++不是个原子性的操作,而是个复合操作。我们可以简单讲这个操作理解为由这三步组成:

  1.读取

  2.加一

  3.赋值

  所以,在多线程环境下,有可能线程A将num读取到本地内存中,此时其他线程可能已经将num增大了很多,线程A依然对过期的num进行自加,重新写到主存中,最终导致了num的结果不合预期,而是小于30000。

解决num++操作的原子性问题

  针对num++这类复合类的操作,可以使用java并发包中的原子操作类原子操作类是通过循环CAS的方式来保证其原子性的。

/** * Created by chengxiao on 2017/3/18. */ public class Counter {
  //使用原子操作类
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); } }

执行结果

300000

关于原子类操作的基本原理,会在后面的章节进行介绍,此处不再赘述。

禁止指令重排序

volatile还有一个特性:禁止指令重排序优化。

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。但是重排序也需要遵守一定规则:

  1.重排序操作不会对存在数据依赖关系的操作进行重排序。

    比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。

  2.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变

    比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。

  重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下,问题就出来了,来开个例子,我们对第一个TestVolatile的例子稍稍改进,再增加个共享变量a

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/801709d732cd0cb9c2a97c297a0740a9.html