单例模式的几种写法

2018-08-18  本文已影响0人  小杰的快乐时光

(1)饿汉模式

public class Singleton{
    private Singleton(){};
    private static final Singleton INSTANCE = new Singleton();
    public static Singleton getInstance(){
        renturn INSTANCE;
    }
}

分析: 第一次加载到内存中的就会被初始化 ,并对外提供一个获取该实例对象的方法,优点:立即执行初始化,线程安全。缺点:会受到反射,序列化影响,浪费资源

(2)懒汉模式:线程不安全,单线程下只在第一次调用时才会实例化

public class Singleton{
    private Singleton(){};
    private static Singleton singleton = null;
    public static Singleton getInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

懒汉加锁模式(1):线程安全,但是每次调用都会加锁,效率很低

public class Singleton{
    private Singleton(){};
    private static  Singleton singleton = null;
    public static synchronized Singleton getInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

双重检查锁的懒汉单例模式(2):保证线程安全,只有在singleton为null时才会加锁,优化锁带来的消耗

public class Singleton{
    private static volatile Singleton singleton = null;
    private Singleton(){};
    public static Singleton getInstance(){
        if(singleton == null){
            synchronized(Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

(3)静态内部类延迟初始化,线程安全

public class Singleton {
   private Singleton(){};
   private static class initInstance{
      private static final Singleton SINGLETON = new Singleton();
   }
   public static Singleton getSingleton(){
      return initInstance.SINGLETON;
   }
}

分析:根据JVM规范,当第一次调用Singleleton.getInstance( )时,Singleleton被首次调用,JVM对其进行初始化操作,此时不会调用Singleleton的构造方法。接下来会调用getInstance()方法,又首次主动使用initInstance()内部类,所以JVM会对initInstance()进行初始化操作,在这个初始化操作中,静态常量INSTANCE被赋值时才调用Singleton()构造方法,完成实例化并返回该对象。
当再次调用Singleleton.getInstance( )时,因为已经进行过初始化操作了,就不会再进行初始化操作,直接返回INSTANCE常量。

解决反射与序列化的影响思路:(以饿汉单例模式为例)

public class Singleton{
   private Singleton(){
      //在私有的无参构造器中进行判断,这样可以解决反射问题
      if (INSTANCE != null){
         throw new RuntimeException("禁止反射创建单例模式");
      }
   };
   private static final Singleton INSTANCE = new Singleton();
   public static Singleton getInstance(){
      return INSTANCE;
   }
   //添加readResolve()方法可以防止序列化问题
   private Object readResolve() throws ObjectStreamException{
      return INSTANCE;
   }
}

枚举类单例模式:可以满足线程安全,天生防止序列化生成新的实例,防止反射攻击

public class Singleton{}

public enum SingletonEnum{
    INSTANCE;
    private Singleton singleton = null;
    private SingletonEnum(){
        singleton = new Singleton();
    }
    public Singleton getInstance(){
        return singleton;
    }
}

调用:
Singleton singleton = SingletonEnum.INSTANCE.getInstance();

Enum是一个普通的类,继承自Java.lang.Enum类

public enum SingletonEnum{
    INSTANCE;
}

将上面枚举编译后的字节码反编译,得到如下代码:

public abstract class SingletonEnum extends Enum<SingletonEnum>{
    public static final SingletonEnum INSTANCE;
    public static SIngletonEnum[]values();
    public static SingletonEnum valuesof(String s);
    static{};
}

通过反编译后得到的代码可知:
INSTANCE 被声明为static,虚拟机会保证一个类的<clinit>() 方法在多线程环境中被正确的加锁、同步。所以,枚举实现是在实例化时是线程安全。

接下来再看序列化的问题
在Java的规范中,每一个枚举类型及其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化与反序列化中,JVM做了一些特殊的规定:在序列化时,Java仅仅只是将枚举对象的name属性输出到结果中,反序列化时则是通过Java.lang.Enum的valuesof()方法根据名称获取枚举对象。
比如:

public enum SingletonEnum{
    INSTANCE;
}

上面枚举类在序列化时,只将 INSTANCE 这个名称输出,反序列化时,再通过这个名称,寻找对应的枚举类型,因此反序列化后的实例与之前被序列化的对象实例相同

接下来看反射的问题
枚举单例

public enum Singleton {
INSTANCE {

@Override
protected void read() {
System.out.println("read");
}

@Override
protected void write() {
System.out.println("write");
}

};
protected abstract void read();
protected abstract void write();
}

反编译后

public abstract class Singleton extends Enum
{

    private Singleton(String s, int i)
    {
        super(s, i);
    }

    protected abstract void read();

    protected abstract void write();

    public static Singleton[] values()
    {
        Singleton asingleton[];
        int i;
        Singleton asingleton1[];
        System.arraycopy(asingleton = ENUM$VALUES, 0, asingleton1 = new Singleton[i = asingleton.length], 0, i);
        return asingleton1;
    }

    public static Singleton valueOf(String s)
    {
        return (Singleton)Enum.valueOf(singleton/Singleton, s);
    }

    Singleton(String s, int i, Singleton singleton)
    {
        this(s, i);
    }

    public static final Singleton INSTANCE;
    private static final Singleton ENUM$VALUES[];

    static
    {
        INSTANCE = new Singleton("INSTANCE", 0) {

            protected void read()
            {
                System.out.println("read");
            }

            protected void write()
            {
                System.out.println("write");
            }

        };
        ENUM$VALUES = (new Singleton[] {
            INSTANCE
        });
    }
}

①类的修饰为abstract,没法实例化
②在static中完成实例化的,所以线程安全

从源码上来看
java.lang.reflect 中的 Constructor 的newInstance中规定了不准通过反射创建枚举对象
(反射在通过newInstance创建对象时,会检查该类是否是枚举类,如果是,则抛出异常,反射失败。)

Constructor 的newInstance.png

参考文章:
singleton模式四种线程安全的实现

上一篇下一篇

猜你喜欢

热点阅读