public enum Singleton{instance;public void whateverMethod(){}}
【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》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。
高手之间的过招,必选择枚举单例模式。
下面附上测试枚举类解决序列化和反射问题的代码块,有兴趣的同学可以跟源码看一下为什么枚举类能解决此问题
// 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);*/}}
编辑:郭宇璐
终审:任广一
