好奇,为什么双重检查锁要使用volatile关键字呢?
大家好,我是茄子。
前言
本文想通过jcstress工具证明对象在创建的过程中如果被访问会出现部分初始化问题,进而表明使用双重检查锁创建单例模式一定要添加volatile
关键字。
class Singleton {
private Singleton() {
}
private volatile static Singleton INSTANCE;
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
对象初始化
public class Singleton {
public static void main(String[] args) {
Singleton singleton = new Singleton();
}
}
我们先使用java Singleton.java
得到字节码,再使用javap -c Singleton.class
查看字节码。
Compiled from "Singleton.java"
public class wiki.sponge.designpatterns.ordering.Singleton {
public wiki.sponge.designpatterns.ordering.Singleton();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #7 // class wiki/sponge/designpatterns/ordering/Singleton
3: dup
4: invokespecial #9 // Method "<init>":()V
7: astore_1
8: return
}
可以看到new
指令、invokespecial
指令和astore
指令,这三个指令的作用分别是为对象分配内存,调用对象的初始化函数,最后将引用赋值给singleton
变量。但是 JVM 中为了提高程序执行速度引入了 JIT 编译器(Just In Time Compiler),而 JIT 为了提高运行速度就会进行指令重排序,这就会导致上面三个指令的执行顺序发生变化:new
、astore
、invokespecial
。这会有什么问题呢?对象还没初始化完成,就被拿去使用了,可能会产生各种问题。
不过口说无凭,我们还是通过代码来验证一下。最开始的设想是使用多个线程调用getInstance
方法获取单例对象,然后检查单例对象中的成员变量是否完成初始化,来证明出现了指令重排序问题。但是这里非常难复现,所以另辟蹊径,只要能证明对象部分初始化问题存在即可。
JCStress 工具
JCStress(Java Concurrency Stress Tests)是一个用于测试和验证Java并发程序正确性的工具。 它是OpenJDK项目的一部分,旨在帮助开发人员发现并发程序中的竞态条件、死锁、内存可见性等问题。 JCStress提供了一组注解和API,使得编写并发测试变得简单和方便。它可以很好的帮助我们完成这个任务。
验证代码
创建一个maven工程,pom文件内容如下:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>wiki.sponge.concurrent</groupId>
<artifactId>jcstress_ordering</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<!--
This is the demo/sample template build script for building concurrency tests
with JCStress.
Edit as needed.
-->
<prerequisites>
<maven>3.0</maven>
</prerequisites>
<dependencies>
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>${jcstress.version}</version>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!--
jcstress version to use with this project.
-->
<jcstress.version>0.5</jcstress.version>
<!--
Java source/target to use for compilation.
-->
<javac.target>1.8</javac.target>
<!--
Name of the test Uber-JAR to generate.
-->
<uberjar.name>jcstress</uberjar.name>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<compilerVersion>${javac.target}</compilerVersion>
<source>${javac.target}</source>
<target>${javac.target}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.2</version>
<executions>
<execution>
<id>main</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>${uberjar.name}</finalName>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jcstress.Main</mainClass>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/TestList</resource>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
测试代码如下,这是jcstress官方提供的一个测试类UnsafePublication.java
@JCStressTest
@Description("Tests if unsafe publication is unsafe.")
@Outcome(id = "-1", expect = Expect.ACCEPTABLE, desc = "The object is not yet published")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "The object is published, but all fields are 0.")
@Outcome(id = "1", expect = Expect.ACCEPTABLE_INTERESTING, desc = "The object is published, at least 1 field is visible.")
@Outcome(id = "2", expect = Expect.ACCEPTABLE_INTERESTING, desc = "The object is published, at least 2 fields are visible.")
@Outcome(id = "3", expect = Expect.ACCEPTABLE_INTERESTING, desc = "The object is published, at least 3 fields are visible.")
@Outcome(id = "4", expect = Expect.ACCEPTABLE, desc = "The object is published, all fields are visible.")
@State
public class UnsafePublication {
/*
Implementation notes:
* This showcases how compiler can move the publishing store past the field stores.
* We need to provide constructor with some external value. If we put the constants in the
constructor, then compiler can store all the fields with a single bulk store.
* This test is best to be run with either 32-bit VM, or 64-bit VM with -XX:-UseCompressedOops:
it seems the compressed references mechanics moves the reference store after the field
stores, even though not required by JMM.
*/
int x = 1;
MyObject o;
@Actor
public void publish() {
o = new MyObject(x);
}
@Actor
public void consume(I_Result res) {
MyObject lo = o;
if (lo != null) {
res.r1 = lo.x00 + lo.x01 + lo.x02 + lo.x03;
} else {
res.r1 = -1;
}
}
static class MyObject {
int x00, x01, x02, x03;
public MyObject(int x) {
x00 = x;
x01 = x;
x02 = x;
x03 = x;
}
}
}
这段代码的意思就是一些线程去访问myObject,另一些线程去创建myObject,在创建的过程中去访问,将对象的成员变量值相加保存到res.r1
中。结果输出-1、4
正常的,输出1、2、3
是我们感兴趣的,它表明会存在部分初始化问题。
运行结果如下
*** INTERESTING tests
Some interesting behaviors observed. This is for the plain curiosity.
2 matching test results.
[OK] wiki.sponge.designpatterns.ordering.UnsafePublication
(JVM args: [-XX:+UnlockDiagnosticVMOptions, -XX:+StressLCM, -XX:+StressGCM])
Observed state Occurrences Expectation Interpretation
-1 123,573,316 ACCEPTABLE The object is not yet published
0 0 ACCEPTABLE_INTERESTING The object is published, but all fields are 0.
1 190 ACCEPTABLE_INTERESTING The object is published, at least 1 field is visible.
2 1,798 ACCEPTABLE_INTERESTING The object is published, at least 2 fields are visible.
3 357 ACCEPTABLE_INTERESTING The object is published, at least 3 fields are visible.
4 3,385,990 ACCEPTABLE The object is published, all fields are visible.
[OK] wiki.sponge.designpatterns.ordering.UnsafePublication
(JVM args: [-XX:-TieredCompilation, -XX:+UnlockDiagnosticVMOptions, -XX:+StressLCM, -XX:+StressGCM])
Observed state Occurrences Expectation Interpretation
-1 119,391,940 ACCEPTABLE The object is not yet published
0 0 ACCEPTABLE_INTERESTING The object is published, but all fields are 0.
1 0 ACCEPTABLE_INTERESTING The object is published, at least 1 field is visible.
2 1,043 ACCEPTABLE_INTERESTING The object is published, at least 2 fields are visible.
3 2,548 ACCEPTABLE_INTERESTING The object is published, at least 3 fields are visible.
4 4,463,570 ACCEPTABLE The object is published, all fields are visible.
可以发现,出现了1、2、3这两种值,也就表明指令重排序会导致对象存在部分初始化,那么访问一个部分初始化的对象,就会出现运行时异常,所以我们要加volatile关键字。
回到单例模式中,如果单例对象有一些引用类的成员变量,那么如果在未初始化的情况下进行访问,就会出现空指针现象。
当然 jcstress 也提供了对双重检查锁的测试代码,不安全的双重检查锁,但是我没有复现出来。大家感兴趣可以看看,如果有复现出现的,也请在评论区讨论,非常感谢。
参考资料
转载自:https://juejin.cn/post/7372514352426450956