Nivelle 开拓视野冲破艰险看见世界 身临其境贴近彼此感受生活

单例模式

2018-08-24

概念:

单例模式的实现主要以下两个步骤:

  • 将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例
  • 在该类内提供一个静态方法,当我们调用这个方法时,如果该类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。

懒汉模式(一)

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

优点:懒加载

缺点:

(1)并发情况下线程不安全。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。

(2) 不能阻止反射攻击

懒汉模式(二)


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


缺点:虽然实现了线程安全性,但是这种实现方法导致每次访问getInstance()方法时都要加锁,效率大大下降。

饿汉模式

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

优点:

  • 线程安全,由类加载机制保证
  • 因为调用方法获取实例时不需要进行同步,相比需要线程同步的实现方式效率更高

缺点:

  • 非懒加载,类初始化就创建实例对象,而不等到需要该对象时调用getInstance()方法才创建对象,导致提早创建对象,如果对象很大时而之后不需要用
  • 即使构造函数是私有的,但是却可以利用反射机制去访问构造函数从而创建处不只一个对象

双重检查

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

优点:

  • 线程安全,效率高,懒加载

缺点:

  • 不能阻止反射攻击
DCL的线程安全性是通过加锁来保证的,而且加锁的过程只是发生在第一次获取对象的时候,所以效率还是很高。还有一个重要的点就是instance字段要用volatile修饰来禁止指令重排。

对singleton进行两次判null检查,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例。

双重检查的volatile关键字:

对singleton变量使用voldtile关键字的原因是,instance = new Singleton(),并非是一个原子操作,事实上jvm中这句话事实上做了三件事:

  • 给instance分配内存
  • 调用Singleton的构造函数来初始化成员变量
  • 将instance对象指向分配的内存空间(这时instance就为非null了)

但是在 JVM的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是1-2-3也可能是1-3-2。如果是后者,则在3执行完毕、2未执行之前,被线程二抢占了,这时instance已经是非null了(但却没有初始化),所以线程二会直接返回instance,然后使用,然后顺理成章地jvm就会报错。

使用volatile的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在volatile变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。

静态内部类模式

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

优点:

  • 这种方式利用了classloder的机制来保证初始化instance时只有一个线程,这种方式时Singleton类被装载了,instance不一定被初始化。因为SingleHolder类没有主动使用,只有显示通过getInstance方法时,才会显示装载SingleHolder类,从而实例化instance.

枚举模式

enum Singleton{
    INSTANCE;
}


详解:

  1. 当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承;
  2. 所有属性都是static的,因为static类型的属性在类被加载之后初始化,当一个类第一次被真正使用到的时候静态资源被初始化,java类的加载和初始化都是线程安全的,所以创建一个enum是线程安全的;
  3. 枚举常数的序列化表单仅由其name属性构成,常数的字段值不存在于表单中;在反序列化枚举常量时,通过ObjectInputStream对象从相关的流中读取枚举常量的name属性值(也就是枚举常量的名称),然后通过调用java.lang.Enum对象的valueOf方法,将从流中获取的枚举常量的枚举类型(指的是继承了Enum类的相关的子类)及其常量名称(也就是枚举常量的名称)一起作为该方法的参数传递给该valueOf方法。
  4. 每一个枚举类型极其定义的枚举变量在JVM中都是唯一的

优点:

  • 枚举里面的INSTANCE 字段是public static final Singleton 类型的,线程安全性有类加载阶段保证,因此不需要进行额外的同步操作
  • 反射机制已经针对了枚举类型的对象做了处理,可以保证不能通过反射机制创建多个对象,所以可以阻止反射攻击
  • 序列化机制也对枚举类型的对象做了处理,在序列化的时候仅仅将枚举对象的name属性序列化,反序列化的时候时候则是通过Enum的valueOf()方法来根据名字查找枚举对象。因此反序列化后的实例也会和之前被序列化的对象实例相同。

如果面对反射和序列化对单例模式的破坏:

  1. 对于反射来说,虽然构造函数是私有的,但反射可以通过访问私有的构造函数来创建多个对象。既然是单例模式,那么我们可以在创建第二个抛出异常

  2. 要让对象支持序列化操作,该对象的类需要implementsSerializable,并且在类中增加一个方法private ObjectreadResolve(),反序列化后获得的并不是原来的对象,而是经过重构的新的对象实例,resolve()这个方法会在反序列化完后调用,其中返回的对象替代了原来反序列化生成的对象。防止通过反序列化机制实现不同的实例。实际上就是用readResolve()中返回的对象直接替换在反序列化过程中重构的对象。

  3. 如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。


上一篇 内部类

下一篇 垃圾

评论