网上结论:
我们先来看看网上普遍的结论:
所谓“懒汉式”与“饿汉式”的区别,是在与建立单例对象的时间的不同。
“懒汉式”是在你真正用到的时候才去建这个单例对象
“饿汉式是在类创建的同时就已经创建好一个静态的对象,不管你用的用不上,一开始就建立这个单例对象
先不说结论,看看下文
代码实现:
饿汉式
public class Singleton1 {
private final static Singleton1 singleton = new Singleton();
private Singleton1() {
System.out.println("饿汉式单例初始化!");
}
public static Singleton1 getSingleton () {
return singleton;
}
}
复制代码
在类静态变量里直接new一个单例
懒汉式
public class Singleton2 {
private volatile static Singleton2 singleton; // 5
private Singleton2() {
System.out.println("懒汉式单例初始化!");
}
public static Singleton2 getInstance () {
if(singleton ==null) { // 1
synchronized(Singleton2.class) { // 2
if(singleton == null) { // 3
singleton = new Singleton2(); //4
}
}
}
return singleton;
}
}
复制代码
代码1 处的判空是为了减少同步方法的使用,提高效率
代码2,3 处的加锁和判空是为了防止多线程下重复实例化单例。
代码5 处的volatile是为了防止多线程下代码4 的指令重排序
测试方法
创建一个Test测试类
public class Test {
public static void main(String[] args) throws IOException {
// 懒汉式
Singleton1 singleton1 = Singleton1.getInstance();
// 饿汉式
Singleton2 singleton2 = Singleton2.getInstance();
}
}
复制代码
运行结果
从结果上看没啥毛病,那我们来加个断点试试。按照以往的认知,饿汉单例是在类加载的时候的实例化,那么运行main方法应该会输出饿汉单例的初始化,我们来看看结果:
public static void main(String[] args) throws IOException {
System.in.read();
// 饿汉式
Singleton1 singleton1 = Singleton1.getInstance();
// 懒汉式
Singleton2 singleton2 = Singleton2.getInstance();
}
复制代码
此时运行结果:
如图是没有结果的,饿汉单例怎么没有实例化呢?原来饿汉单例是在本类加载的时候才实例化的,在断点的时候还没有加载饿汉单例。 我们来详细复习一下类加载:
类的加载分为5个步骤:加载、验证、准备、解析、初始化
初始化就是执行编译后的< cinit>()方法,而< cinit>()方法就是在编译时将静态变量赋值和静态块合并到一起生成的。
所以说,“饿汉模式”的创建对象是在类加载的初始化阶段进行的,那么类加载的初始化阶段在什么时候进行呢?jvm规范规定有且只有以下7种情况下会进行类加载的初始化阶段:
- 使用new关键字实例化对象的时候
- 设置或读取一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候
- 调用一个类的静态方法的时候
- 使用java.lang.reflect包的方法对类进行反射调用的时候
- 初始化一个类的子类(会首先初始化父类)
- 当虚拟机启动的时候,初始化包含main方法的主类
- 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
综上,基本来说就是只有当你以某种方式调用了这个类的时候,它才会进行初始化,而不是jvm启动的时候就初始化,而jvm本身会确保类的初始化只执行一次。那如果不使用这个单例对象的话,内存中根本没有Singleton实例对象,也就是和“懒汉模式”是一样的效果。
当然,也有一种可能就是单例类里除了getInstance()方法还有一些其他静态方法,这样当调用其他静态方法的时候,也会初始化实例,但是这个很容易解决,只要加个内部类就行了:
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance () {
return LazyHolder.INSTANCE;
}
}
复制代码
总结
网上的结论普遍说单例过早占用资源,而推荐使用“懒汉模式”,但他们忽略了单例何时进行类加载,经过以上分析,“懒汉模式”实现复杂而且没有任何独占优点,“饿汉模式”完胜。“饿汉模式”使用场景推荐:
- 当单例类里有其他静态方法的时候,推荐使用静态内部类的形式。
- 当单例类里只有getInstance()方法的时候,推荐直接new一个静态的单例对象。
更新:
关于枚举类的:这里做个测试:
public enum SingletonEnum {
INSTANCE;
public SingletonEnum getInstance() {
return INSTANCE;
}
SingletonEnum() {
System.out.println("枚举类单例实例化啦");
}
public static void test() {
System.out.println("测试调用枚举类的静态方法");
}
}
复制代码
测试类:
public static void main(String[] args) throws IOException {
SingletonEnum.test();
System.in.read();
SingletonEnum singletonEnum=SingletonEnum.INSTANCE;
}
复制代码
由此得出结论,枚举类的单例和普通的“饿汉模式”一样,都是在类加载(调用静态方法)的时候初始化。但是枚举类的另一个优点是能预防反射和序列化,因此再次得出结论
- 当单例类里有其他静态方法的时候,推荐使用静态内部类的形式。
- 当单例类里只有getInstance()方法的时候,推荐直接new一个静态的单例对象。
- 当需要防止反射和序列化破坏单例的时候,推荐用枚举类的单例模式