单例模式

一、简介:

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。保证一个类仅有一个实例,并提供一个访问它的全局访问点。

  总计为3点就是:

  1. 单例类只能有一个实例。

  2. 单例类必须自己创建自己的唯一实例。

  3. 单例类必须给所有其他对象提供这一实例。

二、解决的问题及应用场景:

解决问题:

       一个全局使用的类频繁地创建与销毁,控制实例数目,节省系统资源;资源共享的情况下,避免由于资源操作时导致的性能或损耗等,如日志文件,应用配置;控制资源的情况下,方便资源之间的互相通信,线程池等。

应用场景:

  1. Windows的Task Manager(任务管理器)就是很典型的单例模式

  2. 网站的计数器,一般也是采用单例模式实现

  3. 应用程序的日志应用,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加

  4. Web应用的配置对象的读取,这个是由于配置文件是共享的资源

  5. 数据库连接池,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗

  6. 多线程的线程池,这是由于线程池要方便对池中的线程进行控制

  7. 需要频繁实例化然后销毁的对象;创建对象时耗时过多或者耗资源过多,但又经常用到的对象;频繁访问数据库或文件的对象

  8. 生产唯一序列号

三、实现:

大致分为饿汉式、懒汉式、双重锁机制、登记式/静态内部类、枚举

1、饿汉式:

顾名思义,一开始就创建出来,比较“饥饿”,这种方式是不存在线程安全问题的。在加载类的时候就会创建类的单例,并保存在类中。为什么这种方式能够保证实例的唯一性呢?因为在同一个类加载器下,类的初始化只会进行一次,并且在多线程环境下,JVM会保证只有一个线程执行类的初始化,所以当我们第一次调用getInstance方法,访问instance静态变量时,Singleton类还没有没有进行初始化,就会首先进行Singleton类的初始化,类的初始化之前会经历加载、验证、准备、解析阶段,然后才到初始化阶段,在初始化阶段中JVM会执行static语句块,此时就会为Singleton在java堆上为分配一块内存,然后把instance指向这块内存,即实例化了Singleton,接着getInstance方法就会返回Singleton的实例,当我们第二次调用getInstance()方法时,它会返回上次在类初始化阶段创建的Singleton实例,而不会再进行一次类的初始化,所以这就保证了每次调用getInstance()方法都是返回同一个实例。

public class Singleton {  
    //此处定义类变量实例并直接实例化,在类加载的时候就完成了实例化并保存在类中
    private static Singleton instance = new Singleton();  
    //定义无参构造器,用于单例实例
    private Singleton (){}  
    //定义公开方法,返回已创建的单例
    public static Singleton getInstance() {  
        return instance;  
    }  
}

2、懒汉式:

顾名思义,就是不做事,这里也是同义,懒汉式就是不在系统加载时就创建类的单例,而是在第一次使用实例的时候再创建。没有加锁 synchronized的严格意义上它并不算单例模式,只适合单线程的程序,多线程必须加锁。

线程不安全实现方式:

public class Singleton {  
    //定义一个私有类变量来存放单例,私有的目的是指外部无法直接获取这个变量,而要使用提供的公共方法来获取
    private static Singleton instance;  
    //定义私有构造器,表示只在类内部使用,亦指单例的实例只能在单例类内部创建
    private Singleton (){}  
    //定义一个公共的公开的方法来返回该类的实例,由于是懒汉式,需要在第一次使用时生成实例
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

线程安全实现方式:

public class Singleton {  
    //定义一个私有类变量来存放单例,私有的目的是指外部无法直接获取这个变量,而要使用提供的公共方法来获取
    private static Singleton instance;  
    //定义私有构造器,表示只在类内部使用,亦指单例的实例只能在单例类内部创建
    private Singleton (){}  
    //定义一个公共的公开的方法来返回该类的实例,由于是懒汉式,需要在第一次使用时生成实例,所以为了线程安全,使用synchronized关键字来确保只会生成单例
    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

3、双重锁机制(Double Check Lock):

在懒汉式实现单例模式的代码中,有使用synchronized关键字来同步获取实例,保证单例的唯一性,但是上面的代码在每一次执行时都要进行同步和判断,无疑会拖慢速度,使用双重加锁机制正好可以解决这个问题。采用双锁机制,安全且在多线程情况下能保持高性能。这种其实是对线程安全的懒汉模式的优化,把原本锁在方法的锁放在了代码块内,这样外边加了一层判断就不会每次都来同步锁等待,减少了性能的损耗。

public class Singleton {  
    private static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
        //如果单例已经存在,那么就不再需要进行同步操作,而是直接返回这个实例,如果没有创建,才会进入同步块
        if (singleton == null) {  
            //同步块的目的与之前相同,目的是为了防止有两个调用同时进行时,导致生成多个实例,有了同步块,每次只能有一个线程调用能访问同步块内容,当第一个抢到锁的调用获取了实例之后,这个实例就会被创建,之后的所有调用都不会进入同步块,直接在第一重判断就返回了单例
            synchronized (Singleton.class) {  
                //为了进入synchronized块后只有对象为null的情况下才创建实例,避免重复创建对象实例,而且synchronized块锁住的是类的Class对象,保证了在多线程环境下只有一个线程进入synchronized块
                if (singleton == null) {  
                    singleton = new Singleton();  
                }  
            }  
        }  
        return singleton;  
    }  
}

少数情况下DCL还是不能保证实例的单一性,问题就出现在:singleton = new Singleton();  在Singleton第一次进行实例化过程中,这个实例化过程可以分为以下3步:

1、在java堆分配中Singleton对象的内存空间;

2、进行Singleton类的初始化过程;

3、把singleton指向1步骤中分配的内存空间.

某些JVM会把这3个步骤进行指令重排序,变为以下顺序:

1、在java堆分配中Singleton对象的内存空间;

3、把singleton指向1步骤中分配的内存空间;

2、进行Singleton类的初始化过程.

如果在单线程环境下这样是没有问题的,因为就算指令重排,在getSingleton方法返回时singleton指向的对象已经完成了初始化,但是在多线程环境下就出现问题了,假设现在有两个线程A、B,线程A执行到了getSingleton方法的注释2,即进行Singleton的实例化,由于指令重排,线程A先执行1、3步骤,此时singleton已经指向了分配的内存空间,导致此时的singleton!= null,而恰好线程B此时执行到了getSingleton方法,进入判断 if(singleton== null),因为此时singleton!= null,所以条件判断为false,不进入if语句,直接来到return语句,返回了还没初始化完毕的singleton,这样就可能导致程序崩溃!因为你在使用一个还未初始化完成的对象。

针对DCL的错误,有两种解决办法,第一种办法是使用Volatile关键字,因为Volatile会禁止指令重排序,保证对象实例化过程按1、2、3步骤进行;第二种办法是再加一个局部变量做一层缓冲,下面分别使用来完善DCL。

使用Volatile关键字:

在多线程环境下,Volatile会禁止指令重排序保证内存可见性,所以线程执行到singleton= new Singleton()时,保证Singleton类初始化完毕后才把singleton引用指向java堆的内存空间,避免另外一个线程访问到未初始化完毕的singleton。

public class Singleton {  
    //加上Volatile关键字修饰
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
        //如果单例已经存在,那么就不再需要进行同步操作,而是直接返回这个实例,如果没有创建,才会进入同步块
        if (singleton == null) {  
            //同步块的目的与之前相同,目的是为了防止有两个调用同时进行时,导致生成多个实例,有了同步块,每次只能有一个线程调用能访问同步块内容,当第一个抢到锁的调用获取了实例之后,这个实例就会被创建,之后的所有调用都不会进入同步块,直接在第一重判断就返回了单例
            synchronized (Singleton.class) {  
                //为了进入synchronized块后只有对象为null的情况下才创建实例,避免重复创建对象实例,而且synchronized块锁住的是类的Class对象,保证了在多线程环境下只有一个线程进入synchronized块
                if (singleton == null) {  
                    singleton = new Singleton();  
                }  
            }  
        }  
        return singleton;  
    }  
}

加一个局部变量做一层缓冲:

这种方法也能够保证DCL的正确性,因为它是先把同为Singleton类型的局部变量singletonVar实例化后,才赋值给singleton,这就不管singletonVar实例化过程中怎么样重排序,在singletonVar还未初始化完毕之前,singleton一直为null,当singletonVar实例化完毕后,singleton才指向它,这样就避免了singleton指向一个未初始化完毕的对象。

public class Singleton {  
    private static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
        //1、增加一个局部变量,同为Singleton类型
        Singleton singletonVar = null;

        //如果单例已经存在,那么就不再需要进行同步操作,而是直接返回这个实例,如果没有创建,才会进入同步块
        if (singleton == null) {  
            //同步块的目的与之前相同,目的是为了防止有两个调用同时进行时,导致生成多个实例,有了同步块,每次只能有一个线程调用能访问同步块内容,当第一个抢到锁的调用获取了实例之后,这个实例就会被创建,之后的所有调用都不会进入同步块,直接在第一重判断就返回了单例
            synchronized (Singleton.class) {  
                //为了进入synchronized块后只有对象为null的情况下才创建实例,避免重复创建对象实例,而且synchronized块锁住的是类的Class对象,保证了在多线程环境下只有一个线程进入synchronized块
                if (singleton == null) {  

                    //2、执行实例时,先实例化这个局部变量
                    singletonVar = new Singleton();  
                    //3、待局部变量实例化完毕后,才把这个实例赋值给要返回的静态变量singleton
                    singleton = singletonVar;
                }  
            }  
        }  
        return singleton;  
    }  
}

4、登记式/静态内部类:

静态内部类形式的特点和饿汉方式一样都是基于类的初始化保证实例的唯一性,同时它在饿汉方式的基础上增加了延迟初始化,在饿汉方式中说到在Singleton类初始化时,就会完成单例类实例的创建,这样就可能导致了提前初始化了单例类,造成资源浪费,而静态内部类就解决了这个缺点,当第一次初始化Singleton类时并不会创建Singleton,只有在调用getSingleton方法时才会导致Singleton类实例化,因为第一次调用getSingleton方法时会让JVM初始化SingletonHolder类,在初始化SingletonHolder类的同时完成了Singleton单例类实例的创建,然后返回Singleton实例,以后调用getSingleton方法时都会返回同一实例。

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

为什么基于类初始化的单例就是线程安全的呢?这是因为类的初始化过程其实是在执行clinit方法,clinit方法又叫做类构造器,clinit方法是由编译器收集所有的静态变量赋值动作和static语句块合并形成的,在多线程环境下,JVM执行clinit方法时,会给clinit方法加锁,多个线程初始化类时,只有一个线程会获得clinit方法的执行权,其他线程会阻塞等待,等某个线程执行完clinit方法后,就完成了类的初始化,这时就会唤醒其他等待线程,其他等待线程发现类已经执行过clinit方法了,就不再重复执行了,所以这就是单例模式线程安全实现的保证,也是单例模式实例唯一性的保证。

5、枚举:

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。只能定义一个枚举,不能定义第二个枚举。

public enum Singleton {  
    INSTANCE;   
}

枚举类本质还是一个类,并且它是final的,所以它不可被继承,它里面的构造方法私有,并且INSTANCE字段是一个静态变量,在static语句块中实例化,所以枚举模式保持单例唯一性的本质还是基于类的初始化,它的原理和前面讲过的饿汉方式、静态内部类形式一样。

四、优缺点:

 饿汉式懒汉式双重锁机制静态内部类枚举
优点安全,获取单例速度快安全,延迟初始化安全,延迟初始化安全,延迟初始化写法简洁,延迟初始化,安全,反射也无法破坏单例
缺点提前初始化单例类,浪费空间第一次使用反应慢,效率低,同步开销大写法复杂,第一次使用加载慢第一次使用加载慢暂时没发现缺点,枚举是java5之后才加入,使用的人少,很多人不熟悉枚举

单例模式的优缺点:

优点:

  1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例

  2. 避免对资源的多重占用

  3. 单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就防止其它对象对自己的实例化,确保所有的对象都访问一个实例

  4. 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性

缺点:

  1. 没有接口,不能继承

  2. 与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化

  3. 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难

  4. 如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失

五、注意事项:

  1. 使用时不能用反射模式创建单例,否则会实例化一个新的对象
  2. 使用懒单例模式时注意线程安全问题
  3. 不能被继承。如果类中只有private的构造方法,那么此类不可以被继承。子类的构造函数一定会调用父类的构造函数,但是如果父类中只有私有的构造方法,那么子类就无法调用父类,就会有问题。
  4. 一般情况下,不建议使用懒汉方式,建议使用饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用登记方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用双检锁方式
  5. 首先如果你确保程序是在单线程环境下工作,那么推荐你使用不加synchronized关键字的懒汉模式;但是如果程序是在多线程环境下工作,这时就要考虑线程安全问题,基于类的初始化的单例模式天生线程安全,可以使用饿汉方式、静态内部类方式、枚举模式;如果你要明确的延迟初始化要求,推荐使用静态内部类方式、DCL、枚举模式;如果你有选择困难症,那不用考虑那么多了,推荐你使用DCL和静态内部类形式;不管在什么场合,都不要考虑使用加synchronized关键字的懒汉模式,它的缺点最多。

六、序列化问题:

单例类是可以保存到磁盘或通过网络传输,换句话说你的单例类是支持序列化的,那么你就要保证单例类在反序列化时不会重新创建新的对象实例,因为反序列化时会创建一个和单例一模一样的实例的,java中通过ObjectInputStream的readObject方法来实现反序列化,它里面会通过反射来创建一个新的实例,所以就算你的构造方法私有,它还是可以通过setAccessible(true)来获得单例构造器的访问权,从而创建一个新的对象实例,反序列化的本质就是反射,换句话说,反射会破坏单例模式的实例唯一性。

那么如何确保单例类反序化时不会重新创建新实例呢?只要在单例类中加入以下方法:

public class Singleton {
    private static Singleton singleton;
    
    //...
    
    private Object readResolve() throws ObjectStreamException {
        //返回单例对象
        return singleton;
    }
}

加入readResolve方法后就可以改变反序列化的规则,在readObject方法中,它发现该类定义了readResolve方法,它就会通过反射调用readResolve方法返回对象实例,而不是默认的通过反射重新创建一个新的对象,所以只要我们在readResolve方法中返回单例对象,readObject方法就会返回单例对象,这样就防止了单例模式被反序列化的破坏。

对于枚举模式不存在反序列化重新生成实例的情况,所以对于枚举模式实现的单例不用考虑反序列化情况,因为枚举的反序列化不是通过反射实现的,而是通过其他方式实现,枚举有自己的一套处理反序列化的机制,类似于使用容器的方式,而对于饿汉模式、静态内部类形式、懒汉模式、DCL的单例就需要考虑反序列情况。

七、单例模式与静态类:

在知道了什么是单例模式后,我想你一定会想到静态类,既然只使用一个对象,为何不干脆使用静态类?这里我会将单例模式和静态类进行一个比较。

  1. 静态方法中产生的对象会在执行后被释放,进而被GC清理,不会一直存在于内存中。

  2. 静态类会在第一次运行时初始化,单例模式可以有其他的选择,即可以延迟加载。

  3. 不需要维持任何状态,仅仅用于全局访问,此时更适合使用静态类。需要维持一些特定的状态,此时更适合使用单例模式。

 

参考:

https://blog.csdn.net/Rain_9155/article/details/103318029

https://www.runoob.com/design-pattern/singleton-pattern.html

https://baijiahao.baidu.com/s?id=1625588808634528054&wfr=spider&for=pc

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值