资讯专栏INFORMATION COLUMN

设计模式--单例模式

leanote / 779人阅读

摘要:双重检查锁单例模式懒汉单例模式中,我们并不需要整个方法都是同步的,我们只需要确保再创建的时候,进行同步即可。单例模式的缺点优点在开头已经说明了,单例模式的缺点在于它一般没有接口,扩展困难,基本上修改源代码是扩展单例模式的唯一方法。

单例模式 定义:

确保某一个类只有一个实例对象,并且该对象是自行实例化的,通过统一的接口向整个系统提供这个实例对象。

使用场景:

避免产生多个对象消耗过多的资源(比如该对象需要用到IO,Database等等),或者某个类的实例化对象应该只有一个的情况。

因为内存中只有一个实例对象的存在,减少了内存开支,同时,如果该对象的产生需要较多资源的时候(内部需要依赖其他对象...),我们可以采取只生成一个对象,然后让这个对象永久驻留在内存中的方式实现。

如果需要定义大量的静态常量和静态方法,也可以采用单例模式实现。

关键点:

1.构造函数不对外开放,一般为private。

2.通过一个static方法或者枚举返回给外部单例对象。

3.在多线程的条件下也能保证只有一个单例对象。

4.确保单例类对象再反序列化的时候不会创建新的对象。

实现方式: 1.饿汉单例模式
public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton(){}

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

优点:实现简单,在类加载的时候完成了初始化工作,避免了多线程同步问题。

缺点:没有实现懒加载,如果这个单例对象没有被使用过,但是对应的类却加载到内存中的话,也会白白的占用不必要的内存。

2.懒汉单例模式
public class Singleton{
    private static Singleton instance = null;
    
    private Singleton(){}
    
    public static synchronized Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

优点:实现了懒加载,在用到单例对象的时候再对其进行初始化,一定程度上节约了资源。

缺点:getInstance挂了一把锁,每次获取这个单例对象都需要同步,不管是不是并发情况下,都会早成不必要的同步开销。

3.DCL双重检查锁单例模式

懒汉单例模式中,我们并不需要整个getInstance方法都是同步的,我们只需要确保再instance创建的时候,进行同步即可。

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

优点:线程安全,懒加载,执行效率高,只有在instance为null的时候才会有同步开销。

缺点:

Double-Checked Lock看起来是非常完美的。但是根据Java的语言规范,上面的代码并非绝对可靠。
出现上述问题, 最重要的2个原因如下:

1, 编译器优化了程序指令, 以加快cpu处理速度.
2, 多核cpu动态调整指令顺序,允许指令乱序执行, 以加快并行运算能力.

问题出现的顺序:

1, 线程A, 发现对象未实例化, 准备开始实例化

2, 由于编译器优化了程序指令, 允许对象在构造函数未调用完前, 将共享变量的引用指向部分构造的对象, 虽然对象未完全实例化, 但已经不为null了.

3, 线程B, 发现部分构造的对象已不是null, 则直接返回了该对象(此时它为null本应该先创建再返回却直接返回了)。

通俗来说,如果线程A的指令发现instance为null,则会去执行初始化的指令,初始化指令最终翻译成汇编指令可能是如下三个部分:

①为内存对象分配内存

②构造函数初始化成员字段

③将创建的对象指定到分配的内存空间中

如果123顺序执行是没有问题的,但是可能存在132乱序执行的情况,如果3执行完成,CPU切换到了另一个线程,同样执行getInstance方法去获取单例对象,单例对象不为空,但是获取到的对象确实不正确的。

这就是DCL失效问题。

改进的办法是,为instance加上volatile修饰符,保证对其修改其它线程立即可见。

private volatile static Singleton instance = null;

虽然volatile又需要额外的性能开销,但是相比安全性,这个开销是值得的。

静态内部类单例模式
public class Singleton{
    private Singleton(){}
    
    public static Singleton getInstance(){
        return SingletonHolder.sInstance;
    }
    
    private static class SingletonHolder{
        private static final Singleton sInstance = new Singleton();
    }
}

根据类加载机制,对于内部类而言,只有再需要的时候才会加载,也就是说位于SingletonHolder中的sInstance只有在第一次调用到getInstance的时候,才会被创建,从而既实现了懒加载,也能够确保线程安全(由JVM确保,在类加载的时候,只有一个线程会执行类加载动作,也就是创建单例对象只会由一个线程完成),推荐使用。

枚举单例
public class EnumSingleton{
    private EnumSingleton(){}
    public static EnumSingleton getInstance(){
        return Singleton.INSTANCE.getInstance();
    }
    
    private static enum Singleton{
        INSTANCE;
        private EnumSingleton singleton;
        // 在加载的时候进行初始化,JVM保证该方法只会被调用一次。
        private Singleton(){
            singleton = new EnumSingleton();
        }
        public EnumSingleton getInstance(){
            return singleton;
        }
    }
}

枚举类和普通类是一样的,但是不同的是枚举实例的创建默认是线程安全的,并且在任何情况下都是只有一个实例对象存在,即便是序列化反序列化也是。

单例模式对(反)序列化的改进

上面所有的单例模式,除了借助枚举来实现外,都存在一个缺点,也就是第四个关键点,我们需要保证单例对象在序列化和反序列化中可以保证对象的一致性,也就是不能通过反序列化违反单例的系统中只存在一个唯一对象的规定。

当然,这个情况的前提是,我们的单例类实现了序列化接口。

通过类的readResolve函数,开发人员可以控制反序列化过程,杜绝在反序列化的时候生成新对象:

public final class Singleton implements Serializable{
    private static final long serialVersionUID = 0L;
    private static final Singleton INSTANCE = new Singleton();
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        return INSTANCE;
    }
    
    private Object readResolve(){
        return INSTANCE;
    }
}

同样的,该方法因为需要用到序列化,自然是要符合序列化的要求,即内部字段也是要可序列化的。

我们将serialVersionUID置为fianl,是为了保证在修改了单例类的内部情况的时候,反序列化也不会抛出InvalidClassException异常,只会将新修改的字段置为默认值。

单例模式的缺点:

优点在开头已经说明了,单例模式的缺点在于它一般没有接口,扩展困难,基本上修改源代码是扩展单例模式的唯一方法。再有,如果单例对象持有Context,很容易引发内存泄露问题,所以一般是用ApplicationContext。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/14969.html

相关文章

  • Android中的设计模式单例模式

    摘要:总结单例是运用频率很高的模式,因为客户端没有高并发的情况,选择哪种方式并不会有太大的影响,出于效率考虑,推荐使用和静态内部类实现单例模式。 单例模式介绍 单例模式是应用最广的模式之一,也可能是很多人唯一会使用的设计模式。在应用单例模式时,单例对象的类必须保证只用一个实例存在。许多时候整个系统只需要一个全局对象,这样有利于我么能协调整个系统整体的行为。 单例模式的使用场景 确保某个类有且...

    罗志环 评论0 收藏0
  • Android中的设计模式单例模式

    摘要:总结单例是运用频率很高的模式,因为客户端没有高并发的情况,选择哪种方式并不会有太大的影响,出于效率考虑,推荐使用和静态内部类实现单例模式。 单例模式介绍 单例模式是应用最广的模式之一,也可能是很多人唯一会使用的设计模式。在应用单例模式时,单例对象的类必须保证只用一个实例存在。许多时候整个系统只需要一个全局对象,这样有利于我么能协调整个系统整体的行为。 单例模式的使用场景 确保某个类有且...

    yzd 评论0 收藏0
  • JavaScript设计模式----单例模式

    摘要:不符合设计模式中的单一职责的概念。引入代理实现单例模式引入代理实现单例模式的特点我们负责管理单例的逻辑移到了代理类中。的单例模式对比在以上的代码中实现的单例模式都混入了传统面向对象语言的特点。 声明:这个系列为阅读《JavaScript设计模式与开发实践》 ----曾探@著一书的读书笔记 1.单例模式的特点和定义 保证一个类仅有一个实例,并且提供一个访问它的全局访问点。 2.传统面向对...

    selfimpr 评论0 收藏0
  • JavaScript设计模式-第一部分:单例模式、组合模式和外观模式

    摘要:但是,这并不是采用单例的唯一原因。使用命名空间单例模式也被称为模块设计模式。函数内部声明了一些局部函数和或变量。紧随函数声明放置即可立即执行外部函数,并将所得的对象文字费赔给变量。 JavaScript设计模式-第一部分:单例模式、组合模式和外观模式 设计模式是一些可靠的编程方式,有助于保证代码更加易于维护、扩展及分离,所有设计模式在创建大型JavaScript应用程序时均不可或缺 单...

    betacat 评论0 收藏0
  • Java设计模式-单例模式(Singleton Pattern)

    摘要:如果需要防范这种攻击,请修改构造函数,使其在被要求创建第二个实例时抛出异常。单例模式与单一职责原则有冲突。源码地址参考文献设计模式之禅 定义 单例模式是一个比较简单的模式,其定义如下: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。 或者 Ensure a class has only one instance, and provide a global point of ac...

    k00baa 评论0 收藏0
  • Java 设计模式单例模式

    摘要:在设计模式一书中,将单例模式称作单件模式。通过关键字,来保证不会同时有两个线程进入该方法的实例对象改善多线程问题为了符合大多数程序,很明显地,我们需要确保单例模式能在多线程的情况下正常工作。 在《Head First 设计模式》一书中,将单例模式称作单件模式。这里为了适应大环境,把它称之为大家更熟悉的单例模式。 一、了解单例模式 1.1 什么是单例模式 单例模式确保一个类只有一个实例,...

    everfight 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<