重排序

本文详细解析了CPU指令重排序的概念,解释了其在单线程和多线程环境中的影响,通过具体代码示例展示了重排序可能导致的多线程问题,并探讨了如何使用volatile关键字来防止特定操作的重排序,确保线程安全。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

重排序的定义直接看书是怎么说的:

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

就是cpu为了优化代码的执行效率,它不会按顺序执行代码,会打乱代码的执行顺序,前提是不影响单线程顺序执行的结果。(当然了,只考虑cpu级别的重排序,还有其他的)
书曰:

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

代码证明:

public class T04_Disorder {
      private static int x = 0, y = 0;
      private static int a = 0, b =0;
  
      public static void main(String[] args) throws InterruptedException {
          int i = 0;
          for(;;) {
              i++;
              x = 0; y = 0;
              a = 0; b = 0;
              Thread one = new Thread(new Runnable() {
                  public void run() {
                      //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
                      //shortWait(100000);
                      a = 1;
                      x = b;
                  }
              });
  
              Thread other = new Thread(new Runnable() {
                  public void run() {
                      b = 1;
                      y = a;
                  }
              });
              one.start();other.start();
              one.join();other.join();
              String result = "第" + i + "次 (" + x + "," + y + ")";
              if(x == 0 && y == 0) {
                  System.err.println(result);
                  break;
              } else {
                  //System.out.println(result);
              }
          }
      }
  
  
      public static void shortWait(long interval){
          long start = System.nanoTime();
          long end;
          do{
              end = System.nanoTime();
          }while(start + interval >= end);
      }
  }

想一下,如果不存在重排序,线程1和线程2输出的x、y值至少有一个等于1,因为假如是线程1先执行到对a=1,那么线程2输出的y就等1,假如是线程2先执行b=1,那么线程1输出的x就等于1,不存在x、y同时等于0的情况。
但如果有重排序,线程1执行的顺序是x=b,a=1,线程2执行的顺序是y=a,b=1,那么输出的结果就是x、y同时等于0!
当然想测出来需要比较长的时间,要捕捉到刚好这个重排序几率比较小,想看结果的得等个10分钟以上。


上一段代码如果不是很清楚,还有一个案例,书籍的原案例,我改动了一下:

class ReorderExample { 
	int a = 0; 
	boolean flag = false; 
	public void writer() { 
	  	a = 1;                  // 操作1
	  	flag = true;            // 操作2 
	}
	public void reader() {
		if (flag) {            // 操作3 
			int i =  a * a;     // 操作4 
		
		}
	}
	 public static void main(String[] args) throws InterruptedException {
          int count = 0;
          for(;;) {
              Thread one = new Thread(new Runnable() {
                  public void run() {
                      //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
                      //shortWait(100000);
                      writer();
                  }
              });
  
              Thread other = new Thread(new Runnable() {
                  public void run() {
                      reader();
                  }
              });
              one.start();other.start();
              one.join();other.join();
              String result = "第" + count + "次 (" + a + "," + flag + ")";
              if(a == 0 && flag == false) {
                  System.err.println(result);
                  break;
              } else {
                  //System.out.println(result);
              }
          }
      }
       public static void shortWait(long interval){
          long start = System.nanoTime();
          long end;
          do{
              end = System.nanoTime();
          }while(start + interval >= end);
      }
}

flag变量是个标记,用来标识变量a是否已被写入。这里假设有两个线程A和B,A首先执行 writer()方法,随后B线程接着执行reader()方法。线程B在执行操作4时,能否看到线程A在操作 1对共享变量a的写入呢?
答案是:不一定能看到。
由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样, 操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。让我们先来 看看,当操作1和操作2重排序时,可能会产生什么效果?请看下面的程序执行时序图:
在这里插入图片描述
虚箭线标识错误的读操作。
操作1和操作2做了重排序。程序执行时,线程A首先写标记变量flag,随后线 程B读这个变量。由于条件判断为真,线程B将读取变量a。此时,变量a还没有被线程A写入,在这里多线程程序的语义被重排序破坏了!


DoubleCheckLock的单例

在这里插入图片描述
T t = new T();的执行过程:
1、new #2 < T > 为申请空间,给T类的m赋值为默认值,即m=0
2、invokespecial #3 <T.< init>> 调用构造方法,把m设为8
3、astore_1 建立连接

DCL的代码

class DCLSinleTon(){
	private volatile static DCLSingleTon ton == null;
	private DCLSingleTon(){
		ton= new DCLSingleTon();
	}
	public DCLSinleTon getTon(){
		if (ton == null ) {
			synchronized(DCLSingleTon.class) {
				if (ton == null ) {
					ton = new DCLSingleTon();
				}
			}
		}
		return ton;
	}
}

分析一下,指令重排会有哪种可能,刚刚通过对象的创建过程我们可以知道,在ton = new DCLSingleTon()这行代码中的执行顺序,首先半初始化会申请空间,第二步会调用构造方法,第三步把ton指向申请的空间。
而第二步和第三步可能会执行重排序,就会把ton指向半初始化的申请空间,这个时候正好有第二个线程来了,判断了一下,发现ton它不等于null了,就会拿到这个特殊的ton。
在这里插入图片描述
volatile就是防止了第二步和第三步的重排序。
来阅读下书籍内容
在这里插入图片描述

由于单线程内要遵守intra-thread semantics,从而能保证A线程的执行结果不会被改变。但 是,当线程A和B按图3-38的时序执行时,B线程将看到一个还没有被初始化的对象。

在这里插入图片描述

这里A2和A3虽然重排序了,但Java内存模型的intra-thread semantics将确保A2一定会排在 A4前面执行。因此,线程A的intra-thread semantics没有改变,但A2和A3的重排序,将导致线程 B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象。

在知晓了问题发生的根源之后,我们可以想出两个办法来实现线程安全的延迟初始化。
1)不允许2和3重排序。
2)允许2和3重排序,但不允许其他线程“看到”这个重排序。

对于前面的基于双重检查锁定来实现延迟初始化的方案(指DoubleCheckedLocking示例代 码),只需要做一点小的修改(把instance声明为volatile型),就可以实现线程安全的延迟初始化.
当声明对象的引用为volatile后,3.8.2节中的3行伪代码中的2和3之间的重排序,在多线程 环境中将会被禁止。上面示例代码将按如下的时序执行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值