天天看点

内部类的单例模式

* 上面的单例实现存在小小的缺陷,那么 有没有一种方法,既能够实现延迟加载,又能够

 * 实现线程安全呢?

 *       还真有高人想到这样的解决方案了,这个解决方案被称为Lazy initialization

 * holder class 模式,这个模式综合使用了java的类级内部类和多线程缺省同步锁的知识,

 * 很巧妙的同时实现了延迟加载和线程安全。

 *

 *

 * 1 相应的基础知识

 *  (1)什么是类级内部类?

 *   简单点说,类级内部类指的是,有static修饰的成员内部类。如果没有static修饰的成员式内

 *   部类被称为对象级内部类。

 *   (2)类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此

 *   可以直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。

 *   (3)类级内部类中,可以定义静态的方法。在静态方法中只能引用外部类中的静态成员方法或变量。

 *   (4)类级内部类相当于其外部类的成员,只有在第一次被使用的时候才会被装载。

 *  

 *   多线程缺省同步锁的知识:

 *   大家都知道,在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制,

 *   但是在某些情况下,JVM已经隐含的为您执行了同步,这些情况下就不用自己再来进行同步控制了。

 *   这些情况包括:

 *   (1)由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时

 *   (2)访问final字段时

 *   (3)在创建线程之前创建对象时

 *   (4)线程可以看见它将要处理的对象时

 *  

 *  

 *   2 解决方案的思路

 *        要想很简单的实现线程安全,可以采用静态初始化器的方式,它可以由JVM来保证线程的

 *   安全性。比如前面的饿汉式实现方式。但是这样一来,不是会浪费一定的空间吗?因为这种

 *   实现方式,会在类装载的时候就初始化对象,不管你需不需要。

 *        如果现在有一种方法能够让类装载的时候不去初始化对象,那不就解决问题了?一种可行的

 *   方式就是采用类级内部类,在这个类级内部类里面去创建对象实例。这样一来,只要不使用到这个类级内部类,

 *   那就不会创建对象实例,从而同步实现延迟加载和线程安全。

public class Singleton_InnerClass {
         
  private static class SingletonHolder{
   //静态初始化器,由JVM来保证线程安全
   private static Singleton_InnerClass instance=new Singleton_InnerClass();
  }
  
  //私有化构造方法
  private Singleton_InnerClass(){
   
  }
  
  public static Singleton_InnerClass getInstance(){
   return SingletonHolder.instance;
  }
 }      

===========================================================================

最近在看何红辉、关爱民著的《​​Android​​源码设计模式解析与实战》,一边学习,一边理解,一边记笔记。

1.定义

确保某个类只有一个实例,能自行实例化并向整个系统提供这个实例。

2.应用场景

  1. 当产生多个对象会消耗过多资源,比如IO和数据操作
  2. 某种类型的对象只应该有且只有一个,比如Android中的Application。

3.考虑情况

  1. 多线程造成实例不唯一。
  2. 反序列化过程生成了新的实例。

4.实现方式

4.1普通单例模式

/**
 * 普通模式
 * @author josan_tang
 */
public class SimpleSingleton {
    //1.static单例变量
    private static SimpleSingleton instance;

    //2.私有的构造方法
    private SimpleSingleton() {

    }

    //3.静态方法为调用者提供单例对象
    public static SimpleSingleton getInstance() {
        if (instance == null) {
            instance = new SimpleSingleton();
        }
        return instance;
    }
}      

在多线程高并发的情况下,这样写会有明显的问题,当线程A调用getInstance方法,执行到16行时,检测到instance为null,于是执行17行去实例化instance,当17行没有执行完时,线程B又调用了getInstance方法,这时候检测到instance依然为空,所以线程B也会执行17行去创建一个新的实例。这时候,线程A和线程B得到的instance就不是一个了,这违反了单例的定义。

4.2 饿汉单例模式

/**
 * 饿汉单例模式
 * @author josan_tang
 */
public class EHanSingleton {
    //static final单例对象,类加载的时候就初始化
    private static final EHanSingleton instance = new EHanSingleton();

    //私有构造方法,使得外界不能直接new
    private EHanSingleton() {
    }

    //公有静态方法,对外提供获取单例接口
    public static EHanSingleton getInstance() {
        return instance;
    }
}      

饿汉单例模式解决了多线程并发的问题,因为在加载这个类的时候,就实例化了instance。当getInstatnce方法被调用时,得到的永远是类加载时初始化的对象(反序列化的情况除外)。但这也带来了另一个问题,如果有大量的类都采用了饿汉单例模式,那么在类加载的阶段,会初始化很多暂时还没有用到的对象,这样肯定会浪费内存,影响性能,我们还是要倾向于4.1的做法,在首次调用getInstance方法时才初始化instance。请继续看4.3用法。

4.3懒汉单例模式

import java.io.Serializable;

/**
 * 懒汉模式
 * @author josan_tang
 */
public class LanHanSingleton {
    private static LanHanSingleton instance;

    private LanHanSingleton() {

    }

    /**
     * 增加synchronized关键字,该方法为同步方法,保证多线程单例对象唯一
     */
    public static synchronized LanHanSingleton getInstance() {
        if (instance == null) {
            instance = new LanHanSingleton();
        }
        return instance;
    }
}      

与4.1的唯一区别在于getInstance方法前加了synchronized 关键字,让getInstance方法成为同步方法,这样就保证了当getInstance第一次被调用,即instance被实例化时,别的调用不会进入到该方法,保证了多线程中单例对象的唯一性。

优点:单例对象在第一次调用才被实例化,有效节省内存,并保证了线程安全。

缺点:同步是针对方法的,以后每次调用getInstance时(就算intance已经被实例化了),也会进行同步,造成了不必要的同步开销。不推荐使用这种方式。

4.4 Double CheckLock(DCL)单例模式

/**
 * Double CheckLock(DCL)模式
 * @author josan_tang
 *
 */
public class DCLSingleton {
    //增加volatile关键字,确保实例化instance时,编译成汇编指令的执行顺序
    private volatile static DCLSingleton instance;

    private DCLSingleton() {

    }

    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                //当第一次调用getInstance方法时,即instance为空时,同步操作,保证多线程实例唯一
                //当以后调用getInstance方法时,即instance不为空时,不进入同步代码块,减少了不必要的同步开销
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}      

DCL失效:

在JDK1.5之前,可能会有DCL实现的问题,上述代码中的20行,在​​Java​​里虽然是一句代码,但它并不是一个真正的原子操作。

instance = new DCLSingleton();      

它编译成最终的汇编指令,会有下面3个阶段:

  1. 给DCLSingleton实例分配内存
  2. 调用DCLSingleton的构造函数,初始化成员变量。
  3. 将instance指向分配的内存空间(这个操作以后,instance才不为null)

在jdk1.5之前,上述的2、3步骤不能保证顺序,也就是说有可能是1-2-3,也有可能是1-3-2。如果是1-3-2,当线程A执行完步骤3(instance已经不为null),但是还没执行完2,线程B又调用了getInstance方法,这时候线程B所得到的就是线程A没有执行步骤2(没有执行完构造函数)的instance,线程B在使用这样的instance时,就有可能会出错。这就是DCL失效。

在jdk1.5之后,可以使用volatile关键字,保证汇编指令的执行顺序,虽然会影响性能,但是和程序的正确性比起来,可以忽悠不计。

Java内存模型

优点:第一次执行getInstance时instance才被实例化,节省内存;多线程情况下,基本安全;并且在instance实例化以后,再次调用getInstance时,不会有同步消耗。

缺点:jdk1.5以下,有可能DCL失效;Java内存模型影响导致失效;jdk1.5以后,使用volatile关键字,虽然能解决DCL失效问题,但是会影响部分性能。

4.5 静态内部类单例模式

/**
 * 静态内部类实现单例模式
 * @author josan_tang
 *
 */
public class StaticClassSingleton {
    //私有的构造方法,防止new
    private StaticClassSingleton() {

    }

    private static StaticClassSingleton getInstance() {
        return StaticClassSingletonHolder.instance;
    }

    /**
     * 静态内部类
     */
    private static class StaticClassSingletonHolder {
        //第一次加载内部类的时候,实例化单例对象
        private static final StaticClassSingleton instance = new StaticClassSingleton();
    }
}      

第一次加载StaticClassSingleton类时,并不会实例化instance,只有第一次调用getInstance方法时,Java虚拟机才会去加载StaticClassSingletonHolder类,继而实例化instance,这样延时实例化instance,节省了内存,并且也是线程安全的。这是推荐使用的一种单例模式。

4.6 枚举单例模式

/**
 * 枚举单例模式
 * @author josan_tang
 *
 */
public enum EnumSingleton {
    //枚举实例的创建是线程安全的,任何情况下都是单例(包括反序列化)
    INSTANCE;

    public void doSomething(){

    } 
}      

枚举不仅有字段还能有自己的方法,并且枚举实例创建是线程安全的,就算反序列化时,也不会创建新的实例。除了枚举模式以外,其他实现方式,在反序列化时都会创建新的对象。

为了防止对象在反序列化时创建新的对象,需要加上如下方法:

private Object readResole() throws ObjectStreamException {
        return instance;
    }      

这是一个钩子函数,在反序列化创建对象时会调用它,我们直接返回instance就是说,不要按照默认那样去创建新的对象,而是直接将instance返回。

4.7 容器单例模式

import java.util.HashMap;
import java.util.Map;

/**
 * 容器单例模式
 * @author josan_tang
 */
public class ContainerSingleton {
    private static Map<String, Object> singletonMap = new HashMap<String, Object>();

    //单例对象加入到集合,key要保证唯一性
    public static void putSingleton(String key, Object singleton){
        if (key != null && !"".equals(key) && singleton != null) {
            if (!singletonMap.containsKey(key)) {   //这样防止集合里有一个类的两个不同对象
                singletonMap.put(key, singleton);   
            }
        }
    }

    //根据key从集合中得到单例对象
    public static Object getSingleton(String key) {
        return singletonMap.get(key);
    }
}      

在程序初始化的时候,讲多种单例类型对象加入到一个单例集合里,统一管理。在使用时,通过key从集合中获取单例对象。这种方式多见于系统中的单例,像​​安卓​​中的系统级别服务就是采用集合形式的单例模式,比如常用的LayoutInfalter,我们一般在Fragment中的getView方法中使用如下代码:

View view = LayoutInflater.from(context).inflate(R.layout.xxx, null);      

其实LayoutInflater.from(context)就是得到LayoutInflater实例,看下面的Android源码:

/**
     * Obtains the LayoutInflater from the given context.
     */
    public static LayoutInflater from(Context context) {
        //通过key,得到LayoutInflater实例
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }      

总结

继续阅读