一单例模式

241 阅读9分钟

单例模式介绍

  1. 单例模式,单例对象的类必须保证只有一个实例存在,许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。如在一个应用中,应该只有一个ImageLoader实例,这个ImageLoader中又含有线程池、缓存系统、网络请求等,很消耗资源,因此没有理由构造多个实例,这种不能自由构造多个对象的情况,这就是单例的使用场景。
  2. 什么是单例模式

  • 确保一个类只有一个实例,而且自行实例滑并向整个系统提供这个实例

  3.单例模式的使用场景

  •     确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个,例如,创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源,这时就要考虑单例模式
  • UML类图
  • 角色介绍
  1. Client 客户端
  2. Singletton 单例类
  • 实现单例模式的几个关键点:
  1. 构造函数不对外开放,一般为Private 不能通过new的方式创建对象
  2. 通过一个静态方法或者枚举返回单例模式类对象
  3. 确保单例类的对象有且只有一个,尤其是在多线程环境下
  4. 确保单例类对象在反序列化时不会重新构建对象
  • 代码事例:

//普通员工
public class Staff{
    public void work(){
    //干活
    }

}
//副总裁
public class VP extends Staff{
    @Override
    public void work(){
     //管理下面的经理
    }

}
//CEO,饿汉单例模式
public class CEO extends staff{
    public static final CEO mCeo=new CEO();
    //构造函数私有
    private CEO(){
    }
    //公有的静态函数,对外暴漏获取单例对象的接口
    public static CEO getCEO(){
          return mCeo;
    }
    @Override
    public void work(){
     //管理VP
    }


}
//公司类
public class Company{
    private List<Staff> allStaffs=new ArrayList<Staff>();
    public void addStaff(Staff per){
        allStaffs.add(per);
    }
    public void showAllStaffs(){
        for(Staff per:allStaffs){
            System.Out.println("Obj:"+per.toString())
        }
    
    }



}
//测试类
public class Test{
    public static void main(String [] args){
            Company cp =new Company();
            //CEO 对象只能通过getCEO()函数获取
            Staff ceo1=new CEO.getCEO();
            Staff ceo2=new CEO.getCEO();

            //公司加入ceo
            cp.addStaff(cdo1);
           cp.addStaff(cdo2);
           //通过new创建VP对象
            Staff vp1=new VP(); 
            Staff vp2=new VP();
             
            Staff staff1=new Staff();
            Staff staff2=new Staff();
            Staff staff3=new Staff();        cp.addStaff(vp1); 
        cp.addStaff(vp2); 
        cp.addStaff(staff1); 
        cp.addStaff(staff2);
        cp.addStaff(staff3);

        cp.showAllStaffs();    }



}

 


从上述的代码中可以看到,CEO类不能通过new 的形式构造对象,只能通过CEO.getCEO()函数获取,而这个CEO对象是静态对象,并且在声明的时候就已经初始化了,这就保证了CEO对象的唯一性,从输出结果中发现,CEO两次输出的CEO对象都是一样的,而VP,Staff等类型的对象使不同的,这个实现的核心在于将CEO类的构造方法私有化,使得外部程序不能通过构造函数来创建CEO对象,而CEO通过一个静态方法返回一个静态对象。

单例模式的实现方式

1、饿汉模式

public class Singleton {
    private Singleton() {}
    private static final Singleton single = new Singleton();
    //静态工厂方法   
    public static Singleton getInstance() {
        return single;
    }
}

上述这种实现方法叫饿汉模式,从代码中可以看到,外界只能通过Singleton.getInstance()方法获得Singleton的实例(single),而这个实例是静态对象,并且在声明的时候就初始化了,这就保证了Sigleton对象的唯一性,以后不再改变,所以天生是线程安全的。

2、懒汉模式

//懒汉式单例类.在第一次调用的时候实例化自己
public class Singleton {
    private Singleton() {}
    private static  Singleton single=null;
    //静态工厂方法
    public static synchronized Singleton getInstance() {
        if (single == null) {
            single = new Singleton();
        }
        return single;
    }
}

懒汉模式同样将构造函数私有化,对外提供Singleton.getInstance()方法获得Singleton的实例(single),不同的是饿汉模式中在类加载的时候就已经初始化了single实例,而懒汉模式中只有开发者调用了Singleton.getInstance()才去创建Single的实例对象(single),同时,细心地读者应该发觉Singleton.getInstance()方法加了一个synchronized 的关键字,这个关键字就是用来保证线程安全的。这种方法实现了单例模式也保证了线程安全,但是每次调用Singleton.getInstance()方法都需要进行同步以保证线程安全造成了很多不必要的同步开销,消耗资源。这个就是懒汉模式的缺点。

3、双重加锁(Double Check Lock)


public class Singleton {
    private Singleton() {}
    private  volatile static  Singleton single=null;
    //静态工厂方法
    public static Singleton getInstance() {
        if (single == null) {
            synchronized (Singleton.class) {
                if (single== null) {
                    single = new Singleton();
                }
            }
        }
        return single;
    }
}

Double Check Lock双重加锁,简称DCL,这种方式实现单例既能够在需要使用时才初始化单例,进行了同步保证了线程安全,也提高了资源的利用率。它的亮点在于Singleton.getInstance()方法中的对single进行了两层判空,第一层判空是为了避免不必要的同步,第二层判空是为了在single为null的情况下才创建实例。读者应该注意到单例对象single 定义添加了关键字volatile ,添加该字段是因为程序执行

single = new Singleton();

这个语句时其实做了三步工作,(1)分别是给Singleton的实例分配内存,(2)调用构造函数初始化成员字段,(3)将single对象指向分配的内存空间(single不再为null),由于Java虚拟机是乱序执行的,所以执行顺序可能是(1)(2)(3),也有可能是(1)(3)(2),在多线程情况下,假如(3)执行完成,(2)未执行,线程从A切换线程B,因为此时single不为null,所以线程B直接取走了未执行步骤(2)的single,因为此时single未调用构造函数初始化,所以B线程在使用single时会出错,此时双重加锁就会失效,上面添加的volatile关键字就是为了解决这个问题,它可以保证single每次都从主存中读取。但是volatile也会影响性能,一般来说在Android开发中,高并发的情况比较少见,所以在实际开发中大多数开发者选择把volatile 字段去掉。

4、静态内部类方式

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

DCL虽然在一定程度上解决了资源消耗、多余的同步、线程安全等问题,但是在也会存在失效的可能,严重会导致程序崩溃。在《Java并发编程实践》中也谈到了这个问题,并直指这种优化是丑陋的不赞成使用,而提倡了上面这种静态内部类的实现方式。这种方式比上面1、2、3都好一些,既实现了线程安全,又避免了同步带来的性能影响,保证了单例对象的唯一性,同时延迟了单例的实例化。

仁者见仁智者见智,在Android开发实践中,通过阅读别人的代码发现,大部分开发者偏向于使用第三种双重加锁的实现方式,不过一般会把volatile 关键字去掉,个人建议还是加上避免出现失效的情况,同时第四种方法得到圣书《Java并发编程实践》的力推,优点也是显而易见,所以个人推荐第三第四种实现方式。

5、枚举类单例

枚举类单例模式是 《Effective Java》 作者极力推荐的单例的方法

特点

特点也就是检举类的特点,我们先看看枚举类的特点吧,多说无用,我们结合 java 代码来分析

// 一周的枚举,这里为了说明问题,只列举到周三
public enum EnumDemo {

  MONDAY,
  TUESDAY,
  WEDNESDAY ;

  public void donSomthing(){}
}复制代码

以上就是一个简单的枚举 Java 类,我们反编译来看一下它的实现机制是杂样的,在这里我使用 jad 来反编译「当然你也可以使用 javap 来反编译还能看到二制」,以上 java 代码反编译出来的结果如下:

枚举类反编译
枚举类反编译

从以上反编译出来的代码图我们可以看出以下几点信息:

  • 1、枚举类类型是 final 的「不可以被继承」
  • 2、构造方法是私有的「也只能私有,不允许被外部实例化,符合单例」
  • 3、类变量是静态的
  • 4、没有延时初始化,随着类的初始化就初始化了「从上面静态代码块中可以看出」
  • 5、由 4 可以知道枚举也是线程安全的

以上就是枚举类的特点,很符合单例模式,并且集成上以上几种单例模式的优点

优缺点
  • 1、优点:除以上特点优点之外,枚举类还有两个优点:写法简单支持序列化和反序列化操作「以上的单例序列化和反序列化会破坏单例模式」并且反射也不能调用构造方法
  • 2、缺点: --
演示代码
public enum  EnumSingleTon {

    INSTACE; // 定义一个枚举原素,代表 EnumSingleTon 一个实例

    /**
     * 枚举中的构造方法只能写成 private 或是不写「不写默认就是 private」,所以枚举防止外部来实例化对象
     */
    EnumSingleTon(){}

    /**
     * 一些额外的方法
     */
    public void doSometing(){
        Log.e("枚举类单例","这是枚举单例中的方法") ;
    }

}

6、使用容器实现单例模式,代码如下:

package demo;

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

public class Singleton {
    private static Map<String, Object> objMap = new HashMap<String, Object>();

    private Singleton() {
    }

    public static void registerService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }

    public static Object getService(String key) {
        return objMap.get(key);
    }
}
12345678910111213141516171819202122

这种实现方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一接口进行获取操作,降低用户使用成本,也对用户隐藏了具体实现,降低耦合度。


Android 中的单例模式

1、 InputMethodManager 类

InputMethodManager 就一个服务类「输入法类」源码目录 Androidsdk\sources\android-26\android\view\inputmethod,部分代码如下:

@SystemService(Context.INPUT_METHOD_SERVICE)
public final class InputMethodManager {
    // 省略若干行代码
    ...

    static InputMethodManager sInstance;

    // 省略若干行代码
    ...

    // 以下是构造方法,没有声明权限就是私有的
    InputMethodManager(Looper looper) throws ServiceNotFoundException {
        this(IInputMethodManager.Stub.asInterface(
                ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE)), looper);
    }

    // 以下是构造方法,没有声明权限就是私有的
    InputMethodManager(IInputMethodManager service, Looper looper) {
        mService = service;
        mMainLooper = looper;
        mH = new H(looper);
        mIInputContext = new ControlledInputConnectionWrapper(looper,
                mDummyInputConnection, this);
    }

    public static InputMethodManager getInstance() {
        synchronized (InputMethodManager.class) {
            if (sInstance == null) {
                try {
                    sInstance = new InputMethodManager(Looper.getMainLooper());
                } catch (ServiceNotFoundException e) {
                    throw new IllegalStateException(e);
                }
            }
            return sInstance;
        }
    }

    // 省略若干行代码
    ...
}复制代码

从上面代码可以看出,InputMethodManager 是一个典型的-- 线程安全的懒汉式单例

2、Editable 类

文件目录:frameworks/base/core/java/android/text/Editable.java 部分代码如下:

private static Editable.Factory sInstance = new Editable.Factory();  

/** 
 * Returns the standard Editable Factory. 
 */  
public static Editable.Factory getInstance() {  
    return sInstance;  
}复制代码

可以看到非常典型的一个饿汉式单例模式

后续设计模式还会继续更新