设计模式之单例模式

单例模式

单例模式定义及应用场景

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点,单例模式是创建型模式。换句话说,一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式。单例模式在现实生活中应用也非常广泛,例如,公司CEO、部门经理等。J2EE标准中的ServletContext、ServletContextConfig等、Spring框架应用中的ApplicationContext、数据库的连接池等也都是单例形式。

饿汉式单例模式

饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题。

示例代码1

1
2
3
4
5
6
7
8
9
10
11
12
public class Hungrysingleton 
{
//先静态、后动态
//先属性、后方法
//先上后下
private static final Hungrysingleton instance = new Hungrysingleton();
private Hungrysingleton(){}
public static Hungrysingleton getInstance()
{
return hungrysingleton;
}
}

示例代码2,利用静态代码块机制

1
2
3
4
5
6
7
8
9
10
11
12
13
//饿汉式静态块单例模式
public class Hungrystaticsingleton
{
private static final Hungrystaticsingleton hungrysingleton;
static {
hungrysingleton = new HungryStaticsingleton();
}
private Hungrystaticsingleton(){}
public static Hungrystaticsingleton getInstance()
{
return hungrysingleton;
}
}

饿汉式单例模式适用于单例对象较少的情况。这样写可以保证绝对线程安全、执行效率比较高。但是它的缺点也很明显,就是所有对象类加载的时候就实例化。这样一来,如果系统中有大批量的单例对象存在,那系统初始化是就会导致大量的内存浪费。也就是说,不管对象用与不用都占着空间,浪费了内存,有可能“占着茅坑不拉屎”。

但是如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。

采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如 Java 中的 PermGen Space OOM),我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。

总之是否要使用饿汉式单例,要结合实际情况考虑。

懒汉式单例模式

为了解决饿汉式单例可能带来的内存浪费问题,于是就出现了懒汉式单例的写法,懒汉式单例模式的特点是,单例对象要在被使用时才会初始化,懒汉式相对于饿汉式的优势是支持延迟加载,下面看懒汉式单例模式的简单实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//懒汉式单例模式在外部需萎使用的时候才进行实例化
public class LazySimplesingleton {
private LazySimplesingleton(){}
//静态块,公共内存区域
private static LazySimplesingleton lazy = null;
public static LazySimplesingleton getInstance()
{
if (lazy == null)
{
lazy = new LazySimplesingleton();
}
return lazy;
}
}

这个在多线程环境下,会出现线程安全问题,可能会创建出多个实例。

解决方法,加锁,使用synchronzed关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//懒汉式单例模式在外部需萎使用的时候才进行实例化
public class LazySimplesingleton {
private LazySimplesingleton(){}
//静态块,公共内存区域
private static LazySimplesingleton lazy = null;
public static synchronzed LazySimplesingleton getInstance()
{
if (lazy == null)
{
lazy = new LazySimplesingleton();
}
return lazy;
}
}

给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度很低。量化一下的话,并发度是 1,也就相当于串行操作了。而这个函数是在单例使用期间,一直会被调用。如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。

解决方案是双重检查

双重检测懒汉式单例模式

饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。有一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式。在这种实现方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了。所以,这种实现方式解决了懒汉式并发度低的问题。具体的代码实现如下所示:

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
public class LazyDoubleCheckSingleton
{
private volatile static LazyDoubleCheckSingleton instance = null;

private LazyDoubleCheckSingleton()
{
}

public static LazyDoubleCheckSingleton getInstance()
{
if (instance == null)
{
synchronized (LazyDoubleCheckSingleton.class)
{
if (instance == null)
{
instance = new LazyDoubleCheckSingleton();
//1.分配内存给这个对象
//2.初始化对象
//3.设置1azy指向刚分配的内存地址
}
}
}
return instance;
}
}

instance要加volatile关键字,因为指令重排序,可能会导致 IdGenerator 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。所以需要给 instance 成员变量加上 volatile 关键字,禁止指令重排序才行。实际上,只有很低版本的 Java 才会有这个问题。现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)。

静态内部类单例模式

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
instancepublic class LazyInnerClassSingleton
{
//使用LazyInnerclassGeneral的时候,默认会先初始化内部类
//如果没使用,则内部类是不加载的
private LazyInnerClassSingleton()
{
// 防止反射创建多个实例
if (LazyHolder.instance != null)
{
throw new RuntimeException("不允许创建多个实例");
}
}

//每一个关键字都不是多余的,static是为了使单例的空间共李,保证这个方法不会被重写、董载
public static final LazyInnerClassSingleton getInstance()
{
//在返回结果以前,一定会先加载内部类
return LazyHolder.instance;
}

//默认不加载
private static class LazyHolder
{
private static final LazyInnerClassSingleton instance = new LazyInnerClassSingleton();
}
}

LazyHolder 是一个静态内部类,当外部类 LazyInnerClassSingleton 被加载的时候,并不会创建 LazyHolder 实例对象。只有当调用 getInstance() 方法时,LazyHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。

LazyInnerClassSingleton 构造函数里判断不为空抛异常是为了只创建一个实例,防止因为使用反射破坏单例。

但这种单例还有可被序列化,反序列化破坏。

枚举式单例模式

基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。具体的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public enum EnumSingleton
{
INSTANCE;

private Object data;

public Object getData()
{
return data;
}

public void setData(Object data)
{
this.data = data;
}

public static EnumSingleton getInstance()
{
return INSTANCE;
}
}

枚举式单例不会被序列化,反序列化破坏。

打赏

请我喝杯咖啡吧~

支付宝
微信