单例bean与类加载过程
构造单例bean的方式有很多种,我们来看一下其中一种,饿汉式
public class Singleton1 implements Serializable {
//1、构造函数私有
private Singleton1() {
if (INSTANCE != null) {
throw new RuntimeException("单例对象不能重复创建");
}
System.out.println("private Singleton1()");
}
//2、创建静态常量对象,Instance
private static final Singleton1 INSTANCE = new Singleton1();
//3、使用getInstance()获取对象
public static Singleton1 getInstance() {
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
public Object readResolve() {
return INSTANCE;
}
}
其保证了单例bean的特性如下:
1、构造函数私有
2、创建静态常量对象,Instance
3、使用getInstance()获取对象
并且对单例bean被破坏进行了防范:
- 构造方法抛出异常是防止反射破坏单例
readResolve()是防止反序列化破坏单例
目前来看,都没什么问题,但是如果我想创建两个静态变量 a与b呢,并且在new的时候对a,b进行++,会发生什么?
代码如下:
public class Singleton1 implements Serializable {
//1、构造函数私有
private Singleton1() {
if (INSTANCE != null) {
throw new RuntimeException("单例对象不能重复创建");
}
//调用构造函数时会对a与b进行++;
a++;
b++;
System.out.println("private Singleton1()");
}
//2、创建静态常量对象,Instance
private static final Singleton1 INSTANCE = new Singleton1();
public static int a;
public static int b=0;
//3、使用getInstance()获取对象
public static Singleton1 getInstance() {
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
public Object readResolve() {
return INSTANCE;
}
}
这时如果我想对a与b进行输出,最终a、b的值会是多少?
public class TestCase {
public static void main(String[] args) {
Singleton1 instance = Singleton1.getInstance();
System.out.println("a="+instance.a+" b="+instance.b);
}
}
想必很多人会认为答案是a与b各自+1;输出 a=1 b=1;
但是真相却是:
private Singleton1()
a=1 b=0
这时为什么呢?
这就与类加载机制有关了,首先我们得知道类加载过程,在类初始化之前,会有一个链接阶段,其中有一个步骤叫做准备

而在准备阶段将会:
- 为类变量(static变量)分配内存并且设置该类变量的默认初始值,即零值
- 这里不包含用final修饰的static,因为final在编译的时候就会分配好了默认值,准备阶段会显式初始化
- 注意:这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
所以a与b在准备阶段被默认初始值为0;
接下来会进行类初始化,而类初始化的时机则有如下7种:
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(比如:Class.forName(“com.atguigu.Test”))
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
- JDK7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
很显然我们执行Singleton1 instance = Singleton1.getInstance();时,便对应上面第2点。所以会进行类初始化。
而在类初始化阶段也就是clinit():
初始化阶段就是执行类构造器方法
<clinit>()的过程此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。也就是说,当我们代码中包含static变量的时候,就会有clinit方法
<clinit>()方法中的指令按语句在源文件中出现的顺序执行<clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>())若该类具有父类,JVM会保证子类的
<clinit>()执行前,父类的<clinit>()已经执行完毕虚拟机必须保证一个类的
<clinit>()方法在多线程下被同步加锁
可以看到第3点,会跟据源文件种的出现顺序执行:
我们再看看最开始的代码中静态常量与变量的先后顺序:
private static final Singleton1 INSTANCE = new Singleton1();
public static int a;
public static int b=0;
相比大家已经恍然大悟,对于答案也呼之欲出了。
没错,当在准备阶段,a,b将会被赋默认值为0,而当我们调用getInstance()时,就会触发类加载的过程,按照源码的先后顺序,先执行new Singleton1(),将会对a与b进行++,所以a与b分别为1。之后继续顺序执行,int a;不会改变a的值,而b=0,则重新将b从1覆盖为0了。所以最终我们显示a=1 b=0;
那么如何解决呢?
没错!只要修改一下变量声明的顺序,将a与b声明在INSTANCE之前,就不会出现a,b数据不一致的问题了!
谢谢大家阅读,才疏学浅,望多多指教!
单例bean与类加载过程的更多相关文章
- Spring 源码学习 - 单例bean的实例化过程
本文作者:geek,一个聪明好学的同事 1. 简介 开发中我们常用@Commpont,@Service,@Resource等注解或者配置xml去声明一个类,使其成为spring容器中的bean,以下我 ...
- 【Spring源码分析】非懒加载的单例Bean初始化前后的一些操作
前言 之前两篇文章[Spring源码分析]非懒加载的单例Bean初始化过程(上篇)和[Spring源码分析]非懒加载的单例Bean初始化过程(下篇)比较详细地分析了非懒加载的单例Bean的初始化过程, ...
- Spring源码分析:非懒加载的单例Bean初始化前后的一些操作
之前两篇文章Spring源码分析:非懒加载的单例Bean初始化过程(上)和Spring源码分析:非懒加载的单例Bean初始化过程(下)比较详细地分析了非懒加载的单例Bean的初始化过程,整个流程始于A ...
- 【Spring源码分析】非懒加载的单例Bean初始化过程(下篇)
doCreateBean方法 上文[Spring源码分析]非懒加载的单例Bean初始化过程(上篇),分析了单例的Bean初始化流程,并跟踪代码进入了主流程,看到了Bean是如何被实例化出来的.先贴一下 ...
- Spring IOC 容器源码分析 - 创建单例 bean 的过程
1. 简介 在上一篇文章中,我比较详细的分析了获取 bean 的方法,也就是getBean(String)的实现逻辑.对于已实例化好的单例 bean,getBean(String) 方法并不会再一次去 ...
- Spring源码分析:非懒加载的单例Bean初始化过程(下)
上文Spring源码分析:非懒加载的单例Bean初始化过程(上),分析了单例的Bean初始化流程,并跟踪代码进入了主流程,看到了Bean是如何被实例化出来的.先贴一下AbstractAutowireC ...
- 【Spring源码分析】非懒加载的单例Bean初始化过程(上篇)
代码入口 上文[Spring源码分析]Bean加载流程概览,比较详细地分析了Spring上下文加载的代码入口,并且在AbstractApplicationContext的refresh方法中,点出了f ...
- Spring源码分析:非懒加载的单例Bean初始化过程(上)
上文[Spring源码分析]Bean加载流程概览,比较详细地分析了Spring上下文加载的代码入口,并且在AbstractApplicationContext的refresh方法中,点出了finish ...
- Spring源码解析之八finishBeanFactoryInitialization方法即初始化单例bean
Spring源码解析之八finishBeanFactoryInitialization方法即初始化单例bean 七千字长文深刻解读,Spirng中是如何初始化单例bean的,和面试中最常问的Sprin ...
- 5.2:缓存中获取单例bean
5.2 缓存中获取单例bean 介绍过FactoryBean的用法后,我们就可以了解bean加载的过程了.前面已经提到过,单例在Spring的同一个容器内只会被创建一次,后续再获取bean直接从单例 ...
随机推荐
- Scanner基础用法
Scanner基础用法 引入包java.util.Scanner 读一个单词 package charpter2; import java.util.Scanner; public class Sca ...
- day09-拦截器&文件上传
拦截器&文件上传 1.拦截器-Interceptor 1.1拦截器概念 拦截器 拦截器(Interceptor):是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行. ...
- 对一些常用RDD算子的总结
虽然目前逐渐sql化,但是掌握 RDD 常用算子是做好 Spark 应用开发的基础,而数据转换类算子则是基础中的基础,因此学习这些算子还是很有必要的. 这篇博客主要参考Spark官方文档中RDD编程一 ...
- 【JavaScript快速排序算法】不同版本原理分析
说明 快速排序(QuickSort),又称分区交换排序(partition-exchange sort),简称快排.快排是一种通过基准划分区块,再不断交换左右项的排序方式,其采用了分治法,减少了交换的 ...
- nginx中的proxy_pass配置
Nginx 是最常用的反向代理工具之一,一个指令 proxy_pass搞定反向代理,对于接口代理.负载均衡很是实用,但 proxy_pass指令后面的参数很有讲究,通常一个/都可能引发一个血案. 通常 ...
- python入门教程之二十三Python3 MySQL 数据库连接 - PyMySQL 驱动
MySQL 是最流行的关系型数据库管理系统,如果你不熟悉 MySQL,可以阅读我们的 MySQL 教程. 本章节我们为大家介绍使用 mysql-connector 来连接使用 MySQL, mysql ...
- Tkinter库的使用
from tkinter import *import tkinter as tkfrom tkinter import Tk, Label,ttkfrom PIL import Image, Ima ...
- LeeCode 92双周赛复盘
T1: 分割圆的最少切割次数 思维题: n 为偶数时,可以对半切割,切割 \(\frac{n}{2}\)次即可 n 为奇数时,不满足对称性,需要切割 n 次 n 为 1 时,不需要切割 public ...
- HTML中script 标签中的那些属性
在HTML中, <script> 标签用于嵌入或引用JavaScript代码. 在 <script> 标签中,有两个属性可以用来控制脚本的加载和执行方式: async 和 de ...
- C# 自定义并动态切换光标
系统有很多光标类型 :Cursors 类 (System.Windows.Input) | Microsoft Docs 本章介绍如何自定义光标.并动态切换光标类型. 动态切换光标类型 以白板书写为例 ...