单例模式

饿汉式单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CustomManager {
private Context mContext;
private static final Object mLock = new Object();
private static CustomManager mInstance;

public static CustomManager getInstance(Context context) {
synchronized (mLock) {
if (mInstance == null) {
mInstance = new CustomManager(context);
}

return mInstance;
}
}

private CustomManager(Context context) {
this.mContext = context.getApplicationContext();
}
}

饿汉式(静态变量)

1
2
3
4
5
6
7
8
9
//饿汉式单例类。在类初始化时,已经自行实例化
public class Singleton{
private Singleton(){}
private static Singleton instance = new Singleton();
//静态工厂方法
public static Singleton getInstance(){
return instance;
}
}

饿汉式(静态常量)

1
2
3
4
5
6
7
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}

饿汉式(静态代码块)

1
2
3
4
5
6
7
8
9
10
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}

饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。

上面三种写法本质上是一样。但使用静态final的实例对象或者静态代码块依旧不能解决在反序列化、反射、克隆时重新生成实例对象的问题。

优点:写法比较简单,在类装载的时候就完成实例化。避免了线程同步问题。

缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

序列化:一是可将一单例的实例对象写到磁盘,实现数据的持久化;二是可实现对象数据的远程传输。当单例对象有必要实现Serializable接口时,即使将其构造函数设为私有,在它反序列化时依然会通过特殊的途径再创建类的一个新实例,相当于调用了该类的构造函数有效地获得了一个新实例!

反射:可通过setAccessible(true)来绕过private限制,从而调用到类的私有构造函数创建对象

克隆:clone()是Object的方法,每一个对象都是Object的子类,都有clone()方法。clone()方法并不是调用构造函数来创建对象,而是直接拷贝内存区域。因此当我们的单例对象实现了Cloneable接口时,尽管其构造函数时私有的,仍可以通过克隆来创建一个新对象,单例模式也相应失效了。

懒汉式单例

1
2
3
4
5
6
7
8
9
10
11
12
//懒汉式单例类。在第一次调用的时候实例化自己
public class Singleton{
private Singleton(){}
private static Singleton instance = null;
//静态工厂方法
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}

优点:懒加载,只有使用的时候才会加载。

缺点:但是只能在单线程下使用。如果在多线程下,instacnce对象还是空,这时候两个线程同时访问getInstance()方法,因为对象还是空,所以两个线程同时通过了判断,开始执行new的操作。所以在多线程环境下不可使用这种方式。

解决懒汉式单例模式线程安全问题,采用了以下三种解决方案:

在getInstance方法上加同步(线程安全,存在同步开销)

1
2
3
4
5
6
7
8
9
10
11
public class Singleton{
private Singleton(){}
private static Singleton instance = null;
//为方法添加同步锁
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}

优点:懒加载,只有使用的时候才会加载,获取单例方法加了同步锁,保正了线程安全。

缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
//线程假装安全,同步代码块
public class Singleton{
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
instance = new Singleton();
}
}
return instance;
}
}

优点:改进了效率低的问题。

缺点:但实际上这个写法还不能保证线程安全,和第四种写法类似,只要两个线程同时进入了 if (singleton == null) { 这句判断,照样会进行两次new操作

双重检查锁定(DCL)(假)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton{
private Singleton(){}
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}

上面的new Singleton()不是原子操作,大致做了3件事:

  1. 给Singleton的实例分配内存
  2. 调用Singleton()的构造函数,初始化成员字段
  3. 将instance对象指向分配的内存空间(此时instance就不是null了)

但是由于Java编译器运行处理器乱序执行,上面的2、3步骤的顺序无法保证。所以有存在DCL失效问题

优点:线程安全;延迟加载;效率较高。

缺点:JVM编译器的指令重排导致单例出现漏洞。

双重检测锁定(真,推荐使用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton{
private static volatile Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}

在jdk1.5之后,具体化了volatile关键字,可以保证instance对象每次都是从主内存中读取,就可以使用DCL的写法来完成单例模式。当然volatile多少会影响到性能。

优点:线程安全;延迟加载;效率较高。

缺点:由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,略微的性能降低,但除非你的代码在并发场景比较复杂或者低于JDK6版本下使用,否则,这种方式一般是能够满足需求的。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CustomManager {
private Context mContext;
private volatile static CustomManager mInstance;

public static CustomManager getInstance(Context context) {
// 避免非必要加锁
if (mInstance == null) {
synchronized (CustomManger.class) {
if (mInstance == null) {
mInstacne = new CustomManager(context);
}
}
}

return mInstacne;
}

private CustomManager(Context context) {
this.mContext = context.getApplicationContext();
}
}

静态内部类(推荐使用这种

1
2
3
4
5
6
7
8
9
public class Singleton{
private static class LazyHolder{
private static final Singleton INSTANCE = new Singleton();
}
private Single(){}
public static final Singleton getInstance(){
return LazyHolder.INSTANCE;
}
}

优点:避免了线程不安全,延迟加载,效率高。

缺点:依旧不能解决在反序列化、反射、克隆时重新生成实例对象的问题。

静态内部类的原理是:

  • SingleTon第一次被加载时,并不需要去加载LazyHolder,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。getInstance()方法并没有多次去new对象,取的都是同一个INSTANCE对象。

  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕

  • 缺点在于无法传递参数,如Context

枚举类型

枚举类型是所有单例实现中唯一一种不会被破坏的单例实现模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum Singleton{
INSTANCE;
public void businessMethod(){
System.out.println("我是一个单例!");
}
}

//调用
public class MainClass{
public static void main(String[] args){
Singleton s1 = Singleton.INSTANCE;
Singleton s2 = Singleton.INSTANCE;
s1.businessMethod();
System.out.println(s1 == s2);//输出true
}
}
  1. 枚举类型是final的(不可被继承)

  2. 构造方法是私有的(也只能是私有,不允许被外部实例化,符合单例)

  3. 类变量是静态的

  4. 没有延时初始化,随着类的初始化就初始化了(从上面静态代码块中可以看出)

  5. 由4知道枚举是线程安全的

优点:写法简单,不仅能避免多线程同步问题,而且还能防止反序列化、反射,克隆重新创建新的对象

缺点:JDK1.5之后才能使用

登记式单例-使用Map容器来管理单例模式(可忽略)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//类似Spring里面的方法,将类名注册,下次从里面直接获取
public class Singleton{
private static Map<String,Singleton> map = new HashMap<String,Singleton>();
static {
Singleton instance = new Singleton();
map.put(instance.getClass().getName(), instance);
}
//保护默认构造子
protected Singleton(){}
//静态工厂方法,返还此类唯一的实例
public static Singleton getInstance(String name){
if(name == null){
name = Singleton.class.getName();
}
if(map.get(name) == null){
try{
map.put(name, (Singleton) Class.forName(name).newInstance());
} catch (InstantiationException e){
e.printStackTrace();
} catch (IllegalAccessException e){
e.printStackTrace();
} catch (ClassNotFoundException e){
e.printStackTrace();
}
}
return map.get(name);
}

//一个示意性的商业方法
public String about(){
return "Hello, I am RegSingleton.";
}
public static void main(String[] args){
Singleton single = Singleton.getInstance(null);
System.out.println(single.about());
}
}

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

缺点:不常用,有些麻烦

单例模式四种需求的满足情况图

Application注意点:

Application并没有采用单例模式来实现,但是由于它的生命周期由框架来控制,和整个应用的保持一致,且确保了只有一个,所以可被看作是一个单例。但若直接用它来存取数据将会有NullPointerException,因为Application不会永驻在内存里,会随进程杀死而被销毁。它再次创建的时候之前所保存的状态都会重置。

为避免这个问题:

  1. 通过传统的intent来显式传递数据(将Parcelable或Serializable对象放入Intent/Bundle。Parcelable性能比Serializable快一个量级,但是代码实现更复杂点)
  2. 重写onSaveInstanceState()和onRestoreInstanceState(),确保进程被杀死时保存必要的状态,在重新打开时可以正确恢复现场。
  3. 使用合适的方式将数据保存到数据库或硬盘
  4. 总是做判空保护和处理