设计模式之单例

155 阅读8分钟

image.png 一、概念

单例的本质: 全局有且只有一个对象,并能够全局访问得到。

单例的优点: 系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。

单例的缺点: 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。

适用场合

  • 控制实例数量
  • 需要频繁的进行创建和销毁的对象;
  • 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
  • 工具类对象;
  • 频繁访问数据库或文件的对象。

实际应用:

Android Universal Image Loader

这是因为它需要缓存图片,对缓存的图片集合做各种操作,需要关注单例中的对象状态,而且明显是需要访问资源的。这就很契合单例的特性。

EventBus

因为它内部缓存了各个组件发送过来的event对象,并负责分发出去,各个组件需要向同一个EventBus对象注册自己,才能接收到event事件,肯定是需要全局唯一的对象,所以采用了单例。EventBus的单例采用的是双重检查加锁单例。

单例的替代

回到开发的场景中,思考我们为什么需要单例。如果是需要提供一个全局的访问点用getInstance()做些操作。除了单例我们还有其他的选择吗?

回去翻看Android源码,有这样一个类。java.lang.Math类它提供对数字的操作和方法计算,它的实现就是全部方法用static修饰符包装提供类级访问。因为当我们调用Math类时只要它的某个类方法做数据操作并不关心对象状态。

单例不需要维护任何状态,仅仅提供全局访问的方法,这种情况考虑使用静态类,静态方法比单例更快,因为静态的绑定是在编译期就进行。

如果你需要将一些工具方法集中在一起时,你可以选择使用静态方法,但是别的东西,要求单例访问资源并关注对象状态时,应该使用单例模式。

二、代码

1、懒汉式

public class Singleton {     
    //定义一个变量类存储创建好的实例对象,默认为空     
    //因为需要在静态方法中访问,加上static修饰符     
    private static Singleton instance =null;     
    //私有化构造方法,防止外部创建对象     
    private Singleton() {    
    }     
    //提供一个类静态方法返回单例对象,可以全局访问     
    public static Singleton getInstance(){         
        //懒就在这 当第一次有方法访问才创建实例         
        //但是之后不会初始化对象        
        if (instance==null){             
            instance=new Singleton();         
        }         
        return instance;     
    } 
}

2、饿汉式

static修饰的变量在类装载时进行初始化。

多个实例的static变量会共享同一块内存区域

即static变量只会在类装载的时候初始化一次,并且多个实例共享内存区域,非常符合单例的需求。

饿汉的单例模式重点就在于饿,一饿就急就会急着用,没有访问也创建对象。

public class SingletonHunger {     
    //定义变量存储创建好的实例     
    //并且创建对象只一次,在类加载的时候    
    private static SingletonHunger instance = new SingletonHunger();    
    //私有化构造方法,防止外部创建对象     
    private SingletonHunger() {   
    }     
    //提供一个类静态方法返回单例对象,可以全局访问     
    public static SingletonHunger getInstance() {         
        //类加载机制保证了实例的创建,就不需要做判断,直接返回        
        return instance;     
    } 
}

饿汉单例模式线程安全。

线程安全

1.饿汉式单例是线程安全的,因为虚拟机类加载机制保证了只会创建一次实例,并且装载类的时候不会发生并发。

2.上文中不加同步锁的懒汉式单例是线程不安全的。

线程安全的懒汉式单例

所以我们需要做的就是分隔时间块,加同步锁就可以实现,不是简单的加锁,而是双重检查加锁即线程安全又能够性能不受太大的影响。

public class SynSingleton {    
    //对保存的对象添加volatile关键字    
    //volatile修饰的变量值,不会被本地线程缓存   
    //所有的操作都是直接操作共享内存,保证多个线程能够正确的处理该变量  
    private volatile static SynSingleton instance = null;    
    //私有化构造方法,防止外部创建对象     
    private SynSingleton() {    
    }    
    //提供一个类静态方法返回单例对象,可以全局访问   
    public static SynSingleton getInstance() {       
        //先检查实例是否为空,不为空进入代码块     
        if (instance == null) {           
            //同步块,线程安全地创建实例        
            synchronized (SynSingleton.class) {     
                //再次检查实例是否为空,为空才真正的创建实例        
                if (instance == null) {              
                    instance = new SynSingleton();         
                }           
            }     
        }        
        return instance;  
    } 
}

更好的单例实现方法

根据上文的分析,常见的两种单例写法都有不同问题和缺陷。最后还有一种写法。即延迟加载又线程安全。这个方案称为 Lazy initialization holder class模式,综合使用Java的类级内部类和多线程缺省同步锁知识。

类级内部类

  • 类级内部类,指有static修饰的成员式内部类,如果没有static修饰的就是对象级内部类
  • 类级内部类相当于外部类的static成分,它的对象与外部对象之间不存在依赖关系,因此可以直接创建。而对象级内部类的实例,绑定在外部对象的实例中。
  • 类级内部类中,可以定义静态方法,静态方法中只能够引用外部类中的静态成员方法或者成员变量
  • 类级内部类相当于其外部类的成员,只在第一次使用时才会被装载。

多线程缺省同步锁

在多线程开发中除了使用synchronized同步锁实现同步控制之外,虚拟机有一些隐含的同步控制,这样就不用我们控制同步,这些隐含的情况包括

  • 由静态初始化器(静态字段或static{}代码块的初始化器)初始化数据时
  • 访问final字段时
  • 在创建线程之前创建对象时
  • 线程可以看见它将要处理的对象时

实现代码

实现的代码思路就是,用静态初始化器的方式,由虚拟机保证线程安全。用类级内部类负责创建实例,只要不使用到这个类级内部类就不会创建实例。两者结合就实现了延迟加载和线程安全。

public class Singleton {    
    /**     
    * 类级内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例没有依赖   
    * 而且只有被调用的时候才会被装载,从而实现延迟加载     
    */    
    private static class SingletonHolder{        
        //静态初始化器 由虚拟机保证线程安全     
        private static Singleton instance= new Singleton ();   
    }    
    
    private Singleton () {     
    }
    
    public static Singleton getInstance(){         
        return SingletonHolder.instance;  
    } 
}

上面的代码在运行中,当getInstance方法第一次被调用,它第一次读取SingletonHolder.instance导致Singleton类得到初始化,而这个类在装载时并同时被初始化,会初始化它的静态域,从而创建Singleton实例,因为是静态域,因此只会在虚拟机装载类的时候初始化一次,由虚拟机保证它的线程安全。这样写的优势就是,getInstance方法没有被同步加锁,并且只是执行一个域的访问,延迟加载没有增加任何的访问开销。

枚举单例

以为我们已经找到很好单例写法和问题解决办法。但是Java就是这么有意思,总有新的想法提出和问题出现。前文的双重检查加锁(double checked locking)单例在Java1.5之前也会某种情况下产生多个实例,并且volatile关键字也会导致的一些复杂的问题。

在《Effective Java 》第二版第3条中,提到这一句话:

单元素的枚举类型已经成为实现单例的最佳方法

这是在Java1.5发行版之后,枚举能够实现单例,只需要编写一个包含单个元素的枚举就可以实现线程安全代码简单的单例。

这里需要说明枚举

  • Java的枚举类型实质上功能齐全的类,它有自己的属性和方法
  • Java枚举类型的基本思想是通过公有的静态final域为每个枚举常量导出实例的类
  • 从某个角度上看,枚举是单例的泛型化,本质上是单元素的枚举
public enum  EnumSingleton {   
    /**      
    * 枚举元素 它就代表单例的一个实例    
    */    
    uniqueInstance;  
    public void setUniqueInstance(){    
    //对应普通单例的对象方法或者是功能操作    
    } 
}

这种单例在功能上与公有域方法相近,但是代码简洁,由虚拟机提供序列化机制,绝对防止反射等方法导致的多次实例化。简洁。高效、线程安全,真的可以说是最佳单例写法。

在使用的时候

//这是普通单例的调用示例        
Singleton.getInstance().doOperate();         
//这是枚举单例的调用示例 没有明显的区别  
EnumSingleton.uniqueInstance.setUniqueInstance();