java单例模式,其中的细节你注意到了吗

简介: 简介Singleton UML单例模式是应用最广的模式之一,它是为了确保某一个类在一个java虚拟机(进程)中有且只有一个实例存在.带来的效益:能够实现资源共享,避免由于资源操作时导致的性能或损耗.

简介

img_309fcad479c518fb65d3135c946e0c19.png
Singleton UML

单例模式是应用最广的模式之一,它是为了确保某一个类在一个java虚拟机(进程)中有且只有一个实例存在.

带来的效益:

  1. 能够实现资源共享,避免由于资源操作时导致的性能或损耗.
  2. 能够实现资源调度,方便资源之间的互相通信.
  3. 控制实例产生的数量,达到节约资源的目的.

缺陷 :

  1. 扩展性差,单例一般没有接口,要扩展只能修改单例类的代码.
  2. 避免在单例中持有生命周期比单例对象短的引用,容易引起内存泄漏.如Android中的Context对象,需要使用 Application Context代替.

下面介绍单例的七种经典实现方法.

饿汉模式

public class Singleton {
    // 静态变量初始化, 由于静态变量在类加载过程中,就会被初始化,且类加载又jvm保证线程安全.
    // 所以这种方式 是线程安全的
    private final static Singleton mInstance = new Singleton();

    // 构造函数私有化
    private Singleton() {
        // 判断存在则抛出异常, 为了避免反射调用,产生多个实例
        if (mInstance != null)
            throw new RuntimeException("instance exist");
    }

    public static Singleton getInstance() {
        return mInstance;
    }
}

饿汉模式将变量声明为静态,将在Singleton类被加载的时候,在cinit阶段进行创建对象,并且是线程安全的, 类加载过程由JVM来保证线程安全.

饿汉模式能否达到懒加载

我们知道饿汉模式的对象实例是在类加载(初始化阶段)的过程就被创建了,并且并不是所有的类都是在程序启动的时候就加载进内存,那么一个类在什么情况下会被加载或者初始化呢?

这在虚拟机规范中是有严格规定的,虚拟机规范指明 有且只有 五种情况必须立即对类进行初始化:

1 ) 遇到newgetstaticputstaticinvokestatic这四条字节码指令

2 ) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

3 ) 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

4 ) 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。

5 ) 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果,REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

--------- 引用自 <<深入理解Java虚拟机:JVM高级特性与最佳实践>>

在这五种情况中,其中2,3,4,5 在单例模式中,几乎不会遇到,这里暂不讨论.
我们来看第一种情况,提到的指令分别对应以下操作:

  1. 外部使用 new 创建该类的对象实例
  2. 类中的静态变量被外部读取或者设置
  3. 外部调用了 该类的静态方法

其中1, 我们把构造函数设置为 私有(需要提防 反射和反序列化),进本上不会产生问题.

对于2, 我们尽量要避免把变量(除单例变量外)设为静态且非私有(除非你确定在做什么,不然很可能出现内存浪费或者内存泄漏,毕竟静态变量生命周期和程序一样长).如果外部调用这样的类变量,将会触发改类初始化.

注释: 静态常量(final static修饰基础类型变量)的调用不会触发类的加载, 该常量会被加入被调用类的常量池中

对于3, 我们单例如果提供静态方法供外部使用,该静态方法被调用时,将也会进行单例类初始化.但是 静态方法,只能调用static变量,参数变量,以及局部变量,而静态变量在单例中基本上只有 单例本身会声明为静态变量, 总结起来就是, 这个静态方法基本只能达到 工具方法的作用,最好不要声明在单例中.

结论: 饿汉模式不能严格上实现懒加载,除非严格按照要求,不在单例中申明无关的静态变量和静态方法,将也能达到 懒加载的效果.

懒汉模式(线程不安全)

public class Singleton {
    private static Singleton sInstance = null;

    private Singleton() {
        // 防止反射调用,被创建出多个实例
        if (sInstance != null)
            throw new RuntimeException("instance exist");
    }
    
    // 调用时创建
    public static Singleton getInstance() {
        if (sInstance == null)
            sInstance = new Singleton();
        return sInstance;
    }
}

这种方式能实现懒加载的目的,并且没有加锁操作,因此线程不安全,减少了资源的消耗.

单线程模型下,推荐这种方式的单例, 在多线程模式下 强烈不推荐.

懒汉模式(线程安全)

public class Singleton {
    private static Singleton sInstance = null;

    private Singleton() {
        // 防止反射调用,被创建出多个实例
        if (sInstance != null)
            throw new RuntimeException("instance exist");
    }

    // 调用时创建
    public synchronized static Singleton getInstance() {
        if (sInstance == null)
            sInstance = new Singleton();
        return sInstance;
    }
}

这种方式这种方式能够达到 懒加载线程安全,但是 它锁住了 整个getInstance()方法,

对于读的操作if (sInstance == null),也进行了加锁,这样对性能有一定的影响.

因此,不大推荐这种方式.

DCL 双重检查锁模式

public class Singleton {
    // 声明为 volatile 是为了避免在多线程中,new对象时,指令重排,
    // 造成对象未创建,而判断为非空的情况
    private volatile static Singleton sInstance = null;

    private Singleton() {
        // 防止反射调用,被创建出多个实例
        if (sInstance != null)
            throw new RuntimeException("instance exist");
    }

    public synchronized static Singleton getInstance() {
        // 不加锁,判断是否为空, 在锁竞争的情况下,提高性能
        if (sInstance == null) {
            // 只有当为空的时候,加锁创建
            synchronized (Singleton.class) {
                if (sInstance == null)
                    sInstance = new Singleton();
            }
        }
        return sInstance;
    }
}

这种方式这种方式能够达到 懒加载线程安全, 并且没有懒汉模式模式的缺点.它只对''(即new对象)操作进行加锁,判断是否为空时,线程无需等待.

这里需要注意, sInstance必须声明为 volatile,不然达不到线程安全. 对象的创建可以拆分为 三条指令,如果对其指令重排就可能出现线程不安全的情况. 具体可以参考笔者的另一篇文章 深入理解 java volatile

因此,比较推荐这种写法.

静态内部类模式

public class Singleton {
    private Singleton() {
        // 防止反射调用,被创建出多个实例
        if (SingletonHolder.sInstance != null)
            throw new RuntimeException("instance exist");
    }

    // 当该静态方法被第一次调用时,SingletonHolder类被加载到内存,
    // 此时,其sInstance变量将会被创建,类加载由jvm保证线程安全
    public static Singleton getInstance() {
        return SingletonHolder.sInstance;
    }

    // 类加载时初始化,达到懒加载的目的.
    // 调用时才被创建
    private static class SingletonHolder {
        private final static Singleton sInstance = new Singleton();
    }
}

这种方式这种方式能够达到 懒加载线程安全.

能够实现懒加载,是因为,不管Singleton中存不存在其他静态变量或者静态方法,都不会影响到 内部静态类SingletonHolder, 只有当getInstance()方法调用时,内部静态类才会被加载,而类加载时,单例被创建实例化. (请对比饿汉模式)

饿汉模式与静态内部类模式对比

饿汉模式 要对类进行约束也能达到懒加载目的. (不适用多余的静态变量和静态方法).

静态内部类模式 不需要进行约束就能达到懒加载目的. 但是需要消耗一个内部类的资源来达到目的.(代价很小)

权衡两者, 推荐使用静态内部类方式.

枚举模式

public enum Singleton {
    INSTANCE;

    public void method() {
        // todo ...
    }
}

上述的单例方式都有两个致命的缺点, 不能完全保证单例在jvm中保持唯一性.

  1. 反射创建单例对象

解决方案 : 在构造上述中判断,当多于一个实例时,再调用构造函数,直接报错.

  1. 反序列化时创建对象

解决方案 : 使用readResolve()方法来避免此事发生.

这两种缺点虽然都有方式解决,但是不免有些繁琐.

枚举类天生有这些特性.而且实现单例相当简单.

关于枚举类型,能够实现 懒加载,线程安全,以及确保单例在jvm中保持唯一性.
请参考笔者的另一篇文章 java 枚举(enum) 全面解读

因此, 强力推荐 使用这种方式创建单例.

但是由于枚举的使用时,枚举类的装载和初始化时会有更多的时间和空间的成本, 它的实现比其他方式需要更多的内存空间,所以在Android这种受资源约束的设备中尽量避免使用枚举单例

总结

  1. 在单线程下,建议使用懒汉模式(线程不安全版)
  2. 多线程,且资源受限(Android),建议使用DCL静态内部类版本
  3. 其他情况,建议使用枚举方法

引用

  1. Android源码设计模式
  2. 深入理解java虚拟机_第二版(周志明)
目录
相关文章
|
3月前
|
设计模式 缓存 安全
Java设计模式的单例模式应用场景
Java设计模式的单例模式应用场景
45 4
|
1月前
|
设计模式 存储 负载均衡
【五】设计模式~~~创建型模式~~~单例模式(Java)
文章详细介绍了单例模式(Singleton Pattern),这是一种确保一个类只有一个实例,并提供全局访问点的设计模式。文中通过Windows任务管理器的例子阐述了单例模式的动机,解释了如何通过私有构造函数、静态私有成员变量和公有静态方法实现单例模式。接着,通过负载均衡器的案例展示了单例模式的应用,并讨论了单例模式的优点、缺点以及适用场景。最后,文章还探讨了饿汉式和懒汉式单例的实现方式及其比较。
【五】设计模式~~~创建型模式~~~单例模式(Java)
|
22天前
|
设计模式 安全 Java
Java 单例模式,背后有着何种不为人知的秘密?开启探索之旅,寻找答案!
【8月更文挑战第30天】单例模式确保一个类只有一个实例并提供全局访问点,适用于需全局共享的宝贵资源如数据库连接池、日志记录器等。Java中有多种单例模式实现,包括饿汉式、懒汉式、同步方法和双重检查锁定。饿汉式在类加载时创建实例,懒汉式则在首次调用时创建,后者在多线程环境下需使用同步机制保证线程安全。单例模式有助于提高代码的可维护性和扩展性,应根据需求选择合适实现方式。
29 1
|
25天前
|
SQL 设计模式 安全
Java编程中的单例模式深入解析
【8月更文挑战第27天】本文旨在探索Java中实现单例模式的多种方式,并分析其优缺点。我们将通过代码示例,展示如何在不同的场景下选择最合适的单例模式实现方法,以及如何避免常见的陷阱。
|
21天前
|
设计模式 安全 Java
Java编程中的单例模式深度解析
【8月更文挑战第31天】 单例模式,作为设计模式中的经典之一,在Java编程实践中扮演着重要的角色。本文将通过简洁易懂的语言,逐步引导读者理解单例模式的本质、实现方法及其在实际应用中的重要性。从基础概念出发,到代码示例,再到高级应用,我们将一起探索这一模式如何优雅地解决资源共享和性能优化的问题。
|
21天前
|
设计模式 安全 Java
Java中的单例模式:理解与实践
【8月更文挑战第31天】在软件设计中,单例模式是一种常用的设计模式,它确保一个类只有一个实例,并提供一个全局访问点。本文将深入探讨Java中实现单例模式的不同方法,包括懒汉式、饿汉式、双重校验锁以及静态内部类等方法。每种方法都有其适用场景和潜在问题,我们将通过代码示例来展示如何根据具体需求选择合适的实现方式。
|
21天前
|
设计模式 安全 Java
Java编程中的单例模式实现与应用
【8月更文挑战第31天】在Java的世界里,单例模式是构建高效且资源友好应用的基石之一。本文将深入浅出地介绍如何通过单例模式确保类只有一个实例,并提供一个全局访问点。我们将探索多种实现方法,包括懒汉式、饿汉式和双重校验锁,同时也会讨论单例模式在多线程环境下的表现。无论你是Java新手还是资深开发者,这篇文章都将为你打开一扇理解并有效应用单例模式的大门。
|
1月前
|
设计模式 SQL 缓存
Java编程中的设计模式:单例模式的深入理解与应用
【8月更文挑战第22天】 在Java的世界里,设计模式是构建可维护、可扩展和灵活的软件系统的基石。本文将深入浅出地探讨单例模式这一经典设计模式,揭示其背后的哲学思想,并通过实例演示如何在Java项目中有效运用。无论你是初学者还是资深开发者,这篇文章都将为你打开一扇洞悉软件设计深层逻辑的大门。
26 0
|
2月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
60 1
|
1月前
|
设计模式 SQL 安全
单例模式大全:细说七种线程安全的Java单例实现,及数种打破单例的手段!
设计模式,这是编程中的灵魂,用好不同的设计模式,能使你的代码更优雅/健壮、维护性更强、灵活性更高,而众多设计模式中最出名、最广为人知的就是Singleton Pattern单例模式。通过单例模式,我们就可以避免由于多个实例的创建和销毁带来的额外开销,本文就来一起聊聊单例模式。