为什么用枚举类来实现单例模式越来越流行?

前言

单例模式是 Java 设计模式中最简单的一种,只需要一个类就能实现单例模式,但是,你可不能小看单例模式,虽然从设计上来说它比较简单,但是在实现当中你会遇到非常多的坑,所以,系好安全带,上车。

单例模式的定义

单例模式就是在程序运行中只实例化一次,创建一个全局唯一对象,有点像 Java 的静态变量,但是单例模式要由于静态变量,静态变量在程序启动的时候就要创建,会造成大量的资源浪费,好的单例模式不会有这个问题。开发中的很多工具类都应用了单例模式,线程池、缓存、日志对象等,它们都只需要创建一个对象,如果创建多份实例,可能会带来不可预知的问题,比如资源的浪费、结果处理不一致等问题。

单例的实现思路

  • 静态化实例对象
  • 私有化构造方法,禁止通过构造方法创建实例
  • 提供一个公共的静态方法,用来返回唯一实例

单例的好处

  • 只有一个对象,内存开支少、性能好(当一个对象的产生需要比较多的资源,如读取配置、产生其他依赖对象时,可以通过应用启动时直接产生一个单例对象,让其永驻内存的方式解决)
  • 避免对资源的多重占用(一个写文件操作,只有一个实例存在内存中,避免对同一个资源文件同时写操作
  • 在系统设置全局访问点,优化和共享资源访问(如:设计一个单例类,负责所有数据表的映射处理)

单例模式的实现

单例模式的主流写法有饿汉模式、懒汉模式、双重检查锁模式、静态内部类单例模式、枚举类实现单例模式五种方式,其中懒汉模式、双重检查锁模式两种模式写法不当,会导致在多线程下不是单例或者单例出异常,后面将会给大家详细介绍。我们从最基本的饿汉模式开始我们的单例编写之路。

饿汉模式

饿汉模式采用的是一种简单粗暴的形式,在定义静态属性时,直接实例化了对象。代码如下:

//在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
public class SingletonObject1 {
    // 利用静态变量来存储唯一实例
    private static final SingletonObject1 instance = new SingletonObject1();

    // 私有化构造函数
    private SingletonObject1(){
        // 里面可能有很多操作
    }

    // 提供公开获取实例接口
    public static SingletonObject1 getInstance(){
        return instance;
    }
}

饿汉模式的优缺点

优点
  • JVM层面的线程安全,static关键字保证了在引用这个变量时,关于这个变量的所以写入操作都完成
缺点
  • 不能实现懒加载,造成空间浪费,如果一个类比较大,我们在初始化的时就加载了这个类,但是我们长时间没有使用这个类,这就导致了内存空间的浪费。

懒汉模式

懒汉模式就像一个懒汉的时候,只有饿了才会想办法找东西来填饱肚子,从来不会先的准备好食物,以防饿了。懒汉模式实现了懒加载,解决了饿汉模式带来的空间浪费问题,实现了使用时才去初始化类,但是也引入了其他的问题,我们先来看看下面这个懒汉模式

public class SingletonObject2 {
    // 定义静态变量时,未初始化实例
    private static SingletonObject2 instance;

    // 私有化构造函数
    private SingletonObject2(){

    }

    public static SingletonObject2 getInstance(){
        // 使用时,先判断实例是否为空,如果实例为空,则实例化对象
        if (instance == null)
            instance = new SingletonObject2();
        return instance;
    }
}

上面这段懒汉模式实现代码,在多线程的情况下,不能保证是单例模式,主要问题出现在实例化对象的时候,所以我单独把实例化的代码提出来,给大伙讲讲为什么在多线程的情况下有可能会初始化多份实例。

     1   if (instance == null)
     2       instance = new SingletonObject2();

假设有两个线程都进入到 1 这个位置,因为我们没有任何资源保护措施,所以两个线程判断的instance都为空,都将去执行 2 的实例化代码,所以就会出现多份实例的情况。

我们已经知道,上面的这段代码出现多份实例是因为没有对资源进行保护,如果我们对资源加锁,是不是可以解决多份实例的问题?确实如此,我们给getInstance()方法加上synchronized关键字,使得getInstance()方法成为受保护的资源就能够解决多份实例的问题。加上synchronized关键字之后代码如下:

public class SingletonObject3 {
    private static SingletonObject3 instance;

    private SingletonObject3(){

    }

    public synchronized static SingletonObject3 getInstance(){
        /**
         * 添加class类锁,影响了性能,加锁之后将代码进行了串行化,
         * 我们的代码块绝大部分是读操作,在读操作的情况下,代码线程是安全的
         *
         */

        if (instance == null)
            instance = new SingletonObject3();
        return instance;
    }
}

这样确实解决了可能出现多份实例的情况,但是加synchronized关键字之后,引入了新的问题,加锁之后将代码进行了串行化,降低了系统的使用性能。getInstance()方法大部分的操作都是读操作,读操作是线程安全的。

懒汉模式的优缺点

优点
  • 实现了懒加载,节约了内存空间
缺点
  • 在不加锁的情况下,线程不安全,可能出现多份实例
  • 在加锁的情况下,会是程序串行化,使系统有严重的性能问题

双重检查锁模式

在懒汉模式中我们知道了getInstance()方法大部分的操作都是读操作,读操作是线程安全的,对getInstance()方法加锁造成了很大的性能问题,由此产生了一种更加优雅的加锁方式,既能对getInstance()加锁,又能不降低性能,这种模式就是我们现在要了解的双重检查锁模式,我们先来看看双重检查锁模式的单例实现:

public class SingletonObject4 {
    private static SingletonObject4 instance;

    private SingletonObject4(){

    }

    public static SingletonObject4 getInstance(){

        // 第一次判断,如果这里为空,不进入抢锁阶段,直接返回实例
        if (instance == null)
            synchronized (SingletonObject4.class){
                // 抢到锁之后再次判断是否为空
                if (instance == null){
                    instance = new SingletonObject4();
                }
            }

        return instance;
    }
}

可以说双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的实现看上去完美无缺,但是上面的实现代码,在多线程的情况下,可能会出现空指针问题,下面我们一起来了解是为什么会出现空指针问题。

空指针问题是由于虚拟机的优化和指令重排序造成的,我们在实例化对象时,虚拟机会对里面的代码进行优化,也许你还不太理解,我们来看看下面一段代码

    private SingletonObject4(){
     1   int x = 10;
     2   int y = 30;
     3  Object o = new Object();

    }

JVM 在实例化SingletonObject4()时不一定按照1、2、3的顺序执行,JVM 会对它进行优化,可能是3、1、2,也可能是2、3、1,JVM 会保证最后都实例化完成。 如果构造函数中操作比较多时,为了提升效率,JVM 会在构造函数里面的属性为全部完成实例化时就返回对象。这也就造成了其他线程获取到实例,在使用某属性时,可能该属性还没实例化完成,就会造成空指针异常。

要解决上面双重检查锁模式带来空指针异常的问题,需要使用volatile关键字,volatile关键字严格遵循happens-before原则,即在读操作前,写操作必须全部完成。添加volatile关键字之后的单例模式代码:

    // 添加volatile关键字
    private static volatile SingletonObject5 instance;

    private SingletonObject5(){

    }

    public static SingletonObject5 getInstance(){

        if (instance == null)
            synchronized (SingletonObject5.class){
                if (instance == null){
                    instance = new SingletonObject5();
                }
            }

        return instance;
    }
}

添加volatile关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

静态内部类单例模式

静态内部类单例模式也称单例持有者模式,实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由static修饰,保证只被实例化一次,并且严格保证实例化顺序。静态内部类单例模式代码如下:

public class SingletonObject6 {

    private SingletonObject6(){

    }
    // 单例持有者
    private static class InstanceHolder{
        private  final static SingletonObject6 instance = new SingletonObject6();

    }

    //
    public static SingletonObject6 getInstance(){
        // 调用内部类属性
        return InstanceHolder.instance;
    }
}

静态内部类单例模式是一种比较好的单例实现模式,也是比较常用的一种单例实现模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

枚举类实现单例模式

枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

public class SingletonObject7 {

    private SingletonObject7(){

    }

    /**
     * 枚举类型是线程安全的,并且只会装载一次
     */
    private enum Singleton{
        INSTANCE;

        private final SingletonObject7 instance;

        Singleton(){
            instance = new SingletonObject7();
        }

        private SingletonObject7 getInstance(){
            return instance;
        }
    }

    public static SingletonObject7 getInstance(){

        return Singleton.INSTANCE.getInstance();
    }
}

破坏单例模式的方法及解决办法

1、除枚举方式外, 其他方法都会通过反射的方式破坏单例,反射是通过调用构造方法生成新的对象,所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例,解决办法如下:

private SingletonObject1(){
    if (instance !=null){
        throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取");
    }
}

2、如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象。

  public Object readResolve() throws ObjectStreamException {
        return instance;
    }

最后

打个小广告,欢迎扫码关注微信公众号:「平头哥的技术博文」,一起进步吧。

原文地址:https://www.cnblogs.com/jamaler/p/11421049.html

时间: 2024-08-02 16:00:51

为什么用枚举类来实现单例模式越来越流行?的相关文章

使用单元素枚举类实现单例模式

在上一篇文章<单例模式的终结者--setAccessible(true)>中介绍了传统单例模式的不足之处,虽然枚举类实现单例模式还没有被广泛采用,但<effective java>里面已经承认单元素枚举类是实现单例模式最好的方法了. 下面写个小demo示范一下,这是只有一个元素的枚举类,枚举类里面也可以写方法. package go.derek; public enum EnumSingleton { instance; public void doSomething(){ Sys

设计模式(一)单例模式:5-单元素枚举类模式

思想: 这是 Effective Java 极力推荐的一种,代码为各种实现中最简单的. 1 public enum EnumSingleton { 2 3 INSTANCE; 4 5 } 单元素枚举类模式 其实现,完全是基于枚举类的特性,可以说天生受到了 JVM 的支持,而且既不用思考反射,也不用考虑多线程,对于自身的循环引用,本质上也是一个对象. 1 @Test 2 public void test() throws Exception { 3 EnumSingleton singleton1

Effective Java之枚举类

随着Effective Java这本书看的越来越多,发现在平时使用Java时犯了很多明显的错误,自己平时知道一点但缺少使用的也越来越多. 关于枚举类,书中的建议是用枚举类替代传统的静态常量,平时,如果一类数据用不同的数值表示,我正常的做法是写一串const static常量来表示,但看了这本书后发现,这样的表示方法虽然可以达到自己的目的,但是存在一些隐患,比如类型安全,还有,在使用上述方法区分不同的类型时,某一个具体的类型中的方法是在外面用过判断然后调用的,在这种情况下,代码就变得不是很漂亮.

枚举类专题(有源代码)

简介:JDK1.5之前需要自定义枚举类,JDK1.5新增的enum关键字用于定义枚举类,若枚举只有一个成员,则可以作为一种单例模式的实现方式 枚举类的属性:枚举类对象的属性不应允许被改动,所以应该使用private final修饰 ·枚举类的使用private final修饰的属性应该在构造器中为其赋值 ·若枚举类显式的定义了带参数的构造器,则在列出枚举值时也必须对应的传入参数 Enum枚举类: ·必须在枚举类的第一行声明枚举类对象. ·枚举类和普通类的区别: 1.使用enum定义的枚举类默认继

枚举类的介绍

枚举类,即对象为可穷举的类 JDK 1.5 新增的 enum 关键字用于定义枚举类,在这之前,需要自定义枚举类 若枚举只有一个成员(对象),则可以作为一种单例模式的实现方式--单例(单子)设计模式(Singleton) 自定义枚举类: public class TestSeason { public static void main(String[] args) { Season spring = Season.SPRING; Season summer = Season.SUMMER; Sea

枚举类enum事例

新建一个枚举类: public enum enum1 { RED("红色"), BLACK, GREEN, YELLOW, BLUE; private String cum; private enum1() {//私有构造方法(必须设置为私有,因为枚举本身是单例模式.) } private enum1(String cum) { this.cum = cum; } public String getCum() { return cum; } } values()方法获取所有枚举常量,可

JavaSE复习_6 枚举类

△单例类是指只有一个实例,而枚举类实际上就是有有限个实例的类,在类里已经把实例定义好了. △枚举类的三种创建形式: 1) enum Week { MON,TUE,WED;//枚举类有默认构造函数创建的三个实例} } 2) enum Week { MON("星期一"), TUE( "星期二"),WED ("星期三" ); //具有参数的构造函数 private String name; Week(String name) { this. name=

zend framework获取数据库中枚举类enum的数据并将其转换成数组

在model中建立这样的模型,在其中写入获取枚举类的方法 class Student extends Zend_Db_Table{ protected $_name ='student'; protected $_primary ='id'; function getPartyEnum($enumname){ //适配器并获取数据 $db = $this->getAdapter(); $db->query("SET NAMES GBK"); $sql = $db->q

枚举类

enum SeasonEnum //这是一个枚举类,有四个枚举值 { SPRING,SUMMER,FALL,WINTER; } public class EnumTest { public void judge(SeasonEnum s) { switch(s) { case SPRING: System.out.println("春天"); break; case SUMMER: System.out.println("夏天"); break; case FALL