单例模式学习记录

一、前言

以前也使用过一些设计模式,特别是单例模式——这个可以说是最简单的设计模式,但是没有经过细致的学习,停留在粗放的使用上,这篇旨在记录自己的设计模式的学习体会上。

二、独一无二的对象

2.1 单例模式的用处

有一些对象只需要一个,比方说:数据库连接池、线程池、缓存、日志,这类对象只能有一个实例,如果有多个实例,就会导致许多问题产生。
例如下面的单例获取DB资源,就是一个简单的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class PropertyMgr {

private static PropertyMgr pmInstance = null;

private PropertyMgr() {

}

/* 获取单例 */
public static PropertyMgr getInstance() {
if (pmInstance == null) {
pmInstance = new PropertyMgr();
}

return pmInstance;

}

2.2 静态变量的缺点

通过程序猿之间的约定可以办到:通过全局变量实现一些类只存在一个实例。
但是,如果将对象赋值给一个全局变量。则程序已开始就创建好对象(跟JVM实现有关),万一该对象非常耗费资源,而程序中一直没有使用这会造成极大的浪费。

2.3 单例模式的优点

单例模式常常用来:

  • 管理共享资源
  • 避免开发人员因为产生太多同一对象而产生Bug
  • 要取得实例,则必须“请求”到一个实例,而不是自行实例话得到一个实例。调用getInstance()这个方法,随时可以获取这个独一无二的实例。(该实例可能在此次调用的时候被创建出来,又或者早已经被创建出来)

三、剖析经典单例模式实现

3.1 以代码注释带剖析经典单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton{

private static Singleton instance;//利用静态变量记录Singleton类的唯一实例

private Singleton(){}//将构造器声明为私有的,只有在Singleton内部方法才能调用该构造器

//该方法实例化对象,并返回这个实例
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton;
}
return instance;
}
//其他方法
}

重点剖析getInstance():

1
2
3
4
5
6
7
8
9
public static Singleton getInstance(){
if(instance == null){
//如果instance为空,表示还没有创建实例,如果他不存在,
//我们利用私有的构造器产生一个Singleton实例,并把它赋值到instance中
//注意:如果我们永远不需要这个实例,则其永远不会被创建
instance = new Singleton();
}
return instance;
}

getInstance()方法是静态的,这意味着它是一个类方法,所以可以在代码的任何地方访问它。这和全局变量是一样的,只不过单例模式的创建可以延迟实例化。

3.2 单例模式的定义

单例模式——确保一个类只有一个实例,并提供一个全局访问点。

  • 将某个类设计为自己管理的单独实例,同时也避免其他类再自省产生实例,想要去的单例实例,通过单例类是唯一的途径。
  • 当需要实例时,向类查询,它会返回一个实例。利用延迟化的方式创建单例,这种做法对资源敏感的对象特别重要。

四、多线程下的单例模式分析

4.1 多线程下出错分析

在多线程下使用上例代码,则会造成线程问题,过程如下图所示:
Alt text

4.2 解决线程问题

4.2.1 同步方法

通过增加synchronized关键字到getInstance()方法,迫使每个县城在进入这个方法之前,要先等候别的线程离开该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton{

private static Singleton instance;

private Singleton(){}

//该方法实例化对象,并返回这个实例
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton;
}
return instance;
}
//其他方法
}

该方法缺点:同步会降低性能,此方法只有第一次执行时才需要真正同步,一旦设置好了instance变量,之后就不需要同步该方法了。

4.2.2 “饿汉”(eagerly)方法

如果应用程序总是创建病使用实例,活着在创建和运行时放面的负担不太繁重,就可以使用这种方法创建单例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton{

private static final Singleton instance = new Singleton();//在静态初始化中创建单例,此处保证了线程安全

private Singleton(){}

public static synchronized Singleton getInstance(){
return instance;
}

//其他方法


}

利用该做法,依赖JVM在加载这个类时马上创建此唯一单例实例,又JVM保证任何线程访问instance静态变量之前,一定先创建此实例。

饿汉式的原理其实是基于classloder机制来避免了多线程的同步问题

饿汉式与之前提到的懒汉式不同,它在我们调用getIns之前就实例化了(在类加载的时候就实例化了),所以不是一个懒加载,这样就有几个缺点:

  • 延长了类加载时间
  • 如果没用到这个类,就浪费了资源(加载了但是没用它)
  • 不能传递参数(很显然适用的场景会减少)

4.2.3 “双重检查加锁”(double-checked locking)减少使用同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton{

private volatile static Singleton instance;

private Singleton(){}

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

//其他方法


}

1、volatile 关键字保证,当instance变量初始化成Singleton实例时,多个线程正确地处理instance变量。

2、在JVM虚拟机栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。

3、volatile用在多线程,同步变量。 线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对A的访问其实访问的是B。只在某些动作时才进行A和B的同步。因此存在A和B不一致的情况。volatile就是用来避免这种情况的。volatile告诉JVM, 它所修饰的变量不保留拷贝,直接访问主内存。

4.2.4 静态内部类

静态内部类原理同上,另外虽然它看上去有点饿汉式,但是与之前的饿汉有点不同,它在类Singleton加载完毕后并没有实例化,而是当调用getIns去加载Holder的时候才会实例化,静态内部类的方式把实例化延迟到了内部类的加载中去了!所以它比饿汉式更优秀!

1
2
3
4
5
6
7
8
9
public class Singleton {

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

4.2.5 枚举方法

1
2
3
public enum Singleton{
INSTANCE;
}

简单应用

你可以通过EasySingleton.INSTANCE来访问,这比调用getInstance()方法简单多了。

线程安全

其次,默认枚举实例的创建是线程安全的.(创建枚举类的单例在JVM层面也是能保证线程安全的),
所以不需要担心线程安全的问题

关于序列化.

以往的单例实现了序列化接口,那么就再也不能保持单例的状态了.因为readObject()方法一直返回一个
新的对象.使用radResolve()来避免此情况发生.

1
2
3
4
//readResolve to prevent another instance of Singleton
private Object readResolve(){
return INSTANCE;
}

关于反射

采用反射来创建实例时.可通过AccessibleObject.setAccessible(),通过反射机制来调用私有构造器.那么枚举可以防止这种创建第二个实例的情况发生.

优点:

  • 应用简单,线程安全,防反序列化.