对象部分初始化:原理以及验证代码(双重检查锁与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. Vue中computed分析

    Vue中computed分析 在Vue中computed是计算属性,其会根据所依赖的数据动态显示新的计算结果,虽然使用{{}}模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的,在模板中放入太 ...

  2. 一文了解.Net Core 3.1 Web API基础知识

    一.前言 随着近几年前后端分离.微服务等模式的兴起,.Net Core也似有如火如荼之势 ,自16年发布第一个版本到19年底的3.1 LTS版本,以及将发布的.NET 5,.NET Core一路更迭, ...

  3. 手把手教你springboot中导出数据到excel中

    手把手教你springboot中导出数据到excel中 问题来源: 前一段时间公司的项目有个导出数据的需求,要求能够实现全部导出也可以多选批量导出(虽然不是我负责的,我自己研究了研究),我们的项目是x ...

  4. YOLOv4: Darknet 如何于 Ubuntu 编译,及使用 Python 接口

    本文将介绍 YOLOv4 官方 Darknet 实现,如何于 Ubuntu 18.04 编译,及使用 Python 接口. 主要内容有: 准备基础环境: Nvidia Driver, CUDA, cu ...

  5. 推荐条+fragment

    主布局 package com.example.dell.day1215; import android.support.design.widget.TabLayout; import android ...

  6. django rest_framework serializer的ManyRelatedField 和 SlugRelatedField使用

    class BlogListSerializer(serializers.Serializer): id = serializers.IntegerField() user = BlogUserInf ...

  7. 单例模式,reorder详解,线程安全,双检查锁

    单例模式,分为饿汉式单例 和 懒汉式单例. 先把本类对象所需内存在main函数执行前就new出来,这是饿汉式单例. 个人思考: 为什么饿汉式不独霸天下,还有什么必要去研究使用cpp11上支持的双检查锁 ...

  8. 使用SSM框架实现Sql数据导出成Excel表

    SSM框架实现SQL数据导出Excel 思路 首先在前端页面中添加一个导出功能的button,然后与后端controller进行交互. 接着在相应的controller中编写导出功能方法. 方法体: ...

  9. Java知识系统回顾整理01基础03变量02基本变量类型

    一.变量类型分类 一个变量的类型,决定了该变量可以包含什么样的值. Java中有八种基本类型,都是Java语言预先定义好的,并且是关键字. 这八种基本类型分别是:  整型 (4种) 字符型 (1种) ...

  10. python数据结构之二叉树的建立实例

    先建立二叉树节点,有一个data数据域,left,right 两个指针域 # coding:utf-8 class TreeNode(object): def __init__(self,left=N ...