likes
comments
collection
share

好奇,为什么双重检查锁要使用volatile关键字呢?

作者站长头像
站长
· 阅读数 25

大家好,我是茄子。

前言

本文想通过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 为了提高运行速度就会进行指令重排序,这就会导致上面三个指令的执行顺序发生变化:newastoreinvokespecial。这会有什么问题呢?对象还没初始化完成,就被拿去使用了,可能会产生各种问题。

不过口说无凭,我们还是通过代码来验证一下。最开始的设想是使用多个线程调用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
评论
请登录