Java 设计模式——单例设计模式(创建型设计模式)

先分享下个人写的工厂设计模式:
工厂设计模式入门级理解
工厂设计模式进阶(建议详细阅读)
工厂设计模式实战经验分享(基于spring动态加载)
抽象工厂设计模式

先上一段代码,思考下为什么这么写

public class SingletonDemo {
    private static SingletonDemo instance;
    private SingletonDemo(){
        System.out.println("Singleton has loaded");
    }
    public static SingletonDemo getInstance(){
        if(instance==null){
            synchronized (SingletonDemo.class){
                if(instance==null){
                    instance=new SingletonDemo();
                }
            }
        }
        return instance;
    }
}

单例网上有很多,随便搜索就能找到,本文仅做记录,方便自己复习技术点
原文地址:https://www.jianshu.com/p/3f5eb3e0b050

一、概述

单例模式的定义就是确保某一个类只有一个实例,并且提供一个全局访问点。属于设计模式三大类中的创建型模式。
单例模式具有典型的三个特点:
只有一个实例。
自我实例化。
提供全局访问点。

其UML结构图非常简单,就只有一个类,如下图:
在这里插入图片描述

二、优缺点

优点:由于单例模式只生成了一个实例,所以能够节约系统资源,减少性能开销,提高系统效率,同时也能够严格控制客户对它的访问。
缺点:也正是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,这样扩展起来有一定的困难。

三、常见实现方式

常见的单例模式实现方式有五种:饿汉式、懒汉式、双重检测锁式、静态内部类式和枚举单例。而在这五种方式中饿汉式和懒汉式又最为常见。下面将一一列举这五种方式的实现方法:

饿汉式:线程安全

调用效率高。但是不能延时加载。示例:

public class SingletonDemo1 {

    //线程安全的
    //类初始化时,立即加载这个对象
    private static SingletonDemo1 instance = new SingletonDemo1();

    private SingletonDemo1() {
    }

    //方法没有加同步块,所以它效率高
    public static SingletonDemo1 getInstance() {
        return instance;
    }
}

由于该模式在加载类的时候对象就已经创建了,所以加载类的速度比较慢,但是获取对象的速度比较快,且是线程安全的。

思考下,它有什么缺点呢?

懒汉式:线程不安全。

public class SingletonDemo2 {

    //线程不安全的
//类初始化时,不初始这个对象,用到的时候再创建(延时加载)
    private static SingletonDemo2 instance = null;

    private SingletonDemo2() {
    }

    //运行时加载对象
    public static SingletonDemo2 getInstance() {
        if (instance == null) {
            instance = new SingletonDemo2();
        }
        return instance;
    }

}

由于该模式是在运行时加载对象的,所以加载类比较快,但是对象的获取速度相对较慢,且线程不安全。如果想要线程安全的话可以加上synchronized关键字,但是这样会付出惨重的效率代价。

思考下,它有什么优点呢?

懒汉式(双重同步锁)

注意:下面的这段代码是不对的!!!

public class SingletonDemo3 {
//类初始化时,不初始这个对象,用到的时候再创建(延时加载)
    private static volatile SingletonDemo3 instance = null;

    private SingletonDemo3() {
    }

  
    /**
     * 对象延迟加载,只要在要用的时候才会创建
     * 但是为了防止对象多线程时重复创建对象,所以必须加synchronized,效率不高,因为多线程时需要等待
     */
    public static SingletonDemo3 getInstance() {
        if (instance == null) {
            synchronized(SingletonDemo3.class){
                 if(instance == null){
                     instance = new SingletonDemo3();
                 }
            }
        }
        return instance;
    }

}

双重检测锁补充

为什么加了同步锁之后还需要二次判空?
因为如果不二次判空那么有可能会出现以下情况:
在这里插入图片描述
这样的话instance就会被初始化两次,所以在获取到锁后还需要进行二次判空。

为什么要使用volatile关键字?
因为java初始化时有可能会进行指令重排
在这里插入图片描述
所以就可能会出现以下情况:
在这里插入图片描述
加入volatile关键字修饰之后,会禁用指令重排,这样就保证了线程同步。

我个人的理解可以用以一句话概括:使用 volatile 修饰是为了保证实例对象的原子性

注:注意单例模式所属类的构造方法是私有的,所以单例类是不能被继承的。 (这句话表述的有点问题,单例类一般情况只想内部保留一个实例对象,所以会选择将构造函数声明为私有的,这才使得单例类无法被继承。单例类与继承没有强关联关系。)

静态内部类实现单例模式

/**
 * 静态内部类实现单例模式
 */
public class Singleton {

    private static class SingletonClassInstance {
        private static final Singleton instance = new Singleton4();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return SingletonClassInstance.instance;
    }

    //...
}

因为内部类不会因为外部类的加载而加载,只有在使用到内部类时才加载,所以静态内部类的单例实现是延时加载且线程安全的(实例在且只在内部类被加载时创建),又因为没有添加同步锁,所以调用效率也高。

枚举单例

/**
 * 枚举单例
 */
public enum Singleton5 {

    //枚举元素本身就是单例的
    INSTANCE;

    public void operation() {
        //...
    }

}

枚举元素天生就是线程安全的单例,调用效率也高,只是无法延时加载!

四、常见应用场景

网站计数器。
项目中用于读取配置文件的类。
数据库连接池。因为数据库连接池是一种数据库资源。
Spring中,每个Bean默认都是单例的,这样便于Spring容器进行管理。
Servlet中Application
Windows中任务管理器,回收站。
等等。

再述双重检测锁式单例

为了解决上述的懒汉式单例因为同步带来的性能损耗,聪明的程序员想到了使用双重检测锁来解决每次调用都需要同步的问题。尽管双重检测锁背后的理论是完美的,但不幸的是由于 Java 的内存模型允许“无序写入” , 错误的双重检测锁式单例并不能保证它会在单处理器或多处理器计算机上顺利运行。

错误双重检测锁式单例

/**
 * 错误双重检测锁式单例
 */
public class Singleton {
   
    private static Singleton instance = null;
   
    private Singleton() {}
   
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();//erro
                }
            }
        }
        return instance;
    }
}

下面是上述代码的运行顺序:

检测实例是否已经初始化创建,如果是则立即返回
获得锁
再次检测实例是否已经初始化创建成功,如果还没有则创建实例

执行双重检测是因为,如果多个线程通过了第一次检测,并且其中一个首先通过了第二次检测并实例化了对象,剩余的线程不会再重复实例化对象。这样,除了初始化的时候会加锁,后续的调用都是直接返回,解决了多余的性能消耗。

隐患

看似天衣无缝,但是这种实现是有隐患的,这个隐患来自于上述代码中注释了 erro 的一行,这行代码大致有以下三个步骤:

在堆中开辟对象所需空间,分配地址
根据类加载的初始化顺序进行初始化
将内存地址返回给栈中的引用变量

由于 Java 内存模型允许“无序写入”,有些编译器因为性能原因,可能会把上述步骤中的 2 和 3 进行重排序,顺序就成了

在堆中开辟对象所需空间,分配地址
将内存地址返回给栈中的引用变量(此时变量已不在为null,但是变量却并没有初始化完成)
根据类加载的初始化顺序进行初始化
现在考虑重排序后,两个线程出现了如下调用:
在这里插入图片描述
此时 T7 时刻 Thread B 对 instance 的访问,访问到的是一个还未完成初始化的对象。所以在使用 instance 时可能会出错。

解决无序写入问题的尝试

public class Singleton {

    private static Singleton instance = null;

    private Singleton() {}

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

此代码的理论是通过一个局部变量和内部同步代码块,使创建实例在内部同步块中进行并赋值给局部变量,退出内部同步块时实例已经初始化完成,然后再从局部变量赋值给 instance,从而使 instance 引用内存空间时,指向的是一个已经初始化完成的实例。

这个代码理论上是可行的,但是理论却与实际背道而驰。实际上这个代码并不是按照上述理论的步骤执行的, Java 语言规范要求不能将 synchronized块中的代码移出来。但是,并没有说不能将 synchronized 块外面的代码移入 synchronized 块中。 编译器在这里会看到一个优化的机会,此优化会删除上面注释 * 的两行代码,组合并产生以下代码

//编译器优化后的代码
public class Singleton {

    private static Singleton instance = null;

    private Singleton() {}

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

同样还是会遇到之前无序写入导致的问题。

正确的双重检测锁式单例

/**
 * 正确的双重检测锁实现单例模式
 */
public class Singleton3 {

    private static volatile Singleton3 instance = null;

    private Singleton3() {}

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

为了解决上述问题,需要在instance前加入关键字volatile。使用了volatile关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。 但是只在 JDK5 及之后有效。

最后分享一段代码,思考一下为什么这么写:

/**高富帅懒汉单例模式**/
public class BasicLazySingleton {

    private static boolean initialized = false; //单例初始化标志位,添加 volatile 保证其可见性
	
	private DBProperties dbProperties;

    private BasicLazySingleton() {
    	synchronized (BasicLazySingleton.class) { //保证线程安全
	        if (!initialized) { //首次初始化
	            initialized = true; //改变标志位
				
				/**业务逻辑部分**/
				dbProperties = null;
	        } else { //多线程同时初始化时,由于 volatile 内存屏障对可见性的保证,initialized 一定已经是 true 了
	            throw new RuntimeException("单例已经被初始化过"); //抛出异常
	        }
        }
    }

	//final 保证了方法不被重写
    public static final BasicLazySingleton getInstance() {
        return BasicLazySingletonHolder.LAZY;
    }

	public DBProperties getDbProperties {
		return dbProperties;
	}

	/**
	根据 Java 特性
	1、使用内部类,规避了 JVM 加载外部类的时候就单例进行初始化
	2、在外部类被调用的时候内部类才会被加载(类的懒加载)
	3、内部类必须在方法调用之前初始化(对象的懒加载,所以调用 BasicLazySingletonHolder.LAZY 之前,才会去构造 BasicLazySingleton)
	4、由 static 对外部的可见性
	5、final 保证了 LAZY 不被重写
	**/
    static class BasicLazySingletonHolder {
        private static final BasicLazySingleton LAZY = new BasicLazySingleton();
    }
}


分享下个人写的工厂设计模式:
工厂设计模式入门级理解
工厂设计模式进阶(建议详细阅读)
工厂设计模式实战经验分享(基于spring动态加载)
抽象工厂设计模式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不能吃辣的JAVA程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值