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;
}
}
这种现象叫做指令重排,是 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;
synchronized 是不能保证指令重排的,也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:
那么t2线程会得到一个未初始化完成的对象(还没有执行构造方法)
// 加volatile修饰,写屏障保证了不会出现指令重排,保证先构造再赋值
private static volatile Singleton5 singleton5;
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,俗话就是什么样的规则写,可以让其他线程可见
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 static int x;
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t2").start();
static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
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);
}
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();