对象部分初始化:原理以及验证代码(双重检查锁与volatile相关)

对象部分初始化被称为 Partially initialized objects / Partially constructed objects / Incompletely initialized objects

这三种不同的说法描述的是同一种情况,即指令重排序(reorder)导致未完全初始化的对象被使用,这会导致某些错误的发生。

文章纯原创,转载请表明地址

对象初始化过程

要理解对象部分初始化,那就要先理解对象初始化。

package Singleton;

public class NewObject {
public static void main(String[] args) {
NewObject newObject = new NewObject();
}
}

上面是一个非常简单的新建对象代码,newObject字段指向堆中新建立的对象,将上面代码反编译成字节码。

0 new #2 <Singleton/NewObject>
3 dup
4 invokespecial #3 <Singleton/NewObject.<init>>
7 astore_1
8 return

阅读字节码

1. new

根据Oracle官方文档描述,第0行(以行前标记为准) 的new指令进行了如下操作

Memory for a new instance of that class is allocated from the garbage-collected heap, and the instance variables of the new object are initialized to their default initial values (§2.3, §2.4). The objectref, a reference to the instance, is pushed onto the operand stack.

翻译一下就是,该指令为指定类的实例在堆中分配了内存空间,并且将这个新对象的实例变量进行了默认初始化,即 int 类型为 0, boolean类型为 false。并且该指令还将一个指向该实例的引用推入操作数栈中。

dup复制一份操作数栈顶的值,并且推入栈中 。

2. invokespecial

这个指令比较复杂,此处只需要知道该指令在此处调用了对象的初始化函数 NewObject.<init>,对象初始化会按照静态变量、静态初始化块->变量、初始化块->构造器等顺序进行初始化,这个不是关键,关键是初始化在此时进行。该指令结束后对象会被正确的初始化。

3. astore

该指令将操作数栈顶的值储存到局部变量表中,astore_1在此处代表的就是将值储存到变量newObject中。

如果变量不是声明在方法中,而是声明在类中,那指令会变为putfield 。无论变量声明在何处,使用哪个指令,目的是为了将操作数栈顶的值储存到它该去的地方。

指令重排下的对象初始化

初始化的过程看起来没有任何问题,按照123的顺序执行的话在使用对象引用时对象一定是初始化完成的,但是为了效率,当今的CPU是”流水线“执行指令,即指令顺序输入,乱序执行,CPU在确保最终结果的前提下会按照最高效率的方式执行指令,而不是顺序的执行。

在对象初始化的过程中,CPU很可能的执行顺序是132,即 new astore invokespecial

如果是在单线程的情况下,132的执行顺序不会造成什么问题,因为CPU会保证不在invokespecial完成前使用对象。

但是在多线程的情况下就不一样了,乱序执行会导致线程A在对象初始化完成前就将引用X指向了堆中的对象,这个引用X是共享资源,其他线程也能看的到这个变量。线程B并不知道线程A中发生了什么,当线程B需要使用引用X的时候会出现以下三种情况

  1. 线程A还未将引用X指向对象,线程B获得的X是null;
  2. 初始化完成,线程B使用的对象是正确的对象;
  3. 引用X指向了堆中的对象,但是线程A中进行的初始化未完成,线程B使用的对象是部分初始化的对象。

Show me the code

对象部分初始化的问题最开始是在学习单例设计模式、双重检查锁(Double-check-lock)的过程中了解到的,DCL由于指令重排序,不在对象上加volatile关键字就会导致对象部分初始化问题。原理问题在国内外各种博客和论坛上都有描述,也都大同小异。

但困扰我的关键在于没有找到能给出DCL不加volatile会出问题的代码,换句话说,大家谈的都是理论,没有博客/文章/回答能够用代码说明这个问题确实存在。

根据维基百科的描述,这个问题是非常难以再现的。

Depending on the compiler, the interleaving of threads by the scheduler and the nature of other concurrent system activity, failures resulting from an incorrect implementation of double-checked locking may only occur intermittently. Reproducing the failures can be difficult.

在我尝试亲手复现错误的代码时,我发现如果要把测试放在单例类中,则一次运行时只能对对象进行一次初始化,其他线程只有在这一次初始化的间隙中有机会调用“不正确”的对象,在这种情况下我可能手动把程序跑上三天三夜都没办法复现一次这个问题。

于是换了一个思路,并不需要在DCL的单例模式中证明这个问题,只要能证明对象部分初始化问题存在即可。

代码设计思路:

  1. 乱序重排发生在对象初始化中,需要有一个线程尽可能多的进行类的初始化,好让其他线程能尽量捕捉到问题(static class Initialize)
  2. 需要许多个线程不断的调用被初始化的类,并且判断这个类是否有被正确初始化(static class GetObject)
  3. 存在一个类作为被初始化的对象(class PartiallyInitializedObject)
  4. 存在一个类持有上面对象的引用,线程通过这个类进行对象初始化并且给引用赋值,也通过这个类获取到引用(class Builder)

代码

mport java.util.Random;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger; class PartiallyInitializedObject{
static long counter;
// final field will avoid partiallyInitializedObject
// final long id = counter++;
public int n;
public PartiallyInitializedObject(int n){
this.n = n;
}
} class Builder{
public int createNumber = 0;
public AtomicInteger getNumber = new AtomicInteger(0);
Random rand = new Random(47);
//private volatile PartiallyInitializedObject partiallyInitializedObject;
private PartiallyInitializedObject partiallyInitializedObject; public PartiallyInitializedObject get(){
getNumber.incrementAndGet();
return partiallyInitializedObject;
} public void initialize(){
partiallyInitializedObject = new PartiallyInitializedObject(rand.nextInt(20)+5);
createNumber++;
}
} public class PartiallyInitialized {
static class Initialize implements Runnable{
Builder builder;
public Initialize(Builder builder){
this.builder = builder;
}
@Override
public void run() {
while(!Thread.interrupted()){
builder.initialize();
}
}
}
static class GetObject implements Runnable{
static int count =0;
final int id = count++;
CyclicBarrier cyclicBarrier;
Builder builder;
public GetObject(CyclicBarrier c, Builder builder){
cyclicBarrier = c;
this.builder = builder;
}
@Override
public void run() {
while (!Thread.interrupted()) {
PartiallyInitializedObject p = builder.get();
if (p.n == 0) {
System.out.println("Thread " + id +" Find Partially Initialized Object " + p.n);
Thread.currentThread().interrupt();
}
}
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("Thread " + id +" Interrupted");
}
} public static void main(String[] args) throws BrokenBarrierException, InterruptedException{
// first initialize(), second get()
// 1 initialize(), 9 get()
Builder builder = new Builder();
CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
ExecutorService exec = Executors.newFixedThreadPool(10); exec.execute(new Initialize(builder));
for(int i=0; i<9; i++){
exec.execute(new GetObject(cyclicBarrier, builder));
}
// exec.execute(new Initialize(builder));
try {
cyclicBarrier.await(3, TimeUnit.SECONDS);
} catch (TimeoutException e) {
System.out.println("No Partially Initialized Object Found");
}
exec.shutdownNow();
System.out.println("Builder create "+builder.createNumber +" Object And Try to get "+ builder.getNumber.get()+ " times");
}
}

Builder 类中的变量partiallyInitializedObject不使用volatile修饰时输出如下

Thread 5 Find Partially Initialized Object 13
Thread 3 Find Partially Initialized Object 23
Thread 0 Find Partially Initialized Object 6
Thread 1 Find Partially Initialized Object 10
Thread 2 Find Partially Initialized Object 11
Thread 8 Find Partially Initialized Object 23
Thread 4 Find Partially Initialized Object 14
Thread 6 Find Partially Initialized Object 6
Thread 7 Find Partially Initialized Object 24
Thread 7 Interrupted
Thread 5 Interrupted
Thread 3 Interrupted
Thread 8 Interrupted
Thread 0 Interrupted
Thread 6 Interrupted
Thread 4 Interrupted
Thread 2 Interrupted
Thread 1 Interrupted
Builder create 46736 Object And Try to get 231239 times

Builder 类中的变量partiallyInitializedObject使用volatile修饰时输出如下

No Partially Initialized Object Found
Builder create 7661170 Object And Try to get 72479637 times
Thread 3 Interrupted
Thread 7 Interrupted
Thread 0 Interrupted
Thread 6 Interrupted
Thread 1 Interrupted
Thread 8 Interrupted
Thread 5 Interrupted
Thread 2 Interrupted
Thread 4 Interrupted
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
at Singleton.PartiallyInitialized$GetObject.run(PartiallyInitialized.java:66)
......

代码中在线程池在执行调用GetObject线程之前先执行Initialize的线程,如果把exec.execute(new Initialize(builder));放到GetObject的线程后面,那就会出现之前说的三种情况中的第一种:GetObject获得的引用为空。

观察代码和输出,在GetObject线程中,只有当对象PartiallyInitializedObject.n的值为0时才会进行输出并且打断当前线程,而在Builderinitialize()中能很明显的看到,对象的n值是大于等于5并且小于25,即永远不可能为0。但输出的结果却证明了GetObject线程在某些时刻确实能得到为0的n值。代码剩余的细节这里就不再赘述。

到这一步就能够说明确实存在指令重排序而导致的对象部分初始化问题,由于synchronizedvolatile保证可见性和有序性的原理并不相同,所以在DCL单例模式这种特殊的情况下,synchronized也不能很好的确保正确。当然,由于种种原因,DCL单例模式已经基本被弃用了,这篇文章只做一些相关的探讨。

参考

https://wiki.sei.cmu.edu/confluence/display/java/TSM03-J.+Do+not+publish+partially+initialized+objects

https://stackoverflow.com/questions/7855700/why-is-volatile-used-in-double-checked-locking

https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.new

对象部分初始化:原理以及验证代码(双重检查锁与volatile相关)的更多相关文章

  1. Java基础教程:多线程杂谈——双重检查锁与Volatile

    Java基础教程:多线程杂谈——双重检查锁与Volatile 双重检查锁 有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化.此时程序员可能会采用延迟初始化.但要正确实 ...

  2. Java中的双重检查锁(double checked locking)

    最初的代码 在最近的项目中,写出了这样的一段代码 private static SomeClass instance; public SomeClass getInstance() { if (nul ...

  3. C++的双重检查锁并不安全(转)

    一个典型的单例模式构建对象的双重检查锁如下: static Singleton * getSingleObject() { if(singleObject==NULL) { lock(); if(si ...

  4. 单例模式中用volatile和synchronized来满足双重检查锁机制

    背景:我们在实现单例模式的时候往往会忽略掉多线程的情况,就是写的代码在单线程的情况下是没问题的,但是一碰到多个线程的时候,由于代码没写好,就会引发很多问题,而且这些问题都是很隐蔽和很难排查的. 例子1 ...

  5. 双重检查锁实现单例(java)

    单例类在Java开发者中非常常用,但是它给初级开发者们造成了很多挑战.他们所面对的其中一个关键挑战是,怎样确保单例类的行为是单例?也就是说,无论任何原因,如何防止单例类有多个实例.在整个应用生命周期中 ...

  6. 【Java学习笔记】线程安全的单例模式及双重检查锁—个人理解

    搬以前写的博客[2014-12-30 16:04] 在web应用中服务器面临的是大量的访问请求,免不了多线程程序,但是有时候,我们希望在多线程应用中的某一个类只能新建一个对象的时候,就会遇到问题. 首 ...

  7. 从学习“单例模式”学到的Java知识:双重检查锁和延迟初始化

    一切真是有缘,上午刚刚看完单例模式,还在为其中的代码块同步而兴奋,下午就遇见这篇文章:双重检查锁定与延迟初始化.我一看,文章开头语出惊人,说这是一种错误的优化,我说,难道上午学的东西下午就过时了吗?仔 ...

  8. 为什么双重检查锁模式需要 volatile ?

    双重检查锁定(Double check locked)模式经常会出现在一些框架源码中,目的是为了延迟初始化变量.这个模式还可以用来创建单例.下面来看一个 Spring 中双重检查锁定的例子. 这个例子 ...

  9. 双重检查锁单例模式为什么要用volatile关键字?

    前言 从Java内存模型出发,结合并发编程中的原子性.可见性.有序性三个角度分析volatile所起的作用,并从汇编角度大致说了volatile的原理,说明了该关键字的应用场景:在这补充一点,分析下v ...

随机推荐

  1. Django request

    ''' 1.HttpRequest.GET 一个类似于字典的对象,包含 HTTP GET 的所有参数.详情请参考 QueryDict 对象. 2.HttpRequest.POST 一个类似于字典的对象 ...

  2. 使用vue-cli(vue脚手架)快速搭建项目-2

    接上一篇文章,这篇文章对如何使用IDEA打开并运行项目做教程 1.将在窗口模式启动的Vue关闭 只需要按住Ctrl+C,输入Y就可以了 2.打开idea 3.复制你项目所在地址,然后点击OK 4.下面 ...

  3. 5.Selector详解

  4. C# 9.0 新特性预览 - 顶级语句

    C# 9.0 新特性预览 - 顶级语句 前言 随着 .NET 5 发布日期的日益临近,其对应的 C# 新版本已确定为 C# 9.0,其中新增加的特性(或语法糖)也已基本锁定,本系列文章将向大家展示它们 ...

  5. pytest自学第一期

    开始自学pytest了,我并不想看网上的各种自学教程和文档,要看咱们今天就看pytest的官方文档,不会英语咱们就用翻译,看不懂原理咱们就翻源码,就人肉试错 学习一个技术,使用速成鸡的套路是一个办法, ...

  6. OpenGL的shader编写,GLSL基本语法

    重要!!! 文章中涉及到的代码在我的GitHub仓库里应该都能对应找到, 关于学习OpenGL的实操项目一般都会在GLBIproject1,2,...中对应找到 每个仓库中都有不同的版本的项目,更新版 ...

  7. Mysql探索之Explain执行计划详解

    前言 如何写出效率高的SQL语句,提到这必然离不开Explain执行计划的分析,至于什么是执行计划,如何写出高效率的SQL,本篇文章将会一一介绍. 执行计划 执行计划是数据库根据 SQL 语句和相关表 ...

  8. springboot的启动流程源码分析

    .测试项目,随便一个简单的springboot项目即可: 直接debug调试: 可见,分2步,第一步是创建SpringApplication对象,第二步是调用run方法: 1.SpringApplic ...

  9. Java知识系统回顾整理01基础01第一个程序02命令行格式编译和执行Java程序

    一.先看运行效果 在控制台下运行第一个Java程序,可以看到输出了字符串 hello world 二.准备项目目录 通常都会在e: 创建一个project目录 在这个例子里,我们用的是e:/proje ...

  10. Python实现的数据结构与算法之基本搜索详解

    一.顺序搜索 顺序搜索 是最简单直观的搜索方法:从列表开头到末尾,逐个比较待搜索项与列表中的项,直到找到目标项(搜索成功)或者 超出搜索范围 (搜索失败). 根据列表中的项是否按顺序排列,可以将列表分 ...