设计模式——单例模式(Singleton Pattern)

设计模式——单例模式(Singleton Pattern)

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
在有些场景中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例,且该类能自行创建这个实例,这就是所谓的单例模式


文章目录

  • 设计模式——单例模式(Singleton Pattern)
  • 单例模式的优缺点
      • 优点
      • 缺点
  • 单例模式的应用场景
  • 单例模式与静态类的区别
  • 常见的7种实现方式
      • 1.饿汉模式(线程安全)
      • 2.懒汉模式(线程不安全)
      • 3.懒汉模式(使用synchronized 同步)
      • 4.懒汉模式(双重锁定检查方式即:Double-Check-Lock)
      • 5.静态内部类方式
      • 6.使用ThreadLocal实现方式
      • 7.使用CAS锁(AtomicReference)来实现
  • 总结


单例模式的优缺点

优点

1.由于单例类封装了它的唯一实例,且对外只提供一个访问该单例的全局访问点,因此可以严格控制用户应该如何访问它。
2.单例模式保证内存中只有一个实例,避免了一些场景下,对象的频繁创建和销毁,减小内存开销,提高性能。
3.通过设置、修改全局访问点,可以优化对共享资源的访问。甚至可以基于单例模式进行拓展,允许可变数目的实例,既节省系统资源,又解决了单例模式下对象共享过多有损性能的问题。

缺点

1.单例模式没有抽象层,扩展困难,要进行扩展只能修改原有代码,违背“开闭原则”。
2.单例类职责过重,所有功能代码通常都写在一个类中。在一定程度上违背了‘单一职责的原则’。因为单例类既承担了工厂的角色,提供了工厂方法,又充当了产品的角色,包含了一些业务方法,将产品的创建和产品本身的功能融合到一起。


单例模式的应用场景

1.Windows的Task(任务管理器) 就是很经典的单例模式。一台电脑正常情况下只能打开一个任务管理器。
2.网站的计数器,一般也采用单例模式实现,否则难以同步。
应用程序的日志应用,一般都才会用单例模式实现,这通常是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
3.数据库连接池的设计一般也是采用单例模式,因为首先数据库资源是共享的,其次数据库连接的开关时非常消耗性能的,因此数据库软件系统中通常使用数据库连接池,主要也是为了节省打开或者关闭数据库连接所引起的效率损耗。
4.操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
5.Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
6.HttpApplication 也是单例的典型应用。熟悉http://ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.
7.多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
8.Application 也是单例的典型应用(Servlet编程中会涉及到)。
9.在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理。
10.在servlet编程中,每个Servlet也是单例。


单例模式与静态类的区别

其实我们可以发现单例模式与静态类在思想上有一定相似点。我认为单例模式其实可以看做是对静态类思想上的一种拓展,但两者的区别希望大家能区分清楚。
1)首先最大的区别就是静态类无法实例化,而单例模式会提供给你一个全局唯一的对象。静态类只是提供给你很多静态方法,这些方法不用创建对象,通过类就可以直接调用。
2)单例模式的灵活性更高,方法可以被override,因为静态类都是静态方法,所以不能被override。其次单例可以继承类(但不能继承实例成员)。
3)单例可以进行懒加载,静态类会在初始化时加载。
4)静态类没有生命周期,不存在状态信息,而它静态方法中产生的对象,会随着静态方法执行完毕而释放掉。而单例模式下产生的实例,会一直存在内存中,不会被GC清除掉,除非整个JVM退出了。详见:单例模式与垃圾回收-----Jvm的垃圾回收机制到底会不会回收掉长时间不用的单例模式对象

总结:
那么时候时候应该用静态类,什么时候应该用单例模式呢?首先如果你只是想使用一些工具方法,那么最好用静态类,静态类比单例类更快,因为静态的绑定是在编译期进行的。如果你要维护状态信息,或者访问资源时,应该选用单例模式。还可以这样说,当你需要面向对象的能力时(比如继承、多态)时,选用单例类,当你仅仅是提供一些方法时选用静态类。


常见的7种实现方式

1.饿汉模式(线程安全)

顾名思义:特别饿,一上来就要吃东西。即立刻加载,在调用getInstance方法前就产生了实例。这种模式缺点也很明显——占用资源。当单例类占用资源很大时,我们其实更希望它懒加载(在被使用时再产生实例)。因此饿汉模式也更适合一初始化就被使用到的类:

/**
 * 单例模式的饿汉实现
 */
public class ClassA {
    //1.私有化构造方法,避免类在外部被实例化,限制产生多个对象
    private ClassA(){ }
    //2.在类的内部创建一个类的实例
    //类初始化时,立即加载这个对象(没有延时加载的优势,但也因此是线程安全的)
    private static final ClassA instance = new ClassA();
    //3.对暴露一个公共的静态方法:将创建的对象返回,只能通过类来调用
    public static ClassA  getInstance(){
        return instance;
    }
}

2.懒汉模式(线程不安全)

顾名思义:非常懒,不到万不得已不出马。即延迟加载(Lazy Loading),也叫懒加载。懒汉式可以解决饿汉模式下浪费内存的问题,但是另一个问题就是:在多线程环境下,可能会存在多个访问者同时访问,发生构造出多个对象的问题。因为多个线程可能同时调用这个方法,同时判断这个对象还没有被创建出来,就各自创建一个实例。:

/**
 * 单例模式的懒汉实现1--线程不安全
 */
public class ClassB {
    //1.私有化构造方法,使得在类的外部不能调用此方法,限制产生多个对象
    private ClassB(){ }
    //2.在类的内部创建一个类的实例
    private static ClassB instance ;
    //3.对暴露一个公共的静态方法:将创建的对象返回,只能通过类来调用
    public static synchronized ClassB  getInstance(){
        if(instance == null) {
            instance = new ClassB();
        }
        return instance;
    }
}

3.懒汉模式(使用synchronized 同步)

通过使用synchronized关键字,这里可以通过锁定方法或是锁定代码块解决线程安全问题。但是缺点就是效率太低,线程是同步运行的,因为synchronized是排它锁,下个线程想要取得对象,就必须要等上一个线程释放,才可以继续执行。

/**
 * 单例模式的懒汉实现2--synchronized
 */
public class ClassB {
    //1.私有化构造方法,使得在类的外部不能调用此方法,限制产生多个对象
    private ClassB(){ }
    //2.在类的内部创建一个类的实例
    private static ClassB instance ;
    //3.对暴露一个公共的静态方法:将创建的对象返回,只能通过类来调用
    public static synchronized ClassB getInstance(){
        if(instance == null) {
            instance = new ClassB();
        }
        return instance;
    }
    /*public static ClassB getInstance(){
        if(instance == null) {
        	synchronized (ClassB.class) {
                ClassB= new ClassB();
            }
            instance = new ClassB();
        }
        return instance;
    }*/
}

4.懒汉模式(双重锁定检查方式即:Double-Check-Lock)

既然我们使用同步代码块是为了防止创建多个instance实例,那么除了第一次调用时是执行了Singleton的构造函数之外,以后的每一次调用都是直接返回instance对象,这样返回对象这个操作耗时不就小了吗,因此有了双重锁定检查模式的写法。

/**
 * 单例模式的懒汉实现3-使用双重校验锁来实现单例模式
 */
public class ClassC {
    //1.私有化构造方法,使得在类的外部不能调用此方法,限制产生多个对象
    private ClassC(){ }
    //2.在类的内部创建一个类的实例
    private static ClassE instance; 
    //3.对暴露一个公共的静态方法:将创建的对象返回,只能通过类来调用
    public static ClassC  getInstance(){
        if(instance == null){ //检查实例,如果为空,就进入同步代码块
            synchronized (ClassC.class){
                if(instance == null){ //再检查一次,仍未空才创建实例
                    instance = new ClassC();
                }
            }
        }
        return instance;
    }
}

看样子已经达到了要求,除了第一次创建对象之外,其它的访问在第一个if中就返回了,直接拿创建好的对象,很完美。
但事实上,JVM将这段代码编译成了8条汇编指令,大致做了三件事:
1)给instance实例分配内存;

2)调用instance的构造器,初始化对象属性;

3)将instance对象指向分配的内存空间(注意到这步时instance就非null了)

如果按正常的指令执行倒也无妨,但JVM为了做指令优化,提高程序运行效率,允许指令重排序。因此,程序在真正运行时可能会变成:

a)给instance实例分配内存;

b)将instance对象指向分配的内存空间(存在内存空间,instance就非null了);

c)调用instance的构造器,初始化对象属性;

这时我们模拟下多线程环境,当线程一执行b)完毕,在执行c)之前,被切换到线程二上,这时候instance判断为非空,此时线程二遇到第一个非空判断,直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化)。

具体来说就是synchronized虽然保证了线程的原子性(即synchronized块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)

那我们该怎么做呢?解决方法便是:禁止指令重排序优化,即使用volatile变量。

/**
 1. 单例模式的懒汉实现3-使用双重校验锁来实现单例模式
 */
public class ClassC {
    //1.私有化构造方法,使得在类的外部不能调用此方法,限制产生多个对象
    private ClassC(){ }
    //2.在类的内部创建一个类的实例
    private volatile static ClassE instance; 
    //3.对暴露一个公共的静态方法:将创建的对象返回,只能通过类来调用
    public static ClassC  getInstance(){
        if(instance == null){ //检查实例,如果为空,就进入同步代码块
            synchronized (ClassC.class){
                if(instance == null){ //再检查一次,仍未空才创建实例
                    instance = new ClassC();
                }
            }
        }
        return instance;
    }
}

tips:我们知道volatile能保证可见性和有序性,而
volatile在该模式的作用并不是为了保证可见性,而是为了禁止重排序,保证原子操作。

为什么没有保证可见性呢?
主要是因为synchronized已经保证了可见性,第二次非null判断是在加锁以后,则根据这一条,另一个线程一定能看到这个引用被赋值。所以即使没有volatile,依旧能保证可见性。

因此主要是为了禁止重排序,
初始化一个实例(SomeType st = new SomeType())在java字节码中会有4个步骤,

1.申请内存空间,
2.初始化默认值(区别于构造器方法的初始化),
3.执行构造器方法
4.连接引用和实例。
这4个步骤后两个有可能会重排序,1234 1243都有可能,造成未初始化完全的对象发布。
volatile确保了先执行构造器方法,将引用和实例连接到一起。如果没有禁止重排序,会导致另一个线程可能获取到尚未构造完成的对象。
参考:java 单例模式中双重检查锁定 volatile 的作用?

5.静态内部类方式

可以看到通过静态内部类方式我们没有进行任何同步操作,那他是如何保证线程安全呢?和饿汉模式一样,是靠JVM保证类的静态成员只能被加载一次的特点,这样就从JVM层面保证了只会有一个实例对象。那么问题来了,这种方式和饿汉模式又有什么区别呢?不也是立即加载么?实则不然,加载一个类时,其内部类不会同时被加载。一个内部类当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时才会被加载,可以说这是饿汉式和懒汉式的组合。从源码可以看到,我们调用getInstance()方法时才会加载静态内部类,进而访问静态内部类的成员进行实例化。这样既解决了饿汉模式下可能造成资源浪费的问题,达到了类似懒汉式的效果,同时又是线程安全的,可以说这种方式是实现单例模式的最优解。

/**
 * 使用静态内部类方式实现单例模式
 */
public class ClassD {
    //1.私有化构造方法,使得在类的外部不能调用此方法,限制产生多个对象
    private ClassD(){ }
    //2.静态内部类里面创建了一个Singleton单例
    private static class Holder{
        private static ClassD instance = new ClassD();
    }
    //3.对暴露一个公共的静态方法:将创建的对象返回,只能通过类来调用
    public static ClassD  getInstance(){
        return Holder.instance;
    }
}

6.使用ThreadLocal实现方式

既然有使用Synchronized锁(以时间换空间的方式)解决单例模式下线程间数据共享的场景,那必然少不了通过ThreadLocal(以空间换时间)实现线程间数据隔离的方式解决线程安全的方式。
ThreadLocal采用以空间换时间的方式,为每一个线程都提供一份变量,因此可以同时访问而互不影响。

/**
 * 使用ThreadLocal实现单例模式
 */
public class ClassE {
    //1.私有化构造方法,使得在类的外部不能调用此方法,限制产生多个对象
    private ClassE(){ }
    //2.在类的内部创建一个类的实例
    private static final ThreadLocal<ClassE> tls = new ThreadLocal<ClassE>(){
        @Override
        protected ClassE initialValue(){
            return new ClassE();
        }
    };
    //3.对暴露一个公共的静态方法:将创建的对象返回,只能通过类来调用
    public static ClassE  getInstance(){
        return tls.get();
    }
}

7.使用CAS锁(AtomicReference)来实现

CAS锁(Compare and Swap):比较并交换,是一种有名的无锁算法,属于乐观锁),用CAS锁来实现单例模式同样是线程安全的。

/**
 * 使用CAS锁来实现单例模式
 */
public final class ClassF {
    //1.私有化构造方法,使得在类的外部不能调用此方法,限制产生多个对象
    private ClassF(){ }
    //2.在类的内部创建一个类的实例
    private static final AtomicReference<ClassF> instance = new AtomicReference<ClassF>(); 
    //3.对暴露一个公共的静态方法:将创建的对象返回,只能通过类来调用
    public static final ClassF getInstance(){
        for(;;){
            ClassF current = instance.get();
            if(current != null){
                return current;
            }
            current = new ClassF();
            if(instance.compareAndSet(null,current)){
                return current;
            }
        }
    }
}

与synchronized相同,都是通过锁实现线程安全。不同的是,相对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现(属于忙等待算法)。因此它不存在线程切换和阻塞的额外消耗,可以支持较大的并行度,但CAS算法的一个重要缺点就是如果忙等待一直不成功,则会一直处于死循环中,对CPU的性能消耗不亚于CPU的上下文切换。因此也不是很推荐。

tips:jdk1.6中synchronized的锁升级就用到了CAS算法,将悲观锁与乐观锁的思想进行了结合。

总结

饿汉式和懒汉式区别

从名字上来说,饿汉和懒汉,

饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,

而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。

另外从以下两点再区分以下这两种方式:

1、线程安全:

饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,

懒汉式本身是非线程安全的,为了实现线程安全有上述多种(除去第一种)写法,这几种实现在资源加载和性能方面有些区别。

2、资源加载和性能:

饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,

而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。

你可能感兴趣的:(设计模式,设计模式,单例模式,singleton)