单例模式概念:
单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的一个类只有一个实例。即一个类只有一个对象实例。(参考百度百科)
实现思路:
- 构造方法私有化
- 声明一个静态私有的本类对象
- 给外部提供一个静态方法获取对象实例
实现方式:
实现方式有很多种本文提出五种:
一. 饿汉模式
/**
* 饿汉模式
* 类被加载后对象被创建
*
* 缺点:没有懒加载效果
*
* @author lixiang
* @date 2019年04月16日 - 11:05
* @history 2019年04月16日 - 11:05 lixiang create.
*/
public class HungrySingleton {
private static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
二. 懒汉模式
/**
* 懒汉模式
* 调用getInstance方法后才创建对象 (延时加载、懒加载)
* 缺点:存在线程安全问题(具体见下文双重校验锁分析)
*
* @author lixiang
* @date 2019年04月16日 - 11:13
* @history 2019年04月16日 - 11:13 lixiang create.
*/
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (null == lazySingleton) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
三. 双重校验锁
**
* 双重校验锁
* 较常用
* 优点:线程安全
* 效率较高
* 延迟加载
*
* @author lixiang
* @date 2019年04月16日 - 11:14
* @history 2019年04月16日 - 11:14 lixiang create.
*/
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton doubleCheckSingleton = null;
private DoubleCheckSingleton() {
}
public static DoubleCheckSingleton getInstance() {
// 第一次校验
if (null == doubleCheckSingleton) {
synchronized(DoubleCheckSingleton.class) {
// 第二次校验
if (null == doubleCheckSingleton) {
doubleCheckSingleton = new DoubleCheckSingleton();
}
}
}
return doubleCheckSingleton;
}
这里重点说下双重校验锁模式,为什么要进行双重校验?为什么加volatile
关键字?
容我给你一一道来:1.如果我们去掉第一个 if (null == doubleCheckSingleton)
校验,聪明的你肯定发现了代码是可以在并发情况下正常运行的,因为有synchronized
来保证我们的线程安全。既然用了synchronized
那必然会损耗性能,因为一旦有线程进入了 synchronized(DoubleCheckSingleton.class){}
块那后续线程就需要阻塞等待。那我们如何在此基础上来提高性能呢?于是就有了第一个 if (null == doubleCheckSingleton)
校验。
2.如果我们去掉第二个if (null == doubleCheckSingleton)
校验,此时代码就有问题了,问题出在哪里呢?
假设当doubleCheckSingleton == null
时来了两个线程A、B同时到达第一个 if (null == doubleCheckSingleton)
校验,毫无疑问顺利通过校验执行if
里面的方法,这时因为synchronized
的缘故只能有一个线程进入另一个线程等待。假设A线程进入了synchronized
块执行了doubleCheckSingleton = new DoubleCheckSingleton();
结束后,B线程再次执行doubleCheckSingleton = new DoubleCheckSingleton();
显然这违背了我们单例模式的初衷。
3.如果我们去掉volatile
关键字,此时代码也是有问题的。在JVM中创建对象和赋值操作是分开进行的,也就是说doubleCheckSingleton = new DoubleCheckSingleton();
语句是分三步执行的:
1). 分配内存空间。
2). 初始化对象。
3). 将doubleCheckSingleton
指向刚分配好的内存地址。
但是JVM并不保证这三个操作的先后顺序,也就是说有可能JVM会按照1–>3–>2来执行。这样就可能出错了,我们以A、B两个线程为例:
a) A线程先进来,由于doubleCheckSingleton == null
,A线程进入了第一个if判断,然后进入synchronized
块,并执行doubleCheckSingleton = new DoubleCheckSingleton();
b) 由于JVM内部的优化机制,JVM先分出了一些内存,并赋值给doubleCheckSingleton (注意此时JVM还没有初始化这个实例),此时A线程切换到B线程。
c) B进行第一个if判断,由于此时doubleCheckSingleton != null
,因此它马上 return doubleCheckSingleton;
并将结果返回给调用该方法的程序。这时B线程打算使用doubleCheckSingleton
实例,却发现它没有被初始化,于是错误发生了。
volatile
关键字足矣另写一篇博客,本文不对其进行具体展开了,这里我们利用的是:对 volatile
变量的写操作,不允许和它之前的读写操作打乱顺序;对 volatile
变量的读操作,不允许和它之后的读写乱序。 如果有时间我再另写一篇博客深入探究一下volatile
关键字。
四. 静态内部类
/**
*
* 静态内部类
*
* 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,
* 在类进行初始化时,别的线程是无法进入的
*
* 优点:懒加载效果
* 线程安全
* 效率高
*
* @author lixiang
* @date 2019年04月16日 - 14:52
* @history 2019年04月16日 - 14:52 lixiang create.
*/
public class StaticInnerSingleton {
private StaticInnerSingleton() {
}
private static class SingletonHolder {
private static final StaticInnerSingleton STATIC_INNER = new StaticInnerSingleton();
}
public static StaticInnerSingleton getInstance() {
return SingletonHolder.STATIC_INNER;
}
}
五. 枚举类
/**
* 枚举类
* 优点:避免多线程同步问题
* 防止反序列化重新创建新的对象
*
* @author lixiang
* @date 2019年04月16日 - 14:55
* @history 2019年04月16日 - 14:55 lixiang create.
*/
public enum EnumSingleton {
ENUM_SINGLETON;
}
总结是对学习的检验、沉淀与升华,如果有问题还请不吝赐教!!!