Intro
众所周知,kotlin提供了专门的关键字实现单例,即object
object Test2 {
fun print() {
LogUtil.d("hhhh", "this = $this")
}
}
// use
Test2.print()
kotlin官方文档中写明了,object声明的单例是线程安全的且在首次访问时完成初始化

使用show kotlin Bytecode 看一下kotlin代码反编译后的结果
public final class Test2 {
@NotNull
public static final Test2 INSTANCE = new Test2();
private Test2() {
}
public final void print() {
LogUtil.d("hhhh", "this = " + this);
}
}
很明显标准的饿汉式单例实现方式,尤其是object 完全就是标准的饿汉式写法,只不过INSTANCE变量可见性是public的
那么懒汉式和饿汉式的区别是啥呢?
下面给出gpt的解答
懒汉式的单例模式是在第一次使用时创建实例,而不是在类加载时创建。这种方式避免了不必要的资源消耗,适合需要延迟初始化的情况。
饿汉式是在类加载时就创建实例,无论你是否使用该实例,都会在类加载时创建。这可能会提前占用资源。
好像和官方文档写的有些出入?我们具体探究一下饿汉式所谓的资源占用问题 即static变量在类加载时的内存占用问题
由此可知static变量在类加载过程中完成实例化,下面就看一下 类的生命周期
根据《Java虚拟机规范 Java SE8》版规定,类或者接口的加载过程分为加载(Loading)、链接(Linking)、初始化(Initialization) 。其中链接(Linking)又可以分为:验证(Verification)、准备(Preparation)、解析(Resolution)
而static变量的内存占用就发生在链接的准备阶段。Java虚拟机为各类型的静态变量赋零值或null。(注意如果是基本数据类型常量 (static final) 会直接赋默认值而非零值)
| 类型 | 默认初始值 |
|---|---|
| byte | (byte)0 |
| short | (short)0 |
| int | 0 |
| long | 0L |
| float | 0.0f |
| double | 0.0 |
| char | \u0000 |
| boolean | false |
| reference | null |
非基本数据类型即reference类型对象的初始化在clinit方法中,即初始化Initialization阶段; 以下是字节码,可以看到clinit方法中对象初始化然后赋值给static变量的过程
public final class com/android/gallery3d/vivo/cloud/originalhelper/Test2 {
// access flags 0x19
public final static Lcom/android/gallery3d/vivo/cloud/originalhelper/Test2; INSTANCE
@Lorg/jetbrains/annotations/NotNull;() // invisible
// access flags 0x8
static <clinit>()V
NEW com/android/gallery3d/vivo/cloud/originalhelper/Test2
DUP
INVOKESPECIAL com/android/gallery3d/vivo/cloud/originalhelper/Test2.<init> ()V
PUTSTATIC com/android/gallery3d/vivo/cloud/originalhelper/Test2.INSTANCE : Lcom/android/gallery3d/vivo/cloud/originalhelper/Test2;
RETURN
MAXSTACK = 2
MAXLOCALS = 0
}
Java 虚拟机规范没有强制约束类加载过程的第一阶段(即:加载)什么时候开始,但对于“初始化”阶段,有着严格的规定。有且仅有 5 种情况必须立即对类进行“初始化”:
1、在遇到 new、putstatic、getstatic、invokestatic 字节码指令时,如果类尚未初始化,则需要先触发其初始化。
2、对类进行反射调用时,如果类还没有初始化,则需要先触发其初始化。
3、初始化一个类时,如果其父类还没有初始化,则需要先初始化父类。
4、虚拟机启动时,用于需要指定一个包含
main()方法的主类,虚拟机会先初始化这个主类。5、当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类还没初始化,则需要先触发其初始化。
这 5 种场景中的行为称为对一个类进行主动引用,除此之外,其它所有引用类的方式都不会触发初始化,称为被动引用。
根据上述规范我们可以得出类的初始化一定是该类被主动引用时
至此,可以反推kotlin文档中说的object单例是线程安全和在第一次访问时初始化的正确性
实践验证
方式 通过jprofiler插件查看堆的实例化对象
下面给出2种不同模式的单例实现
// 饿汉式
public class SingletonEager {
private static final SingletonEager instance = new SingletonEager();
public static int i = 1;
public static final int j = 1;
private int[] largeArray; // 声明一个大数组
private String largeString; // 声明一个大字符串
private double[] additionalLargeArray; // 另一个大数组用于进一步增加内存占用
// 构造器用于初始化这些大属性
public SingletonEager() {
}
public static SingletonEager getInstance() {
return instance;
}
public void print() {
System.out.println("this SingletonEager");
}
public static void printStatic() {
System.out.println("this SingletonEager static");
}
}
// 懒汉式
public class SingletonLazy {
private static SingletonLazy instance;
public static int i = 1;
public static final int i1 = 129;
public static int i2 = 1;
public static int j = -1;
SingletonLazy() {
// private constructor to prevent instantiation
}
public static synchronized SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
public void print() {
System.out.println("this SingletonLazy");
}
public static void printStatic() {
System.out.println("this SingletonLazy static");
}
}
常规用法
public class Test {
public static void main(String[] args) {
// 被动引用
System.out.println(SingletonEager.class);
System.out.println(SingletonLazy.class);
// get StaticFinal 不会触发类加载,编译阶段直接替换常量,字节码可证
System.out.println(SingletonEager.j);
System.out.println(SingletonLazy.j);
// getStatic
System.out.println(SingletonEager.i);
System.out.println(SingletonLazy.i);
// invokeStatic
SingletonEager.printStatic();
SingletonLazy.printStatic();
// 反射
try {
Class.forName("SingletonEager");
Class.forName("SingletonLazy");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
// new
SingletonEager eager = new SingletonEager();
SingletonLazy lazy = new SingletonLazy();
// 常规用法
SingletonEager.getInstance().print();
SingletonLazy.getInstance().print();
// 避免程序执行完直接退出
try {
System.in.read();
} catch (IOException e) {
}
}
}
常规用法,发现2种单例模式均正常触发实例化

验证putStatic getStatic 反射 等形式触发类加载,结果如下,均触发了类加载,因未出发getInstance,所以只有饿汉式完成了实例化

new 方式触发类加载,结果如下,有2个饿汉式实例,1个懒汉式实例,多出来的一个饿汉式实例是类加载时触发的static变量初始化

验证SingletonEager.class的被动引用方式,未发现类的实例对象

static final 基础类型的常量的例外,也访问了static变量但是未触发类加载

从字节码可以看出来常量会在编译时就被替换,不会触发类加载;重点看L3位置方法,并未触发getStatic 因此未触发类加载
public static main([Ljava/lang/String;)V
TRYCATCHBLOCK L0 L1 L2 java/io/IOException
L3
LINENUMBER 10 L3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ICONST_1
INVOKEVIRTUAL java/io/PrintStream.println (I)V
L4
LINENUMBER 11 L4
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ICONST_M1
INVOKEVIRTUAL java/io/PrintStream.println (I)V
L0
LINENUMBER 40 L0
GETSTATIC java/lang/System.in : Ljava/io/InputStream;
INVOKEVIRTUAL java/io/InputStream.read ()I
POP
L1
LINENUMBER 43 L1
GOTO L5
L2
LINENUMBER 41 L2
FRAME SAME1 java/io/IOException
ASTORE 1
L5
LINENUMBER 44 L5
FRAME SAME
RETURN
L6
LOCALVARIABLE args [Ljava/lang/String; L3 L6 0
MAXSTACK = 2
MAXLOCALS = 2
}
结论
回到最初的问题,懒汉式所谓的延迟初始化只不过将对象实例化的过程从类加载初始化阶段延后到了类使用时的getInstance方法中,通过getInstance获取单例的用法,因此不会有显著区别
综上,kotlin object的单例是饿汉式单例,常规使用饿汉式单例和懒汉式单例无明显差异