单例模式--确保对象唯一性

单例模式的动机

为什么需要单例模式呢?对于一个软件系统的某些类而言, 我们无须创建多个实例。 举个大家都熟知的例子——Windows任务管理器, 如图3-1所示, 我们可以做一个这样的尝试, 在Windows的“任务栏”的右键弹出菜单上多次点击“启动任务管理器”, 看能否打开多个任务管理器窗口?( 注: 电脑中毒或私自修改Windows内核者除外) 。

常情况下, 无论我们启动任务管理多少次, Windows系统始终只能弹出一个任务管理器窗口,也就是说在一个Windows系统中, 任务管理器存在唯一性。 为什么要这样设计呢? 我们可以从以下两个方面来分析: 其一, 如果能弹出多个窗口, 且这些窗口的内容完全一致, 全部是重复对象, 这势必会浪费系统资源, 任务管理器需要获取系统运行时的诸多信息, 这些信息的获取需要消耗一定的系统资源, 包括CPU资源及内存资源等, 浪费是可耻的, 而且根本没有必要显示多个内容完全相同的窗口; 其二, 如果弹出的多个窗口内容不一致, 问题就更加严重了, 这意味着在某一瞬间系统资源使用情况和进程、 服务等信息存在多个状态, 例如任务管理器窗口A显示“CPU使用率”10%, 窗口B显示“CPU使用率”为15%, 到底哪个才是真实的呢?
由此可见, 确保Windows任务管理器在系统中有且仅有一个非常重要。

回到实际开发中, 我们也经常遇到类似的情况, 为了节约系统资源, 有时需要确保系统中某个类只有唯一一个实例, 当这个唯一实例创建成功之后, 我们无法再创建一个同类型的其他对象, 所有的操作都只能基于这个唯一实例。 为了确保对象的唯一性, 我们可以通过单例模式来实现, 这就是单例模式的动机所在。

单例模式的结构

单例模式是结构最简单的设计模式一, 在它的核心结构中只包含一个被称为单例类的特殊类。 单例模式结构如图所示:
在这里插入图片描述
单例模式结构图只包含一个单例角色:

  • Singleton(单例):在单例类的内部实现只生成一个实例, 同时它提供一个静态的
    getInstance()工厂方法, 让客户可以访问它的唯一实例; 为了防止在外部对其实例化, 将其构造函数设计为私有; 在单例类内部定义了一个Singleton类型的静态对象, 作为外部共享的唯一实例。

单例模式的实现方式

饿汉单例
线程安全,安全是由类加载机制保证

public class Singleton {
    private static final   Singleton s = new Singleton();
    private Singleton(){}
    
    public static Singleton getInstance(){
        return s;
    }

    public static void main(String[] args){
    Singleton singleton = Singleton.getInstance();
    }
}

懒汉单例
懒汉单例模式和饿汉单例模式的区别就是:懒汉创建了延迟对象,同时饿汉式的单例对象是被修饰为final类型。

  • 优点:尽最大可能节省内存空间
  • 缺点:在多线程编程中,使用懒汉式可能造成类的对象在内存中不唯一,虽然用过修改代码可以改正这些问题,但是降低了效率,线程不安全。
public class Singleton {
    private static    Singleton s = null;
    private Singleton(){}

    public static Singleton getInstance(){
        //两个线程同时进行if(s==null)判断,则都会进入if条件吗?
        if(s==null) 
            s = new Singleton();
        return  s;
    }
}

全局锁式
线程安全,线程同步时效率不高(synchronized)

public class Singleton {
    private static  Singleton singleton;
    private Singleton(){}

    //synchronized修饰的是静态方法,锁住类对象
    public synchronized static Singleton getInstance(){
        if(singleton == null)
            singleton = new Singleton();
        return singleton;
    }
}

静态代码块
线程安全,类主动加载时才初始化实例,实现了懒加载策略,且线程安全。

public class Singleton {
    private final  static Singleton singleton;
    private Singleton(){}

    static {
        singleton = new Singleton();
    }

    public static Singleton getInstance(){
        //使用前将singleton属性通过静态代码块实现
        return singleton;
    }

    public static void main(String[] args){
        //第一次调用Singleton,JVM需要负责将其加载到内存中,在加载过程处理静态代码块
        Singleton.getInstance();
        //第二次调用Singleton,JVM中已经存在Singleton,直接使用getInstance
        Singleton.getInstance();
    }
}

双重校验锁式
线程安全,且实现了懒加载策略,同时保证了线程同步时的效率。但是volatile强制当前线程每次读操作进行时,保证其他所有线程写操作已完成。volatile使得JVM内部的编译器舍弃了编译时优化,对于性能有一定的影响

public class Singleton {
    private static  Singleton singleton;
    private Singleton(){}
    
    public  static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    //内层if判断使用的时间(起作用时机)
                    //第一次两线程同时调用getInstance,都会进入外层if判断
                    //内层if判断是针对第二个人进入synchronized代码块线程,此时第一个线程已经创建出对象
                    //第二个线程无需创建
                    singleton = new Singleton();
                }
            }
        }
        return  singleton;
    }
}

静态内部类式(推荐)
线程安全,不存在线程同步问题,且单例对象在程序第一次getInstance()时主动加载SingletonHolder 和 静态成员INSTANCE

public class Singleton {
    private Singleton(){}

    private static class SingletonHolder{
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

枚举式
线程安全,不存在线程同步问题,且单例对象在枚举类型INSTANCE,第一次引用时通过枚举的 构造函数 初始化
这种方式是Effective Java 书籍提倡的方式。它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象

public class Singleton {
    private Singleton(){}

   enum SingletonEnum{
        INSANCE;
        
        private final Singleton singleton;
        private SingletonEnum(){
            singleton = new Singleton();
        }
   }
   
   public static Singleton getInstance(){
        return SingletonEnum.INSANCE.singleton;
   }
}

单例模式总结

单例模式作为一种目标明确、 结构简单、 理解容易的设计模式, 在软件开发中使用频率相当高, 在很多应用软件和框架中都得以广泛应用。

优点:

  • 单例模式提供了对唯一实例的受控访问。 因为单例类封装了它的唯一实例, 所以它可以严格控制客户怎样以及何时访问它。
  • 由于在系统内存中只存在一个对象, 因此可以节约系统资源, 对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
  • 允许可变数目的实例。 基于单例模式我们可以进行扩展, 使用与单例控制相似的方法来获得指定个数的对象实例, 既节省系统资源, 又解决了单例单例对象共享过多有损性能的问题。
    缺点:
  • 由于单例模式中没有抽象层, 因此单例类的扩展有很大的困难。
  • 单例类的职责过重, 在一定程度上违背了“单一职责原则”。 因为单例类既充当了工厂角色, 提供了工厂方法, 同时又充当了产品角色, 包含一些业务方法, 将产品的创建和产品的本身的功能融合到一起。
  • 现在很多面向对象语言(如Java、 C#)的运行环境都提供了自动垃圾回收的技术, 因此, 如果实例化的共享对象长时间不被利用, 系统会认为它是垃圾, 会自动销毁并回收资源, 下次利用时又将重新实例化, 这将导致共享的单例对象状态的丢失。
    适用场景:
  • 系统只需要一个实例对象, 如系统要求提供一个唯一的序列号生成器或资源管理器, 或者需要考虑资源消耗太大而只允许创建一个对象。
  • 客户调用类的单个实例只允许使用一个公共访问点, 除了该公共访问点, 不能通过其他途径访问该实例。
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

降温vae+

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值