JMM-有序性

文章目录

  • 无序性
  • 指令级并行
  • 解决办法
  • 问题
    • 解决问题
  • happens-before

无序性

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序

// 这两行代码执行顺序是不一定的,可能先对j赋值
i = ...; 
j = ...; 

指令级并行

  • 示例代码
/**
 * @author pangjian
 * @ClassName ConcurrencyTest
 * @Description 并发测试
 * @date 2021/11/3 14:10
 */
@JCStressTest // 标记此类为一个并发测试类
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok") // 描述测试结果
@Outcome(id = {"0"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!!") // 描述测试结果
@State // 标记此类是有状态的
public class ConcurrencyTest {

    int num = 0;
    boolean ready = false;

    /**
     * @Description: 线程1执行此方法
     * @Param r:
     * @return void
     * @date 2021/11/3 20:40
    */
    @Actor
    public void actor1(I_Result r) {
        if (ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    /**
     * @Description: 线程2执行此方法,猜想如果ready为true了,那么num肯定赋值为2了,那么r1肯定为2+2=4,
     *               或者还没来得及ready为false,进入else语句,r1=1
     *               还有一种特殊情况,指令重排,ready首先执行了赋值了true,但没来得及num=2,那么执行0+0=0
     * @Param r:
     * @return void
     * @date 2021/11/3 20:40
    */
    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }

}
  • 测试
    JMM-有序性_第1张图片

  • 运行得到测试结果

这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现,可以看到测试结果里面,出现0结果的有5千多次

在这里插入图片描述


解决办法

加volatile修饰ready,维持可见性和有序性

@JCStressTest // 标记此类为一个并发测试类
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok") // 描述测试结果
@Outcome(id = {"0"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!!") // 描述测试结果
@State // 标记此类是有状态的
public class ConcurrencyTest {

    int num = 0;
    volatile boolean ready = false;

    /**
     * @Description: 线程1执行此方法
     * @Param r:
     * @return void
     * @date 2021/11/3 20:40
    */
    @Actor
    public void actor1(I_Result r) {
    	// 读屏障,确保指令重排时,不会将屏障之后的代码重排在读屏障之前
        if (ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    /**
     * @Description: 线程2执行此方法,猜想如果ready为true了,那么num肯定赋值为2了,那么r1肯定为2+2=4,
     *               或者还没来得及ready为false,进入else语句,r1=1
     *               还有一种特殊情况,指令重排,ready首先执行了赋值了true,但没来得及num=2,那么执行0+0=0
     * @Param r:
     * @return void
     * @date 2021/11/3 20:40
    */
    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true; // 写屏障,ready是volatile赋值带写屏障,确保指令重排序时,不会将写屏障之前的代码排在写屏障之后,也就是num=2不会出现在ready=true后面

    }

}

但不能解决指令交错


问题

  • 失败单例的示例
package singleton;

/**
 * @author pangjian
 * @ClassName Singleton5
 * @Description 双重检查
 * @date 2021/7/2 12:03
 */

public class Singleton5 {

    private static Singleton5 singleton5; // 不加volatile修饰,是一个错误的单例

    private Singleton5(){}

    /**
     * @Description:加入双重检查,解决了线程安全问题,同时解决懒加载问题
     * @return singleton.Singleton3
     * @date 2021/7/2 11:30
     */
    public static Singleton5 getInstance(){
        if(singleton5 == null){
            // 锁住该类,所有访问该类的线程,一次只有一个可以执行。,即使两个线程进入到此,一个线程先进去判断为空就创建单例对象,由于volatile修饰单例对象,第二个进去的时候,判断不为空,则不会创建对象,保留了只有一个单例
            synchronized (Singleton5.class) {
                if (singleton5 == null) {
                    singleton5 = new Singleton5();
                }
            }
        }
        return singleton5;
    }

}

// singleton5 = new Singleton5();语句的字节码
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
  • 17 表示创建对象,将对象引用入栈 // new Singleton
  • 20 表示复制一份对象引用 // 引用地址
  • 21 表示利用一个对象引用,调用构造方法
  • 24 表示利用一个对象引用,赋值给 static INSTANCE

synchronized 是不能保证指令重排的,也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:

JMM-有序性_第2张图片

那么t2线程会得到一个未初始化完成的对象(还没有执行构造方法)

解决问题

// 加volatile修饰,写屏障保证了不会出现指令重排,保证先构造再赋值
private static volatile Singleton5 singleton5;

happens-before

happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,俗话就是什么样的规则写,可以让其他线程可见

  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
static int x;
static Object m = new Object();
new Thread(()->{
	synchronized(m) {
	x = 10;
	}
},"t1").start();

new Thread(()->{
	synchronized(m) {
	System.out.println(x);
 	}
},"t2").start();
  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x;
new Thread(()->{
 	x = 10;
},"t1").start();

new Thread(()->{
 	System.out.println(x);
},"t2").start();
  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;
x = 10;
new Thread(()->{
 	System.out.println(x);
},"t2").start();
  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
static int x;
Thread t1 = new Thread(()->{
 	x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过
    t2.interrupted 或 t2.isInterrupted)
static int x;
public static void main(String[] args) {
	Thread t2 = new Thread(()->{
 	while(true) {
 		if(Thread.currentThread().isInterrupted()) {
 			System.out.println(x);
 			break;
 		}
 	}
},"t2");
t2.start();
new Thread(()->{
 	sleep(1);
 	x = 10;
 	t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
 	Thread.yield();
}
System.out.println(x);
}
  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
  • 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子
volatile static int x;
static int y;
new Thread(()->{ 
	y = 10;
	x = 20;
},"t1").start();
new Thread(()->{
 	// x=20 对 t2 可见, 同时 y=10 也对 t2 可见
 	System.out.println(x); 
},"t2").start();

你可能感兴趣的:(juc,java,开发语言,后端)