Java设计模式之单例模式(实现、应用的详细总结)

目录

 

一、单例模式简介

二、单例模式的简单实现方式

三、线程安全的懒汉式单例模式

四、破坏单例模式的攻击及解决方案

五、枚举单例模式(最优美的单例实现方式)

六、单例模式的应用

七、单例模式总结


一、单例模式简介

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

1.单例模式的定义

     单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例的访问方法。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求调用。总之,选择单例模式就是为了避免不一致状态。

2.单例模式的要素

(1)单例类只能有一个实例

        需要私有其构造方法,确保其他人不能创建该单例对象的新实例。

(2)单例类必须自己创建唯一实例

       需要在单例类中创建单例对象。

  (3)单例类必须给其他对象提供该实例一个全局的访问方法

       需要将实例对象声明为私有,并提供一个公共的public访问方法返回该实例。

二、单例模式的简单实现方式

1.饿汉式

     饿汉式(也称为立即加载方式)是在单例类加载初始化时,就创建单例对象供外部使用。除非系统重启,否则这个对象不会改变,所以饿汉式天生就是线程安全的。

//饿汉式
public class Singleton{
    //私有构造方法
    private Singleton(){}     
    //创建并私有实例
    private static Singleton instance = new Singleton(); 
    //提供公共的全局的获取方式
    public static Singleton getInstance(){
        return instance;
    } 
}

2.懒汉式

懒汉式(也称为延迟加载方式),它在第一次获取单例对象时创建该单例对象,避免不使用时浪费资源。

//懒汉式
public class Singleton{
    private Singleton(){}
    private Singleton instance = null;
    public static Singleton getInstance(){
        if(instance == null){
            //第一次使用时创建
            instance = new Singleton();
        }
        return instance;
    }
}

三、线程安全的懒汉式单例模式

      在多线程的环境下,使用上述的懒汉式延迟加载,可能会产生多个单例对象。某一时刻,有线程A、B执行到第6行处,此时线程A执行if语句,检查出单例对象为null,并在准备执行第8行代码时,被切换出去让出CPU的使用权。接着线程B掌握了CPU的使用权,但是单例对象还未创建,因此线程B也将执行第8行代码创建到单例对象。导致创建多个单例对象。接下来介绍几种多线程安全的单例实现方式。

1.同步锁方式

    懒汉式创建方式在获取方法判空时会出现多线程安全问题,因此可以为该段代码加上同步锁,使得同一时刻只能有一个线程判断单例对象是否为null,并创建单例对象。

public class Singleton {
    private Singleton() {}
    private static Singleton instance = null;
    public static Singleton getInstance() {    
        //等同于public static synchronized Singleton3 getInstance()
        synchronized(Singleton.class){
            if(instance == null){
                instance = new Singleton();
            }
        }
        return instance;
    }
}

2.双重检测锁(不建议使用)

    在方法上加synchronized同步锁或使用同步代码块对类加同步锁,此这种方式虽然解决了多个实例对象问题,但是运行效率却很低下,下一个线程想要获取对象,就必须等待上一个线程释放锁之后,才可以继续运行。在此基础进行改进,得到双重检测机制,实现如下:

public class Singleton{
    private Singleton() {}
    private static Singleton instance = null;
    // 双重检查
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

       双重检测方式,首先判断单例对象是否为null,如果不为null,直接返回单例对象。如果单例对象尚未创建,则将创建单例对象的代码定义为同步块,防止多线程创建多个单例对象。相比于将整个方法加上同步锁,双重检测机制只对需要锁的代码部分加锁。在检查instance不为null,不需要执行加锁和初始化操作。因此可以大幅降低synchronized带来的性能开销。

     然而双重检测机制由于指令重排的现象,可能导致线程获取的单例对象为null指针。因此,实际上为非线程安全的方式,不建议使用。原因是创建对象的语句实际上并非一个原子操作,它在实际上可以分为三行伪代码:

//1、分配对象的内存空间
memory = allocate();
//2、初始化对象
ctorInstance(memory);  
//3、设置instance指向刚分配的内存地址
instance = memory; 

     上面代码的2和3,可能会被重排序(在一些JIT编译器中),即编译器或处理器为提高性能改变代码执行顺序,重排序之后的伪代码是这样的:

//1、分配对象的内存空间
memory = allocate(); 
//3、设置instance指向刚分配的内存地址
instance = memory;
//2、初始化对象
ctorInstance(memory);

      在单线程时,重排序不会对最终结果产生影响。但是并发的情况下,可能会导致某些线程访问到未初始化的变量。对于双重检测机制来说,假设有线程A和B准备获取单例对象,此时线程A判断单例对象未初始化,并执行到第9行创建单例对象。若线程A创建对象时发生指令重排,并执行完instance = memory让出CPU。注意,此时单例对象虽未初始化,但是已经指向内存某处地址,因此不为null。当线程B获取CPU使用权,并执行到第6行时,会误以为单例对象已创建,并返回一个未初始化的引用。

3.进化版双重检测机制

    双重检测机制根本问题是会出现指令重排,导致返回一个未初始化的单例对象。庆幸的是,Java提供了一个关键字volatile,可以禁止指令重排序,因此用volatile修饰单例对象,可以达到禁止指令重排的效果。

public class Singleton{
    private Singleton() {}
    //volatile修饰单例对象
    private static volatile Singleton instance = null;
    // 双重检查
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

  关于关键字volatile可以参考本人博客:https://blog.csdn.net/u012723673/article/details/80682208

4.静态内部类实现

     上述方式,要么是加载类时直接创建单例对象,没有线程安全的隐患,但是在不使用单例时浪费了资源;要么是延迟创建单例对象,直到需要时才创建,但是存在线程安全的隐患。因此,考虑有没有一种方式,能够同时兼顾两种方式的优点。既能在第一次使用时被创建,又能避免使用同步锁,保证高并发环境下的性能。而内部类和类的初始化方式就能同时满足上述要求,它在单例类中定义了一个私有的静态内部类用于创建单例对象,并在公共的getInstance方法中对内部类中的单例实例进行引用。实现如下:

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

     当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性

5.静态代码块实现

     了解JVM原理可知Java类的运行分为三步:装载、连接和初始化。类的静态代码块发生在类的初始化阶段,而类的初始化是类在被调用时才会发生:

(1)当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)

(2)当调用某个类的静态方法时

(3)当使用某个类或接口的静态字段时

(4)当调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时

(5)当初始化某个子类时

(6)当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)

   所以把创建单例对象的操作放在静态代码块中,只有单例类被使用的时候才会执行,且只执行一次。

public class Singleton{ 
	private static Singleton instance = null;	 
	private Singleton(){} 
    //在类装载的时候不会被执行,只有初始化的时候才执行
	static{
		instance = new Singleton();
	}
	public static Singleton getInstance() { 
		return instance;
	} 
 }

   静态内部类实现和静态代码块实现的区别:

      假设单例类中还有其他静态方法或者静态变量时。静态内部类实现方式在调用其他静态方法或变量时,不会创建单例对象。而静态代码块实现方式在调用其他静态方法或变量时(触发单例类的初始化),创建单例对象,此时可能并不需要使用单例对象。因此,静态内部类实现方式属于懒汉式,而静态代码块方式则属于变种的饿汉式。

四、破坏单例模式的攻击及解决方案

    上述这些方法没有考虑反射机制和序列化机制的情况。事实上,如果考虑了反射和序列化,则上述的单例方式就无法保证单例类只能有一个实例。例如,通过Java反射机制能够实例化构造方法为private的类。下面介绍破坏单例模式的场景。

1.反射破坏单例模式

    假设使用静态内部类方式实现单例模式,通过反射创建单例对象并验证一致性。

public static void main(String[] args) throws Exception {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton s3 = constructor.newInstance();
        System.out.println(s1+"\n"+s2+"\n"+s3);
        System.out.println("正常情况下,实例化两个实例是否相同:"+(s1==s2));
        System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(s1==s3));
}
    
结果为:
com.test.Singleton@15db9742
com.test.Singleton@15db9742
com.test.Singleton@6d06d69c
正常情况下,实例化两个实例是否相同:true
通过反射攻击单例模式情况下,实例化两个实例是否相同:false

解决方案:这种情况可以通过设置一个全局变量标志,当第二次创建时抛出异常。

public class Singleton{
    private static volatile boolean isCreate = false;//默认是第一次创建
    private Singleton() {
      //判断是否为第一次创建
      if(isCreate) {
         throw new RuntimeException("已被实例化一次,不能在实例化");
      }
	  isCreate = true;
    }
    private static class SingletonHolder{
        private static Singleton instance = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

2.序列化和反序列化破坏单例模式

     假设使用静态内部类实现单例模式,并且单例类实现了序列化接口Serializable,通过序列化和反序列化创建单例对象并验证一致性。

public static void main(String[] args) {
  //序列化
  Singleton s1 = Singleton.getInstance();
  File file = new File("Singleton.txt");
  FileOutputStream fos = new FileOutputStream(file);
  ObjectOutputStream oos = new ObjectOutputStream(fos);
  oos.writeObject(s1);
  //反序列化
  FileInputStream fis = new FileInputStream(file);
  ObjectInputStream ois = new ObjectInputStream(fis);
  Singleton s2 = (Singleton) ois.readObject();
  System.out.println(s1+"\n"+s2);
  System.out.println("通过序列化攻击单例模式情况下,实例化两个实例是否相同:"+(s1==s2));
}

结果为:
com.test.Singleton@5c647e05
com.test.Singleton@4c873330
通过序列化攻击单例模式情况下,实例化两个实例是否相同:false

解决方案:在反序列化的过程中使用readResolve()方法返回单例对象,实现的代码如下:

public class Singleton implements Serializable { 
	private static final long serialVersionUID = 1L;
    private Singleton(){}
	//内部类
	private static class SingletonHolder{
        private static Singleton instance = new Singleton();
	} 
	public static Singleton getInstance() { 
		return SingletonHolder.instance;
	}
	//该方法在反序列化时会被调用
	protected Object readResolve() throws ObjectStreamException {
		System.out.println("调用了readResolve方法!");
		return SingletonHolder.instance; 
	}
}

3.克隆方法破坏单例模式

     假设使用静态内部类实现单例模式,并且单例类实现了克隆接口Cloneable、重写了clone()方法,通过clone()方法创建单例对象并验证一致性。代码如下:

public class Singleton implements Cloneable{ 
    private Singleton(){}
	//内部类
	private static class SingletonHolder{
        private static Singleton instance = new Singleton();
	} 
	public static Singleton getInstance() { 
		return SingletonHolder.instance;
	}
	//重写clone方法,调用父类的clone
	@Override
	protected Object clone() throws CloneNotSupportedException {
		return super.clone();
	}
}

public static void main(String[] args) {
  //序列化
  Singleton s1 = Singleton.getInstance();
  Singleton s2 = (Singleton) s1.clone();
  System.out.println(s1+"\n"+s2);    
  System.out.println("通过克隆攻击单例模式情况下,实例化两个实例是否相同:"+(s1==s2));
}
结果为:
com.test.Singleton@33909752
com.test.Singleton@4c873330
通过克隆攻击单例模式情况下,实例化两个实例是否相同:false

解决方案:重写clone()方法,直接返回单例对象

public class Singleton implements Cloneable{ 
    private Singleton(){}
	//内部类
	private static class SingletonHolder{
        private static Singleton instance = new Singleton();
	} 
	public static Singleton getInstance() { 
        return SingletonHolder.instance;
	}
	//重写clone方法,直接返回单例对象
	@Override
	protected Object clone() throws CloneNotSupportedException {
       return SingletonHolder.instance;
	}
}

五、枚举单例模式(最优美的单例实现方式)

      之所以将枚举方式的单例单独列出来,是因为该方式不仅简洁优美,而且能够免受攻击破坏。枚举类方式实现单例模式是Effective Java作者Josh Bloch 提倡的方式,具有以下优点:

(1)代码简洁优美

(2)线程安全

(2)能够防止反射和序列化破坏单例,同时禁止重写clone方法

1.枚举类单例如何防止克隆

枚举类可以实现Cloneable接口,但是禁止重写clone()方法,因此其他对象不能通过克隆的方式创建单例对象。

      这是因为Enum类已经将clone()方法定义为final了,并且Enum在使用clone()时直接抛出异常,这就是枚举为什么能防止克隆破环的原因。

2.枚举类单例如何防止反射

     反射实现的主要步骤:首先通过class对象的getDeclaredConstructor()获取到反射对象的构造器,然后通过newInstance()调用其构造方法获取对象,具体代码如下:

/通过反射获取
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton reflex = constructor.newInstance();

上述代码在创建对象时,会直接抛出异常throw new NoSuchMethodException。具体如下:

Exception in thread "main" java.lang.NoSuchMethodException: com.lxp.pattern.singleton.EnumSingleton.<init>()
 at java.lang.Class.getConstructor0(Class.java:3082)
 at java.lang.Class.getDeclaredConstructor(Class.java:2178)
 ...

这是因为枚举类的getDeclaredConstructors()获取所有构造器,会发现并没有我们所设置的无参构造器,只有一个参数为(String.class,int.class)构造器。查看Enum源码中的构造方法,可知这两个参数是name和ordial两个属性。

public abstract class Enum<E extends Enum<E>>
            implements Comparable<E>, Serializable {
        private final String name;
        private final int ordinal;
        protected Enum(String name, int ordinal) {
            this.name = name;
            this.ordinal = ordinal;
        }
        //余下省略
}

     枚举Enum是个抽象类,一旦某个类声明为枚举,实际上就是继承了Enum,所以会有(String.class,int.class)的构造器。既然可以获取到父类Enum的构造器,那么上述问题是否因为自身类没有无参构造方法才导致的异常。直接使用父类Enum的构造,是否会反射成功呢?

Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor();
constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);//其父类的构造器
constructor.setAccessible(true);
EnumSingleton singleton = constructor.newInstance("testInstance",11);

运行结果依旧出错,异常报错如下:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    ...

进一步查看Constructor类的newInstance方法源码发现:

@CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

12行处的代码表明反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。

3.枚举类单例如何防止序列化

     Java的序列化机制针对枚举类型是特殊处理的。简单来讲,在序列化枚举类型时,只会存储枚举类的引用和枚举常量的名称。随后的反序列化的过程中,这些信息被用来在运行时环境中查找存在的枚举类型对象。

     这样就可以在同一个运行时环境中反序列化枚举常量,并且会得到同一个实例对象。然而,在不同的JVM中对枚举类型进行反序列化,可能会得到不同的hashcode。但是对单例对象来说,拥有相同的hashcode并不是一个必要的条件。重点是该类永远不能有多余一个的实例(同一个JVM),枚举类型的序列化机制保证只会查找已经存在的枚举类型实例,而不是创建新的实例。

六、单例模式的应用

1.单例模式的优点

(1)单例模式在内存中只有一个实例,减少了内存开支。特别是在一个对象需要频繁地创建、销毁,且创建或销毁的性能无法优化时,使用单例模式优势很明显。

(2)单例模式只生成一个实例,减少了系统的性能开销。当一个对象产生需要比较多的系统资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,并永久驻留内存的方式来解决。

(3)单例模式能够避免对资源的多重占用。例如写文件操作,由于只有一个实例,避免同时对一个资源文件的写操作。

(4)单例模式可以在系统设置全局的访问点,优化和共享资源访问。

2.单例模式的缺点

(1)单例模式没有接口,扩展很困难。若要扩展,只能修改源代码。

(2)单例模式对测试不利。在并发开发环境中,如果单例模式没有创建完成,是不能进行测试的。

3.单例模式的应用场景

在一个系统中,要求一个类有且仅有一个对象,并且出现多个对象会产生不好的结果,就可以采用单例模式。例如:

(1)在整个项目中需要一个共享访问点或共享数据。

(2)创建一个对象需要消耗资源过多,如需要访问IO和数据库等资源。

(3)需要定义大量静态常量和静态方法(如工具类)的环境。

具体应用有:

(1)任务管理器就是很典型的单例模式。不能同时打开两个任务管理器

(2)回收站也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

(3)网站的计数器,一般也是采用单例模式实现,否则难以同步。

(4)数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的时间和资源的损耗。

(5)多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。

七、单例模式总结

      单例模式的实现有饿汉式、懒汉式、变种懒汉式(加同步锁、双重检测锁、静态内部类)、枚举类。各种实现方式的优缺点及适用场景如下:

 优点缺点适用场景
饿汉式简单方便,天生线程安全

不管是否使用了单例对象,都会生成单例对象。且由于静态对象是在类加载时生成,会降低应用的启动速度。

适用:类对象的功能简单,占用内存小,使用频繁

不适用:类对象功能复杂,占用内存大,使用效率低

懒汉式

使用时创建,应用的启动速度快

不是线程安全的,如果多线程同时调用,可能会产生多个单例对象

适用:单例功能复杂,占用内存大,对应用启动速度有要求

不适用:多线程环境

同步锁同懒汉式加锁有额外开销 
双重检测懒加载,线程安全,效率高

可能发生指令重排,可以通过volatile解决

 
静态内部类

实现简单,懒加载,线程安全

增加了静态内部类

 
枚举

线程安全,有效防止攻击破坏

枚举占用内存会多一点

 

推荐:在单例对象功能简单,占用内存小,且使用频繁时,建议使用饿汉式

          在单例对象功能复杂,占用内存大,且使用频率低时,建议使用懒汉式中的静态内部类实现

          在对单例模式的安全性有要求,能够有效防止攻击破坏时,建议使用枚举类。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

老鼠只爱大米

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

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

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

打赏作者

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

抵扣说明:

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

余额充值