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

Java 设计模式——单例设计模式(创建型设计模式),第1张

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

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

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() {
    }

  
    
    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();/
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 及之后有效。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5677443.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存