vlambda博客
学习文章列表

【JAVA技术应用】—— 单例模式


各位双体的小伙伴们,


今天同样为大家整理了


满满的干货小知识


跟随我来一起了解下吧!

JAVA单例模式

饿汉式(线程安全)

懒汉式(线程安全)

静态内部类(线程安全)

双重检验锁

枚举单例


Java中单例(Singleton)模式是一种广泛使用的设计模式。单例模式的主要作用是保证在Java程序中,某个类只有一个实例存在。一些管理器和控制器常被设计成单例模式。


单例模式有很多好处,它能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间;能够避免由于操作多个实例导致的逻辑错误。如果一个对象有可能贯穿整个应用程序,而且起到了全局统一管理控制的作用,那么单例模式也许是一个值得考虑的选择。


单例模式有很多种写法,大部分写法都或多或少有一些不足。下面将分别对这几种写法进行介绍。



饿汉式(线程安全)
public class Singleton{ private static final Singleton instance = new Singleton(); private Singleton(){} public static Singleton newInstance(){ return instance; }



从代码中我们看到,类的构造函数定义为private的,保证其他类不能实例化此类,然后提供了一个静态实例并返回给调用者。


饿汉模式是最简单的一种实现方式,饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在。它的好处是只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。它的缺点也很明显,即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。


这种实现方式适合单例占用内存比较小,在初始化时就会被用到的情况。但是,如果单例占用的内存比较大,或单例只是在某个特定场景下才会用到,使用饿汉模式就不合适了,这时候就需要用到懒汉模式进行延迟加载。



懒汉式(线程安全)
public class Singleton {  private static Singleton instance;  private Singleton (){}  public static synchronized Singleton getInstance() {  if (instance == null) {  instance = new Singleton();  }  return instance;  } 



懒汉模式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候使用懒汉模式就是一个不错的选择。


这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。



静态内部类(线程安全)
public class Singleton {  private Singleton(){ } public static Singleton getSingleton(){  return Inner.instance;  }  private static class Inner {  private static final Singleton instance = new Singleton();  } 


这种方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。


实现代码简洁。和双重检查单例对比,静态内部类单例实现代码真的是太简洁,又清晰明了。


延迟初始化。调用getSingleton才初始化Singleton对象。


线程安全。JVM在执行类的初始化阶段,会获得一个可

以同步多个线程对同一个类的初始化的锁。



双重检验锁
public class Singleton {  private volatile static Singleton singleton; //1:volatile修饰 private Singleton (){}  public static Singleton getSingleton() {  if (singleton == null) { //2:减少不要同步,优化性能 synchronized (Singleton.class) { // 3:同步,线程安全 if (singleton == null) {  singleton = new Singleton(); //4:创建singleton 对象 }  }  }  return singleton;  } }



延迟初始化。和懒汉模式一致,只有在初次调用静态方法getSingleton,才会初始化signleton实例。


性能优化。同步会造成性能下降,在同步前通过判读singleton是否初始化,减少不必要的同步开销。


线程安全。同步创建Singleton对象,同时注意到静态变量singleton使用volatile修饰。


为什么要使用volatile修饰?


虽然已经使用synchronized进行同步,但在第4步创建对象时,会有下面的伪代码:

memory=allocate(); //1:分配内存空间ctorInstance(); //2:初始化对象singleton=memory; //3:设置singleton指向刚排序的内存空间


当线程A在执行上面伪代码时,2和3可能会发生重排序,因为重排序并不影响运行结果,还可以提升性能,所以JVM是允许的。如果此时伪代码发生重排序,步骤变为1->3->2,线程A执行到第3步时,线程B调用getsingleton方法,在判断singleton==null时不为null,则返回singleton。但此时singleton并还没初始化完毕,线程B访问的将是个还没初始化完毕的对象。当声明对象的引用为volatile后,伪代码的2、3的重排序在多线程中将被禁止!


volatile如何实现防止指令重排序的呢?volatile是否还有别的作用的,有兴趣的同学可以去了解一下


上面提到的四种实现单例的方式都有共同的缺点:


1.需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。


2.可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。

而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。


因此《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。



枚举单例
public enum Singleton{ instance; public void whateverMethod(){} }



高手之间的过招,必选择枚举单例模式。

下面附上测试枚举类解决序列化和反射问题的代码块,有兴趣的同学可以跟源码看一下为什么枚举类能解决此问题

// Copyright 2016-2101 Pica.package com.pica.cloud.base.api.workstation.server.service;
import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;
/** * @ClassName SingleTon * @Description TODO * @Author Chongwen.jiang * @Date 2020/5/10 10:46 * @ModifyDate 2020/5/10 10:46 * @Version 1.0 */public class SingleTon implements Serializable{ private SingleTon(){}
private volatile static SingleTon instance;
private String content;
public static SingleTon getInstance(){ if(instance == null) { synchronized (SingleTon.class) { if(instance == null) { instance = new SingleTon(); } } } return instance; }
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException { SingleTon s1 = SingleTon.getInstance(); s1.setContent("内容答复"); /*SingleTon s2 = SingleTon.getInstance(); Constructor<SingleTon> constructor = SingleTon.class.getDeclaredConstructor(); constructor.setAccessible(true);
SingleTon s3 = constructor.newInstance(); System.out.println(s1 == s2); System.out.println(s1 == s3);*/
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("SingleTon.obj")); os.writeObject(s1); os.flush(); os.close();
FileInputStream is = new FileInputStream("SingleTon.obj"); ObjectInputStream oss = new ObjectInputStream(is); SingleTon s4 = (SingleTon)oss.readObject(); oss.close(); is.close(); System.out.println(s4.getContent()); System.out.println(s1 == s4); }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }}
// Copyright 2016-2101 Pica.package com.pica.cloud.base.api.workstation.server.service;
import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;
/** * @ClassName EnumSingleTon * @Description TODO * @Author Chongwen.jiang * @Date 2020/5/10 11:17 * @ModifyDate 2020/5/10 11:17 * @Version 1.0 */public enum EnumSingleTon implements Serializable { INSTANCE; EnumSingleTon(){}
private String content;
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException { EnumSingleTon s1 = EnumSingleTon.INSTANCE; s1.setContent("内容答复"); EnumSingleTon s2 = EnumSingleTon.INSTANCE; Constructor<EnumSingleTon> constructor = EnumSingleTon.class.getDeclaredConstructor(String.class, int.class); constructor.setAccessible(true);
EnumSingleTon s3 = constructor.newInstance(); System.out.println(s1 == s2); System.out.println(s1 == s3);
/*ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("EnumSingleTon.obj")); os.writeObject(s1); os.flush(); os.close();
FileInputStream is = new FileInputStream("EnumSingleTon.obj"); ObjectInputStream oss = new ObjectInputStream(is); EnumSingleTon s4 = (EnumSingleTon)oss.readObject(); oss.close(); is.close(); System.out.println(s4.getContent()); System.out.println(s1 == s4);*/ }}



编辑:郭宇璐

终审:任广一