java对象半初始化问题是怎么回事

news2024/11/25 18:48:27

文章目录

  • 一、前言
    • 1. 什么是Java对象半初始化
    • 2. 对象半初始化问题引发的影响
  • 二、对象半初始化问题详解
    • 1. Java对象创建过程
  • 2. 对象半初始化问题产生的原因
  • 三、实例分析:对象半初始化问题的表现
    • 1. 单线程环境下的半初始化
    • 2. 多线程环境下的半初始化
  • 四、解决方案及防范措施
    • 1. 禁止指令重排序
    • 2. 使用安全发布对象的模式
  • 5.参考文档

背景: 最近无意间看到关于java对象半初始化问题,趁着周末今天聊聊java对象半初始化问题。
在这里插入图片描述

一、前言

1. 什么是Java对象半初始化

Java对象的创建过程包括内存分配、执行构造方法进行初始化和设置堆内存中的引用地址。在多线程环境下,由于Java内存模型允许指令的重排序,可能导致一个线程看到了另一个线程创建的对象的引用地址,但是这个对象还没有完成初始化。此时,我们称这个对象为半初始化对象。

2. 对象半初始化问题引发的影响

对象半初始化问题可能导致多线程环境下程序的行为异常,比如:

  • 线程安全问题 一个线程在使用尚未初始化完成的对象时可能导致数据不一致或程序崩溃。
  • 可见性问题 多个线程在访问半初始化对象时可能看到不一致的状态,导致程序逻辑错误。
  • 内存泄漏 半初始化对象可能导致内存泄漏,因为在垃圾回收时,半初始化对象可能被误认为是可达对象。

二、对象半初始化问题详解

1. Java对象创建过程

  • 分配内存空间:为新对象分配内存空间,通常在堆(Heap)中进行。此时,分配给对象的内存地址都是默认值,如0、false、null等。
  • 初始化对象:执行构造方法进行初始化,完成对象的状态设置。此阶段会按照程序员的意愿设置对象的属性值。
  • 堆内存中设置引用地址:将对象的引用地址设置到变量上,使得程序可以访问和操作对象。

2. 对象半初始化问题产生的原因

  • Java内存模型:Java内存模型允许指令的重排序,即构造函数的执行顺序可以与代码编写的顺序不一致。这是为了提高处理器和编译器的执行效率。然而,在多线程环境下,这种优化可能导致对象半初始化问题。
  • 重排序导致的问题:由于指令重排序,可能导致一个线程看到了另一个线程创建的对象的引用地址,但这个对象还没有完成初始化,从而导致对象半初始化问题。以下是一个简单的例子:
public class Example {
    private static Example instance;
    private int value;

    private Example() {
        value = 10;
    }

    public static Example getInstance() {
        if (instance == null) {
            instance = new Example();
        }
        return instance;
    }

    public int getValue() {
        return value;
    }
}

Example 类是一个单例类,通过 getInstance() 方法获取唯一的实例。由于指令重排序,可能出现以下执行顺序:

  1. 分配内存空间
  2. 堆内存中设置引用地址(此时 instance 不为 null 了,但对象还没有完成初始化)
  3. 初始化对象

在多线程环境下,如果线程A在执行getInstance()方法时,在第2步和第3步之间,线程B也开始执行getInstance()方法。此时,线程B发现instance 不为 null,于是直接返回instance,但此时instance指向的对象尚未完成初始化。这样,线程B就会访问半初始化的对象。

为了避免这种情况,可以使用双重检查锁定(Double-Checked Locking)的方法:

public static Example getInstance() {
    if (instance == null) {
        synchronized (Example.class) {
            if (instance == null) {
                instance = new Example();
            }
        }
    }
    return instance;
}

但是,这种方法仍然可能会遇到对象半初始化问题。为了彻底解决该问题,需要使用volatile关键字:

private static volatile Example instance;

使用volatile关键字可以禁止指令重排序,确保对象在分配内存空间、初始化对象和设置堆内存中的引用地址的顺序不被打乱。这样就可以避免对象半初始化问题。

三、实例分析:对象半初始化问题的表现

1. 单线程环境下的半初始化

在单线程环境下,由于没有并发操作,对象的初始化通常是按照预定的顺序进行的,因此很少出现半初始化的情况。但是,在某些特殊情况下,例如循环依赖或者异常处理中,仍然可能出现对象半初始化的问题。

例如,以下的代码中,由于构造函数中的自引用创建了一个循环依赖,导致对象在初始化过程中产生了一个半初始化的自引用。

public class Example {
    private Example self;

    public Example() {
        // 在对象初始化过程中创建一个自引用
        self = this;
    }

    public void check() {
        if (self != null) {
            System.out.println("Object is partially initialized!");
        }
    }
}

public static void main(String[] args) {
    Example example = new Example();
    example.check();
}

在这个例子中,check() 方法会打印出 “Object is partially initialized!”,因为当它被调用时,self 引用的对象还处在初始化过程中。

2. 多线程环境下的半初始化

在多线程环境下,由于线程的并发执行和指令的重排序,很容易导致对象半初始化的问题。尤其是在使用单例模式或者延迟初始化的时候,这种问题更加明显。

例如,以下的代码中,尽管使用了双重检查锁定,但是由于指令重排序,其他线程仍然可能看到一个半初始化的单例对象。

public class Singleton {
    private static Singleton instance;
    private SomeObject obj;

    private Singleton() {
        obj = new SomeObject();
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    public SomeObject getObj() {
        return obj;
    }
}

public static void main(String[] args) {
    // 创建多个线程并发获取单例对象
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            Singleton singleton = Singleton.getInstance();
            // 检查获取到的单例对象是否初始化完全
            if (singleton.getObj() == null) {
                System.out.println("Singleton is partially initialized!");
            }
        }).start();
    }
}

多个线程并发执行 getInstance() 方法,可能会看到一个半初始化的 Singleton 对象,即 obj 属性可能为 null

四、解决方案及防范措施

1. 禁止指令重排序

  • 使用final关键字:在多线程环境中,final关键字可以保证初始化完成的可见性,防止对象在被初始化完成之前被其他线程所引用。
  • 使用构造函数私有化:私有化构造函数可以强制用户通过特定的方式创建对象,从而防止用户在未完成初始化的情况下获取对象的引用。

2. 使用安全发布对象的模式

  • 使用不可变对象:不可变对象一旦被初始化,其状态就不能被改变,这样就可以保证没有其他线程可以看到对象在被初始化过程中的状态。
  • 使用volatile关键字:volatile关键字可以保证多线程环境下变量的可见性和禁止指令重排序,从而防止对象半初始化的问题。
  1. 在实际编程中,应尽量避免在构造函数中引用自身或暴露自身的引用,防止逸出引起的问题。

  2. 对于单例对象,可以通过内部类的方式来实现,既能保证线程安全,又能保证单例对象的唯一性。

例如:

public class Singleton {
    private Singleton() {
        // do something
    }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Singleton 的初始化被推迟到了 SingletonHolder 类被真正的加载时才执行,同时由 JVM 来保证线程安全和单一性。

5.参考文档

  1. 《Java Memory Model》https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html
  2. Java Tutorials - Concurrency.https://docs.oracle.com/javase/tutorial/essential/concurrency/
  3. 《java内部模型》https://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1050548.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java 基于 SpringBoot 的校园疫情防控系统

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝30W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 文章目录 1 简介2.主要技术3 需求分析4系统设计4.1功能结构4.2 数据库设计4.2.1 数据库E/R图4.2.2 数据库表…

预编译(1)

目录 预定义符号&#xff1a; 使用&#xff1a; 结果&#xff1a; 预编译前后对比&#xff1a; #define定义常量&#xff1a; 基本语法&#xff1a; 举例1&#xff1a; 结果&#xff1a; 预编译前后对比&#xff1a; 举例2&#xff1a; 预编译前后对比&#xff1a; 注…

嵌入式软硬件

在这里插入图片描述

APP渗透测试

APP反抓包突破 抓包失败分析 工具证书未配置 app不使用HTTP/S协议 反模拟器 1.使用真机进行抓包 2.用模拟器模拟真机 3.逆向删除反模拟器代码打包重新测试 反证书 SSL证书绑定分为单向校验和双向校验&#xff0c;单向校验就是客户端校验服务端的证书&#xff0c;双向…

回调函数的用途

一、函数指针 在讲回调函数之前&#xff0c;我们需要了解函数指针。 我们都知道&#xff0c;C语言的灵魂是指针&#xff0c;我们经常使用整型指针&#xff0c;字符串指针&#xff0c;结构体指针等 int *p1; char *p2; STRUCT *p3; //STRUCT为我们定义的结构体但是好像我们一…

【CTFHUB】SSRF绕过方法之靶场实践(二)

SSRF POST请求 提示信息&#xff1a; 这次是发一个HTTP POST请求.对了.ssrf是用php的curl实现的.并且会跟踪302跳转.加油吧骚年 首先测试了http的服务请求&#xff0c;出现对话框 输入数值后提示&#xff1a;只能接受来自127.0.0.1的请求 右键查看源码发现key值 通过file协…

k8s+kubeedge+sedna安装的全套流程

一&#xff0c;环境准备 把两台虚拟机的ip地址设置成静态的IP地址&#xff0c;否则ip地址会变 虚拟机配置静态IP&#xff08;NAT模式&#xff09;_nat子网的准入_阿祖&#xff0c;收手吧的博客-CSDN博客​​​​​​ 节点IP软件 云节点192.168.133.139kubernetescloudcore边…

与初至波相关的常见误解

摘要: 初至波是指检波器首次接收到的波. 对它的误解会使我们失去重要的信息. 1. 波从震源到检波器的传导过程 从震源产生波以后, 有些波通过地面直接传导到检波器, 这些称为直达波 (面波);有些在地层中传播,遇到两种地层的分界面时 产生波的反射,在原来地层中形成一种新波, …

【算法——双指针】LeetCode 15 三数之和

题目描述&#xff1a; 解题思路&#xff1a;双指针 首先&#xff0c;按升序对数组进行排序。然后&#xff0c;我们可以用如下步骤求解&#xff1a; 初始化一个空的结果集result&#xff0c;用于存储找到的和为0的三元组。 遍历整个数组&#xff0c;直到倒数第三个元素&#xff…

基于APP数据爬取的运行环境

前提 数据爬取本就是“道高一尺&#xff0c;魔高一丈”&#xff1b;越往后&#xff0c;爬取越接近于真实&#xff0c;真实包含了真实的运行环境&#xff08;不再是简单地伪造请求、User-Agent和Cookie等&#xff09;和真实的操作流程。本文对APP的运行环境做了简单梳理以供参考…

python爬虫基于管道持久化存储操作

文章目录 基于管道持久化存储操作scrapy的使用步骤1.先转到想创建工程的目录下&#xff1a;cd ...2.创建一个工程3.创建之后要转到工程目录下4.在spiders子目录中创建一个爬虫文件5.执行工程setting文件中的参数 基于管道持久化存储的步骤&#xff1a;持久化存储1&#xff1a;保…

【Linux学习】05-1Linux上安装部署各类软件

Linux&#xff08;B站黑马&#xff09;学习笔记 01Linux初识与安装 02Linux基础命令 03Linux用户和权限 04Linux实用操作 05-1Linux上安装部署各类软件 文章目录 Linux&#xff08;B站黑马&#xff09;学习笔记前言05-1Linux上安装部署各类软件JDK安装部署Tomcat安装部署maven…

Matlab绘图函数subplot、tiledlayout、plot和scatter

一、绘图函数subplot subplot(m,n,p)将当前图窗划分为 mn 网格&#xff0c;并在 p 指定的位置创建坐标区。MATLAB按行号对子图位置进行编号。第一个子图是第一行的第一列&#xff0c;第二个子图是第一行的第二列&#xff0c;依此类推。如果指定的位置已存在坐标区&#xff0c;…

ahk系列——ahk_v2实现win10任意界面ocr

前言&#xff1a; 不依赖外部api接口&#xff0c;界面简洁&#xff0c;翻译快速&#xff0c;操作简单&#xff0c; 有网络就能用 、还可以把ocr结果非中文翻译成中文、同样可以识别中英日韩等60多个国家语言并翻译成中文&#xff0c;十分的nice 1、所需环境 windows10及其以上…

【小沐学C++】C++ 基于Premake构建工程项目(Windows)

文章目录 1、简介2、下载和安装2.1 下载2.3 快速入门 3、使用3.1 支持的工程文件Project Files3.2 构建设置Build Settings3.3 链接Linking3.4 配置Configurations3.5 平台Platforms3.6 过滤Filters3.7 预设值Tokens 4、测试4.1 测试1&#xff1a;入门例子4.2 测试2&#xff1a…

STL upper_bound和lower_bound函数

声明&#xff1a; 首先包含头文件#include<algorithm> 这里的两个函数所运用的对象必须是非递减的序列&#xff08;也就是数组&#xff0c;数组必须是非递减的&#xff09;&#xff0c;只有这样才可以使用upper_bound和lower_bound这两个函数。 还有一点&#xff0c;就…

手摸手带你 在Windows系统中安装Istio

Istio简介 通过负载均衡、服务间的身份验证、监控等方法&#xff0c;Istio 可以轻松地创建一个已经部署了服务的网络&#xff0c;而服务的代码只需很少更改甚至无需更改。 通过在整个环境中部署一个特殊的 sidecar 代理为服务添加 Istio 的支持&#xff0c;而代理会拦截微服务…

【C++】string 之 find、rfind、replace、compare函数的学习

前言 上篇文章&#xff0c;我们学习了assign、at、append这三个函数 今天&#xff0c;我们来学习find、 函数 find函数 引入 我们都知道&#xff0c;find函数可以是string类中&#xff0c;用于查找字符或者字符串的函数 也可以是&#xff0c;<algorithm>头文件中&am…

帝国CMS插件-最全免费帝国CMS插件大全

在如今的数字时代&#xff0c;网站管理员和内容创作者面临着一个共同的挑战&#xff1a;如何快速而有效地生成并发布大量内容&#xff0c;以吸引更多的用户和读者。帝国CMS&#xff08;Empire CMS&#xff09;是一个备受欢迎的内容管理系统&#xff0c;许多网站使用它来创建和管…

3.物联网射频识别,RFID

一。ISO14443-2协议简介 1.ISO14443协议组成及部分缩略语 &#xff08;1&#xff09;14443协议组成 14443-1 物理特性 14443-2 射频功率和信号接口 14443-3 初始化和防冲突 14443-4 传输协议 &#xff08;2&#xff09;针对前两节的名词&#xff0c;对应的英语缩略语 PCD&…