单例设计模式
开篇一张思维图:
单例模式的设计思路是:该类负责创建自己的对象,同时确保这个应用中只有单个对象被创建。
个人理解的单例模式分两个大类:饿汉模式和懒汉模式。
饿汉模式
先介绍饿汉模式,为什么呢?因为这种模式比较简单,并且是线程安全的。
代码实现如下:
/**
* thread safe.
*
* date: 2020/5/24
*
* @author 袁小黑
* @version 1.0.0
*/
public class IvoryTower {
private final static IvoryTower INSTANCE = new IvoryTower();
private IvoryTower() {
}
public static IvoryTower getInstance() {
return INSTANCE;
}
}
上面的案例参考 https://github.com/iluwatar/java-design-patterns 而来。
这一种实现有两个关键点:
-
私有化 构造函数。
-
暴露getInstance及初始化
这个实现是线程安全的。为什么呢?
我们直接看看这个类的字节码(省略了部分,只留下有需要的):
public class com/taldh/design/patterns/singleton/IvoryTower {
// compiled from: IvoryTower.java
// access flags 0x1A
private final static Lcom/taldh/design/patterns/singleton/IvoryTower; INSTANCE
// access flags 0x2
private <init>()V
L0
LINENUMBER 16 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 17 L1
RETURN
L2
LOCALVARIABLE this Lcom/taldh/design/patterns/singleton/IvoryTower; L0 L2 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public getInstance()Lcom/taldh/design/patterns/singleton/IvoryTower;
L0
LINENUMBER 20 L0
GETSTATIC com/taldh/design/patterns/singleton/IvoryTower.INSTANCE : Lcom/taldh/design/patterns/singleton/IvoryTower;
ARETURN
L1
LOCALVARIABLE this Lcom/taldh/design/patterns/singleton/IvoryTower; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x8
static <clinit>()V
L0
LINENUMBER 14 L0
NEW com/taldh/design/patterns/singleton/IvoryTower
DUP
INVOKESPECIAL com/taldh/design/patterns/singleton/IvoryTower.<init> ()V
PUTSTATIC com/taldh/design/patterns/singleton/IvoryTower.INSTANCE : Lcom/taldh/design/patterns/singleton/IvoryTower;
RETURN
MAXSTACK = 2
MAXLOCALS = 0
}
从第40行可以看出IvoryTower.INSTANCE 是在clinit方法中赋值的,其实所有的static方法都是在clinit方法中赋值的,如果看到后面有机会给大家讲字节码可以讲一下这方面(挖个坑)。现在这里先这样记住:static变量,是在clinit方法中赋值的,而所有的clinit方法每个类只执行一次。除非将这个类卸载再加载,否则clinit方法不会执行两次。
所以这个实现是线程安全的,实现也非常简单。但是如果我的类比较大,字段上百个,而且并不是所有引入我的jar的人都会需要这个单例一开始就初始化好。那这个时候这种实现方式就不到好,就需要用到下面的懒汉模式。
懒汉模式
因为线程不安全的实现现实世界是不怎么会用到的,所以这里就不介绍了。
线程安全的最简单实现
/**
* date: 2020/5/24
*
* @author 袁小黑
* @version 1.0.0
*/
public class LazyLoadedInvoryTower {
private LazyLoadedInvoryTower invoryTower = null;
private LazyLoadedInvoryTower() {
}
public static synchronized LazyLoadedInvoryTower getInstance() { // 这里加了synchronized
if (invoryTower != null) {
return invoryTower;
}
invoryTower = new LazyLoadedInvoryTower();
return invoryTower;
}
}
其实上面这种实现虽然简单,但是性能不高。
synchronized实现单例模式的优化
public class LazyLoadedDoubldCheckingInvoryTower {
private volatile LazyLoadedDoubldCheckingInvoryTower invoryTower = null;
private LazyLoadedDoubldCheckingInvoryTower() {
}
public static LazyLoadedDoubldCheckingInvoryTower getInstance() {
if (invoryTower == null) {
synchronized (LazyLoadedDoubldCheckingInvoryTower.class) {
if (invoryTower == null) {
invoryTower = new LazyLoadedDoubldCheckingInvoryTower();
}
}
}
return invoryTower;
}
}
这里对invoryTower 判空了两次。为什么呢?
其实这两个判空称为double checking ,double checking问题是因为多线程访问的时候队列阻塞引起的问题的解决方案。这个如果后面有机会给大家讲多线程会给大家介绍。(再挖个坑)
double checking 的再次优化
public class LazyLoadedDoubldCheckingInvoryTower {
private volatile LazyLoadedDoubldCheckingInvoryTower invoryTower = null;
private LazyLoadedDoubldCheckingInvoryTower() {
}
public LazyLoadedDoubldCheckingInvoryTower getInstance() {
LazyLoadedDoubldCheckingInvoryTower result = invoryTower;
if (result == null) {
synchronized (LazyLoadedDoubldCheckingInvoryTower.class) {
result = invoryTower;
if (result == null) {
invoryTower = result = new LazyLoadedDoubldCheckingInvoryTower();
}
}
}
return result;
}
}
这里只是加了个临时变量result,但是却性能提升了25%(effective java第二版)。大家注意一下invoryTower变量是volatile类型,其实这里也是一把锁,后面有机会再给大家介绍(再三挖坑)。
双重校验锁,线程安全,推荐使用。
其实上面的的实现都会有一个风险就是会被反射破坏。
可以被反射破坏
public class ReflectInvokeSingleton {
public static void main(String[] args) {
try {
LazyLoadedInvoryTower instance = LazyLoadedInvoryTower.getInstance();
Constructor<LazyLoadedInvoryTower> constructor = LazyLoadedInvoryTower.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazyLoadedInvoryTower lazyLoadedInvoryTower = constructor.newInstance();
System.out.println("lazyLoadedInvoryTower = " + lazyLoadedInvoryTower);
System.out.println("instance = " + instance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
获取的对象是不同的实例。
防止被破坏的方式
public class LazyLoadedInvoryTower {
private volatile static LazyLoadedInvoryTower invoryTower = null;
private LazyLoadedInvoryTower() {
if (invoryTower != null) { // 在这一块加一个断言
throw new RuntimeException("invoryTower must be singeton!");
} else {
invoryTower = this;
}
}
public static synchronized LazyLoadedInvoryTower getInstance() {
if (invoryTower != null) {
return invoryTower;
}
invoryTower = new LazyLoadedInvoryTower();
return invoryTower;
}
}
使用枚举实现单例模式
public enum SingletonEnum {
INSTANCE();
private static int logicInteger;
SingletonEnum() {
}
public static int getLogicInteger() {
return logicInteger;
}
}
枚举可以防止反射破坏单例模式
public static void main(String[] args) {
try {
Constructor<SingletonEnum> constructor = SingletonEnum.class.getDeclaredConstructor();
} catch (Exception e) {
e.printStackTrace();
}
}
运行结果:
内部类实现单例模式
public class InitializingOnDemandHolderIdiom {
/**
* private constructor.
*/
private InitializingOnDemandHolderIdiom() {
}
public InitializingOnDemandHolderIdiom getInstance() {
return HelperHolder.INSTANCE;
}
private static class HelperHolder {
private static final InitializingOnDemandHolderIdiom INSTANCE = new InitializingOnDemandHolderIdiom();
}
}
这种的实现的推荐使用在double checking之上,因为它没定义volatile等多线程锁的方法。充分利用了内部类实现的方式,多线程访问性能在double checking实现方式之上,而且内部类实现不会在getInstance调用之前加载,所以也是懒加载的实现之一。
总结:
饿汉模式 | 懒汉模式 | |
---|---|---|
优点 | 实现简单,线程安全 | 不会消耗那多么资源,只有在自己想要使用的时候去实例化这个对象。 |
缺点 | 面对某些比较重的类的时候,但引入jar的人不一定用到,这样会浪费内存空间。 | 实现涉及复杂的多线程编程设计。推荐使用内部类实现方式及枚举实现方式。 |
更多精彩内容,欢迎关注我的公众号