一、创建型模式--单例模式

##一、创建型模式--单例模式

单例模式介绍

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

单例模式的定义

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

单例模式的使用场景

    确保某个类有且只有一个对象的情景,避免产生多个对象,消耗过多的资源,或者某种类型的对象只应该有且只有一个。例如:创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源,这时候就需要考虑使用单例模式。

单例模式的关键点

  • 构造函数不对外公开,一般为private
  • 通过一个静态方法或者枚举返回单例类对象
  • 确保单例类的对象有且只有一个,尤其是在多线程下。
  • 确保单例在反序列化时不会重现构建对象。

单例模式的简单示例

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

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

//CEO
publi class CEO extends Staff{
    private static final CEO mCeo = new CEO();
    //私有化构造方法
    private CEO(){
    }
    public static CEO getCeo(){
        return mCeo;
    }

    @Override
    public void work(){
        //管理下面的VP
    }
}

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

public class Client {
    public static void main(String[] args) {
        Conpany cp = new Conpany();
        //CEO只能通过getCeo()函数获取
        Staff ceo1 = CEO.getCeo();
        Staff ceo2 = CEO.getCeo();
        //通过new创建VP对象
        Staff vp1 = new VP();
        Staff vp2 = new VP();
        //通过new创建Staff对象
        Staff staff1 = new Staff();
        Staff staff2 = new Staff();
        Staff staff3 = new Staff();
        cp.addStaff(ceo1);
        cp.addStaff(ceo2);
        cp.addStaff(vp1);
        cp.addStaff(vp2);
        cp.addStaff(staff1);
        cp.addStaff(staff2);
        cp.addStaff(staff3);
        cp.showAllStaffs();
    }
}

每个对象都有一个唯一的hashcode值,打印结果请自行测试。

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

单例模式-懒汉模式

懒汉模式是声明一个静态对象,并且在用户第一次调用getInstance时进行初始化。而上述的饿汉式(CEO类)是在声明静态对象时就已经初始化。懒汉单例模式的实现代码如下。

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

总结:getInstance()方法中添加了synchronized关键字,也是就是说getInstance()是一个同步方法,这样在多线程下保持了单例对象唯一性。但是存在一个问题,即使是instance已经被初始化(第一次调用就初始化好了),每次调用getInstance()时都会进行同步,消耗不必要的资源,这也是懒汉单例模式存在最大的问题。


最后:懒汉单例模式优点时只有在需要使用的时候才初始化,在一定程度上节省资源;缺点是第一次加载时需要及时进行实例化,反应稍慢,最大的问题是每次getInstance都进行同步,造成不必要的同步开销,这种模式一般不建议使用。

单例模式-Double CheckLock实现单例

DCL方式实现单例模式的优点是既能够在需要的时候才初始化单例,又能够保证线程安全,且单例对象初始化之后调用getInstance不进行同步锁。代码如下:

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

单例模式-枚举单例

更为简单的单例实现方式:

public enum SingletonEnum{
    INSTANCE;
    public void dosomething(){
        system.out.println("do sth.");
    }
}

枚举在java中和普通类是一样的,不仅有字段,还有自己的方法。最重要的是枚举默认是线程安全的。

⚠️通过序列化可以将一个单例的实例对象写到磁盘,然后再读回来从而有效的获得一个实例。即使构造函数是私有的,反序列化时然后可以通过特殊的途径去创建一个新的实例,相当于调用该类的构造方法。反序列化操作提供一个很特殊的钩子函数,类中具有一个私有的、被实例化的方法readResolve(),这个对象可以让开发人员控制对象的反序列化。例如:上述几个示例如果要杜绝反序列化时重新生成新对象,那么必须加入如下方法:

private Object readResolve() throw ObjectStreamException{
    return sInstance;
}

也就是说在readResolve()中将sInstance对象返回,而不是重新生成一个新的对象。对于枚举,并不存在这个问题,因为即使反序列化也不会重新生成新的实例。

单例模式-使用容器实现单例模式

直接贴代码

public class SingletonManager{
    private static Map<String,Object> objMap = new HashMap();
    private Singleton(){}
    public static void registerService(String key,Object instance){
        if(!objMap.containKey(key)){
            objMap.put(key,instance);
        }
    }
    public static Object getService(String key){
        return objMap.get(key);
    }    
}

在程序开始时,将许多单例类型注入到一个统一的管理类中,在使用的时候更具key获取对象对应类型的操作。这种方式可以使得我们管理多种类型的单例,并且在使用的时候通过统一的接口进行操作,降低用户的使用成本,也对用户隐藏了具体细节,降低耦合度。

###扩展

Android源码中单例模式的探索

在Android开发中,我们经常使用context.getSystemService(Context.*)来获取系统级别的服务。如 WindowsMangerService、ActivityManagerService、LayoutInflater等。接下来,我们以LayoutInflater为例来说明

未完待遇….

Create by Diamond_Lin
2016年8月21日。