设计模式一:单例模式

我的简书:https://www.jianshu.com/u/c91e642c4d90
我的CSDN:http://blog.csdn.net/wo_ha
我的GitHub:https://github.com/chuanqiLjp
我的个人博客:https://chuanqiljp.github.io/

版权声明:转载需要在醒目注明出处

#设计模式系列

1. 设计模式一:单例模式

2. 设计模式二:观察者模式(发布订阅模式)

一.单例模式的诞生背景

在一个项目中我们需要控制类的实例只能有一个,而且客户端只能从一个全局访问点访问到它就可以使用单例模式,单例模式的本质就是控制实例的数目(在这里就是一个)。单例模式分为懒汉模式和饿汉模式。

二.单例模式的使用场景

1.要求生成唯一的序列号的环境
2.在整个项目中需要有访问一个共享访问点或共享数据,如配置文件;
3.创建一个对象需要消耗的资源过多,如数据库的访问;
4.需要定义大量的静态常量或静态方法(如工具类)的环境。

三.单例模式的代码示例

饿汉模式

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
// 2.创建静态实例
private static final Singleton instance = new Singleton();
// 1.私有构造方法
private Singleton() {
}
// 3.提供静态的对象提供方法
public static Singleton getInstance() {
return instance;
}
}

懒汉模式初级版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
// 2.创建静态变量
private static Singleton instance;
// 1.私有构造方法
private Singleton() {
}
// 3.提供静态的对象提供方法,先检查对象是否实例化,若为null则实例化
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

饿汉模式自带线程安全的属性,但是懒汉模式上面的这种写法,是线程不安全的,当多线程环境中有可能创建多个实例,因此我们可以对getInstance( )方法进行加锁,出现懒汉模式升级版一。

懒汉模式升级版一

1
2
3
4
5
6
public synchronized static Singleton getInstance() {     //性能低
if (instance == null) {
instance = new Singleton();
}
return instance;
}

升级版一虽然解决了线程安全问题,但是由于每次获取对象实例时都需要同步,这样性能会明显下降,其实我们可以增加判断,当对象为null时,才需要同步进行创建对象,如果不为null则可以直接返回,出现懒汉模式升级版二。

懒汉模式升级版二(双重检验加锁)

当获取实例时先判断对象是否实例化,若存在直接返回,若不存在进行同步操作,同步操作再次判断对象是否为null,是则实例化对象,代码入下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {         //适用JDK1.5及以后
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();//由于不是原子操作,禁止指令重排序优化,因此instance需要 volatile关键字修饰
}
}
}
return instance;
}
}

双重检验加锁机制分析

1
2
3
4
5
这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
1.给 instance 分配内存
2.调用 Singleton 的构造函数来初始化成员变量
3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。我们只需要将 instance 变量声明成 volatile 就可以了,volatile禁止指令重排序优化。但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。

懒汉模式升级版三(静态内部类)

这里采用静态初始化的方式,它由JVM来保证线程安全性,当类装载的时候不去初始化对象,在这个内部类中去实例化对象,因此只要不使用内部类就不会创建对象,这样能解决线程安全的问题还可以实现延迟加载且不会增加任何访问开销,代码如下:

1
2
3
4
5
6
7
8
9
public class Singleton {        //推荐,但只能在无参构造的时候使用
private Singleton (){}
private static class SigletonHolder{
private static Singleton instance=new Singleton();
}
public static Singleton getInstance(){
return SigletonHolder.instance;
}
}

四.单例模式的总结(优缺点,比较)

比较:

饿汉模式:类加载速度比较满,获取对象使用时比较快,适用无参构造的,线程安全的;
懒汉模式:类加载速度比较快,第一次获取对象使用时比较慢,线程不安全的(可以通过加锁解决),体现了延迟加载的思想,

解决懒汉模式的线程安全问题:

1
2
3
1、由于懒汉是线程不安全的,因此可以采用双重检查加锁机制,即进入获取实例的方法后先检查实例是否存在,若不存在进入同步代码块,再次检查实例是否存在,如果不存在才真正创建实例,但是需要属性定义时需要volatile 关键字修饰(适用JDK1.5及以后)
2、对getInstance方法进行加锁(性能底);
3、静态内部类的静态属性;

如何选择

如果单件模式实例在系统中经常会被用到,饿汉式是一个不错的选择。反之如果单件模式在系统中会很少用到或者几乎不会用到,那么懒汉式是一个不错的选择。

单例模式的本质

控制实例的数目,当需要控制类的实例只能有一个,而且客户端只能从一个全局访问点访问到它就可以使用单例模式。

局限性

只能保证在一个JVM中保持单例,高并发的服务器中需要其他的控制来实现;

相关知识或概念

1
2
延迟加载(lazy  load): 一开始不要加载资源或者数据,一直等到马上要使用的这个资源或者数据时,实在躲不过去了才去加载;
缓存的思想: 每次操作的时候,先到内存中找,看有没有这些数据,如果有那么直接使用,如果没有那么就去获取他并设置到缓存中,下次访问的时候就可以直接从内存中读取了,从而节省大量的时间,是一种典型以空间换时间的方案

#参考链接
http://www.cnblogs.com/dolphin0520/p/3920373.html
http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/

我的CSDN博客地址:http://blog.csdn.net/wo_ha/article/details/77567446