单例模式(Singleton Pattern)

什么是单例模式

首先来看一段单例模式的通用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton{
//自行实例化
private static final Singleton instance = new Singleton();
/**
*1,构造函数为私有,不能通过new获得对象实例,限制实例产生
*2,自行实例化
*/

private Singleton(){
};
//只能通过调用Singleton来获取其实例
public static Singleton getInstance(){
return instance;
}
//单例模式中的其他方法尽量是static,
public static void otherFuc(){
}
}

通过代码中的注释可以发现,单例模式中的构造方法为私有的,是不能被外部通过new来实例化的。Singleton类通过private构造方法能确保在一个应用中只产生一个Singleton的实例,并且是自行实例化的。

单例模式使用场景

在一个应用中,要求一个类只产生一个对象,如果出现多个类的对象就会导致应用出现问题的情况。(比如,Android中Application类就只能产生一个,通过这个单例类来实现缓存数据的存储等)
具体使用场景如下:

  • 需要定义大量的静态常量和方法(如工具类)的环境,可以采用单例模式。(也可以直接定义这些常量和方法为static直接.使用)
  • 创建一个对象需要消耗过多的资源(比如I/O,访问数据库等),可以采用单例模式。(Android中DBHelper可以参考参考)
  • 在整个项目中需要一个共享访问点和共享数据,比如Android中通过Share Perfrence来共享数据等,Application类就是单例类。
  • 要求生成唯一序列号的环境。

单例模式优缺点

优点

  • 由于单例模式在内存中只有一个实例,减少了内存开销,特别是那种一个对象需要不断的创建、销毁而且类的性能又无法优化的情况下,单例模式你值得拥有。
  • 由于单例模式只生成一个实例,可以减少系统的性能开销。当一个对象的产生需要较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在启动应用的时候,直接产生一个单例对象,然后用永久驻留内存的方式来解决。(Android开发中也经常会遇到,比如百度地图、推送服务的初始化,AsyncImagerLoader初始化等)<注意:在使用单例模式的时候要注意JVM的GC机制,单例在内存中被干掉了,再使用就会报异常>
  • 单例模式可以避免对系统资源的多重占用,比如一个写文件的操作,由于只有一个实例,所以可以避免对同一个文件资源同时写操作。
  • 单例模式可以在系统中设置一个全局的访问点,优化和共享资源访问,例如可以设计一个单例类来负责数据表的映射处理。

缺点

  • 单例模式一般没有接口,扩展很困难,所以如果想扩展单例类,唯一的办法就是修改类的代码。(为什么单例类没有接口?因为单例类实在内部自行实例化的,并且提供单一实例,而接口或抽象类是不能被实例化的。)
  • 在并行开发中,如果单例类没开发完全,则对于测试来说,是不利的。想一想单例没有抽象也没有接口,测试人员也无法使用mock的方式来虚拟一个对象。
  • 单例模式(在类中干很多事)和单一职责(只做一件事)是相悖的,当然仍和设计模式都不是绝对要遵从的,视具体的情况而定,白猫黑猫抓到老鼠才是好猫。

注意事项

在高并发的情况下,要注意单例模式的线程同步问题,避免产生多个实例。单例模式有很多中不同的实现方法。开头的那段代码是不会产生多个实例滴。但是下面这段代码就有可能产生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton{
//未实例化
public static Singleton instance = null;
//防止从外部被实例化
private Singleton(){
}
//获取实例
public static Singleton getInstance(){
if(instance==null){
instance = new Singleton();
}
return instance;
}
}

该单例模式在低并发的情况下不容易出现问题,如果系统并发量大可能在内存中出现多个单例类的实例和预期(内存中只存在一个单例)不一样。比如线程A,在执行到if(instance==null)时,在单例将要被初始化,但是还没被初始化的时候(此时instance==null),线程B,也执行到if(instance==null)判断为true,那么A拥有了一个单例实例,而B也会创建一个单例实例。这个时候内存中就存在两个实例,就和预期只存在一个不符。
那么如何解决这种“异常”情况呢?

  • 简单粗暴的方式,就是像文章开篇那样 private static Singleton instance = new Singleton()这种(饿汉式单例)防止出现多个单例对象。
  • 还有一种就是在getInstance时,使用synchronzined来实现线程锁。比如下面的两种方式(饱汉式单例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//方式1
public static synchronzined Singleton getInstance(){
if(instance==null){
instance = new Singleton();
}
return instance;
}
//-----------------我是分割线----------------
//方式2
private static Object lock=new Object();
public static Singleton getInstance(){
if(instance==null){
synchronized (lock)
{
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}

其次需要考虑对象复制的情况。在Java中,对象默认是不能被复制的,如果实现了Cloneable接口,并实现了clone方法则可以直接通过对象复制的方式创建一个新对象。
一般情况下,不用考虑单例的复制情况。很少出现一个单例类还主动要求被复制的情况。解决这个问题的方法,就是别让单例类实现Cloneable接口就好了。

有上限的多例模式

有上线的多例模式是单例模式的一种扩展,比如只要求一个类在内存中只存在三个对象实例怎么做?代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MultiSingletons{
//定义上限为3个实例
private static int maxNum = 3;
private static ArrayList<MultiSingletons> mInstances = new Array<MultiSingletons>();
//产生3个实例对象
static{
for(int=0;i<maxNum;i++){
mInstaces.add(new MultiSingletons());
}
}

private MultiSingletons(){
}

public static ArrayList<MultiSingleton> getMInstances(){
return mInstances;
}
}

小结

单例模式在平时的开发中应用的比较广泛,比如Android开发中经常使用单例模式。
不过在使用单例模式时,一定要注意JVM的垃圾回收机制,就是单例对象如果在内存中长久不使用,那么JVM就会认为该对象为垃圾,在CPU空闲的情况下,该对象会被清除掉,下次调用该对象则需要重新产生一个对象。
如果在单例类中,保存了程序的一些状态值,那么这些状态值会在变成初始值。这个时候应用拿到的值就不是之前保存在单例中的值,就会出现故障。对于需要记录状态值的情况,自行管理单例的生命周期,或者通过观察者模式,记录单例中值的变化,然后保存值到文件中,那么在下次发现变化时,可以将正确的值恢复,从而避免数据丢失。