首页 > 其他分享 >[设计模式]单例模式

[设计模式]单例模式

时间:2024-08-09 17:16:59浏览次数:10  
标签:Singleton 静态 singleton 模式 枚举 static 单例 设计模式 public

单例模式

懒汉式,线程不安全

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

懒汉式,线程不安全

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。

懒汉式,线程安全

public class Singleton {
    public static Singleton singleton;
    private Singleton(){

    }
    // 主要就是加了锁机制,避免两个线程都检查到null,然后分别创建了singleton
    public static synchronized Singleton getInstance(){
        if (singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。

饿汉式,线程安全

public class Singleton {
    // static 一方面保证了instance在类加载的时候就会生成
    // 另一方面保证了这个instance是属于这个类的
    private static Singleton singleton = new Singleton();
    private Singleton(){

    }
    public static Singleton getInstance(){
        return singleton;
    }
}

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

双检锁/双重校验锁(DCL,即 double-checked locking),线程安全,效率高

首先来看一下单线程懒汉模式的缺点

public class Singleton {
    // 声明静态变量
    public static Singleton singleton;
    // 私有化构造方法,使得外界不能调用new
    private Singleton(){

    }
    // 构造一个静态方法,属于这个类的静态方法
    // 在这个方法中检查类的静态变量是否已经生成
    // 如果没有生成,在方法中生成
    public static Singleton getInstance(){
        if (singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

一个情况是,如果有一个线程A检查singleton == null条件成立,此时另一个线程B也检查singleton == null条件成立。
B线程new了一个singleton,同样的A线程也new一个singleton,这样就不符合单例模式了。

另外一个情况是,如果线程A判断singleton == null条件成立,
于是开始new singleton(),但是new的过程并不是一气呵成的,
他需要三个步骤,分别是,分配空间,初始化Singleton,把内存空间地址赋给singleton,这三个步骤有可能被指令重排序,即先发生分配空间,把内存空间地址赋给singleton,然后再初始化,

如果分配空间,把内存空间地址赋给singleton,这两步完成之后,B线程进来判断singleton == null不成立,虽然对象没有完全建立起来,但是这个引用已经有值了,直接把未完全new的对象返回了,这是不对的。

首先我们来解决第一个情况,为了避免两个线程进入都进入判断,我们可以给整个函数加锁

public class Singleton {
    // 声明静态变量
    public static Singleton singleton;
    // 私有化构造方法,使得外界不能调用new
    private Singleton(){
    }
    // 构造一个静态方法,属于这个类的静态方法
    // 在这个方法中检查类的静态变量是否已经生成
    // 如果没有生成,在方法中生成
    public static synchronized Singleton getInstance(){
        if (singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

但是实际上这是没有必要的,因为如果对整个函数加锁,在singleton已经存在之后,众多的读操作会被阻塞在函数之外,排队等待进去函数,这是不必要的,不需要阻塞单例生成之后的读操作。

可以采用另一种写法

public class Singleton {
    // 声明静态变量
    public static Singleton singleton;
    // 私有化构造方法,使得外界不能调用new
    private Singleton(){
    }
    // 构造一个静态方法,属于这个类的静态方法
    // 在这个方法中检查类的静态变量是否已经生成
    // 如果没有生成,在方法中生成
    public static Singleton getInstance(){
        synchronized(Singleton.class){
            if (singleton == null){
            	singleton = new Singleton();
        	}
        }
        return singleton;
    }
}

这样写还有一个问题,就是线程确实不需要在函数外进行等待,但是进入函数之后,线程还是在继续等待,等待判断singleton == null,
这样的写法保证了判断和产生新对象是一气呵成的,但是效率不够

public class Singleton {
	private volatile static Singleton singleton;
	private Singleton (){}
	public static Singleton getSingleton() {
		if (singleton == null) {
			synchronized (Singleton.class) {
				// 第一个进来的线程会看到null,后续线程不会看到null
				if (singleton == null) {
					singleton = new Singleton();
				}
			}
		}
		return singleton;
    }
}

这样写一方面保证了效率,因为singleton == null不成立的时候,也就是单例已经产生之后,直接可以进行读操作,不需要排队等待

另一方面也解决了重复new的问题,因为在单例未生成的时候,需要排队等待,不会产生两个线程都判断singleton == null,都产生单例的情况。

情况一到此就解决了,那么情况二呢?

这时候如果线程A正在new新对象,并且发生了执行重新排序,已经完成了分配空间,把地址赋值给引用两个操作,这时线程B走到了第一个singleton == null,发现不成立,直接取走了单例,但是这个时候的单例并不是一个合法的单例。如何解决这个问题呢?

可以使用关键字volatile

volatile有两个作用,

其一,加volatile的对象,如果想要读取这个对象,需要从主内存读取一份,保证读到的是最新的对象,如果对这个对象进行了写操作,要立刻写回主内存,保证主内存中一直都是最新的对象。

其二,禁止指令重排序,这样就保证了new对象的工程中最后才对引用singleton赋值,避免了判断singleton == null不成立,但是读取的时候读到的是一个不完全的singleton。

静态内部类,线程安全,延迟加载

是否 Lazy 初始化:

是否多线程安全:

实现难度:一般

描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。

public class Singleton {
    private static class SingletonHelp{
        private static final Singleton singleton = new Singleton();
    }
    private Singleton(){
    }
    public static Singleton getSingleton(){
        return SingletonHelp.singleton;
    }
}

这种方法一方面克服了传统的饿汉式外部类加载就产生单例的浪费,因为singleton类加载的时候内部类并没有被加载,所以内部类的静态变量并未生成。

另一方面,这种方法也保证了线程安全,因为单例的生成是类加载的时候一次性生成的,不存在线程并发的问题。

PS:静态内部类

静态内部类(static nested class)的类加载过程与普通类有一些相似之处,但也有其独特之处。以下是静态内部类的类加载过程的详细描述:

类加载时机

  • 独立加载:静态内部类是属于外部类的一个静态成员,但它和外部类是独立的。因此,静态内部类不会在外部类加载时自动加载,只有在静态内部类本身被使用时才会加载。
  • 使用场景:静态内部类会在以下情况发生时加载:
    • 静态内部类的静态成员(字段、方法)被调用时。
    • 静态内部类的实例被创建时。
    • 静态内部类的静态初始化块被执行时。

类加载步骤

与普通类的加载过程相同,静态内部类的加载过程包括以下几个步骤:

  1. 加载(Loading)

    • 虚拟机通过类加载器读取静态内部类的.class文件,将其字节码加载到内存中,并创建一个Class对象来表示该类。
  2. 连接(Linking)

    • 验证(Verification):确保静态内部类的字节码文件格式正确,满足Java语言规范的要求。
    • 准备(Preparation):为静态内部类的静态变量分配内存,并将其初始化为默认值。
    • 解析(Resolution):将静态内部类的符号引用转换为直接引用。
  3. 初始化(Initialization)

    • 执行静态内部类的静态初始化块(如果有)和静态变量的初始化赋值语句。

特点和注意事项

  • 独立性:静态内部类与外部类是相对独立的。虽然静态内部类可以访问外部类的静态成员,但它们的类加载过程是独立的。
  • 访问权限:静态内部类可以访问外部类的所有静态成员,包括私有静态成员。
  • 外部类的引用:静态内部类不持有对外部类实例的引用,因为它不依赖于外部类的实例。这与非静态内部类(成员内部类)不同,后者需要持有外部类的实例引用。

代码示例

以下是一个包含静态内部类的简单Java代码示例:

public class OuterClass {
    private static String outerStaticField = "Outer Static Field";

    static class StaticNestedClass {
        static {
            System.out.println("Static Nested Class Initialized");
        }

        void display() {
            System.out.println("Accessing: " + outerStaticField);
        }
    }

    public static void main(String[] args) {
        System.out.println("Main Method Start");
        StaticNestedClass nestedObject = new StaticNestedClass();
        nestedObject.display();
    }
}

说明

  • 在上述代码中,静态内部类StaticNestedClass在其静态成员被访问或者实例被创建时才会被加载和初始化。
  • StaticNestedClass的静态初始化块会在第一次使用时执行,打印出"Static Nested Class Initialized"。
  • 外部类的静态字段outerStaticField可以被静态内部类直接访问。

总结来说,静态内部类的类加载过程与普通类的加载过程类似,只有在静态内部类的成员被访问或实例被创建时,静态内部类才会被加载和初始化。

枚举,最优解

JDK 版本:JDK1.5 起

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。

public enum Singleton {
    SINGLETON;
    public void show() {
        System.out.println("枚举类实现单例");
    }
}

PS:枚举类

在Java中,枚举(enum)是一种特殊的类,用于定义一组固定的常量。枚举类不仅仅是一个简单的常量集合,它还可以包含字段、方法和构造函数。以下是关于Java枚举类的详细说明和使用示例:

基本定义

枚举类使用enum关键字定义。每个枚举常量都是该枚举类的一个实例。

public enum Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}

在这个例子中,Day枚举包含了星期天到星期六的七个常量。

使用枚举常量

你可以直接使用枚举常量,并在代码中进行比较和操作。

public class EnumTest {
    public static void main(String[] args) {
        Day today = Day.WEDNESDAY;

        switch (today) {
            case MONDAY:
                System.out.println("Today is Monday");
                break;
            case TUESDAY:
                System.out.println("Today is Tuesday");
                break;
            case WEDNESDAY:
                System.out.println("Today is Wednesday");
                break;
            // 其他情况...
            default:
                System.out.println("Another day");
        }
    }
}

枚举方法

每个枚举类都继承自java.lang.Enum,因此它有一些内置方法,如values()valueOf()

  • values()方法返回包含所有枚举常量的数组。
  • valueOf(String name)方法返回具有指定名称的枚举常量。
public class EnumMethods {
    public static void main(String[] args) {
        // 使用 values() 方法
        for (Day day : Day.values()) {
            System.out.println(day);
        }

        // 使用 valueOf() 方法
        Day day = Day.valueOf("MONDAY");
        System.out.println("Day is: " + day);
    }
}

枚举可以包含字段、方法和构造函数

枚举类不仅可以包含常量,还可以定义自己的字段、方法和构造函数。

public enum Day {
    SUNDAY("Weekend"), 
    MONDAY("Weekday"), 
    TUESDAY("Weekday"), 
    WEDNESDAY("Weekday"), 
    THURSDAY("Weekday"), 
    FRIDAY("Weekday"), 
    SATURDAY("Weekend");

    private String type;

    // 构造函数
    Day(String type) {
        this.type = type;
    }

    // 获取 type 字段的方法
    public String getType() {
        return type;
    }
}

在这个例子中,Day枚举包含了一个type字段,用于表示每一天是工作日还是周末。

带有抽象方法的枚举

枚举类还可以包含抽象方法,每个枚举常量都必须实现这个方法。

public enum Operation {
    PLUS {
        double apply(double x, double y) { return x + y; }
    },
    MINUS {
        double apply(double x, double y) { return x - y; }
    },
    TIMES {
        double apply(double x, double y) { return x * y; }
    },
    DIVIDE {
        double apply(double x, double y) { return x / y; }
    };

    abstract double apply(double x, double y);
}

在这个例子中,每个枚举常量都实现了apply方法,用于执行相应的数学运算。

枚举实现接口

枚举类可以实现接口,使得枚举实例可以被当作接口的实现来使用。

public enum Direction implements Moveable {
    NORTH, SOUTH, EAST, WEST;

    @Override
    public void move() {
        System.out.println("Moving " + this);
    }
}

interface Moveable {
    void move();
}

在这个例子中,Direction枚举实现了Moveable接口,并且每个枚举常量都可以调用move方法。

结论

Java中的枚举类是一种功能强大的工具,不仅可以定义常量,还可以包含方法、字段、构造函数,甚至实现接口。它提供了类型安全的常量集合,并且可以进行更复杂的操作和扩展。通过枚举类,可以使代码更加清晰和易于维护。

标签:Singleton,静态,singleton,模式,枚举,static,单例,设计模式,public
From: https://www.cnblogs.com/DCFV/p/18351091

相关文章

  • Rust实现构建器模式和使用Bon库中的构建器
    实现构建器模式的一种方式这里参考资料2的文章,修改部分代码后如下。这段代码的目的是使用构建器模式创建和初始化Person对象。以下是各部分的解释:结构体定义Person:定义了一个结构体,包含name、age、address和sex四个字段。address和sex是可选的PersonBuilder:用于逐步构......
  • [设计模式]装饰者模式
    抽象构件publicabstractclassFastFood{publicStringdesc;publicintprice;publicabstractStringgetDesc();publicabstractintgetPrice();}具体构件米饭publicclassRiceextendsFastFood{publicRice(){this.desc......
  • “斯诺克”不等于“台球”-《分析模式》漫谈17
    DDD领域驱动设计批评文集做强化自测题获得“软件方法建模师”称号《软件方法》各章合集“AnalysisPatterns”的第一章有这么一句:Considersomeonewhowantstowritesoftwaretosimulatea game of snooker. 2004(机械工业出版社)中译本的译文为: game翻译成“游......
  • K8S中,flannel有几种模式?
    在Kubernetes(K8S)中,Flannel作为一个流行的容器网络接口(CNI)插件,用于为集群中的容器提供网络互通能力。Flannel支持多种模式来实现其网络功能,主要包括以下几种常见模式:1.VXLAN模式描述:VXLAN(VirtualExtensibleLAN)是Flannel的默认后端驱动,它使用VXLAN封装技术来创建跨节点的虚拟......
  • 基于java+springboot+vue基于MVC模式的考研论坛交流管理系统的设计与实现万字文档和PP
    前言......
  • 策略模式揭秘:如何让飞书、企业微信、钉钉的入职与生日祝福更智能?
    继上一篇飞书、企业微信、钉钉如何精准推送入职与生日祝福背后的数据魔法之后,今天在此基础上分享下策略模式。策略模式是一种行为型设计模式,在工作中使用的频次非常高。生日祝福,入职周年祝福等,每一种都是一种不同的策略。不了解背景的人可以先去看看入职周年祝福与生日......
  • Java设计模式和AOP编程
    Java六大设计原则;Java23种设计模式(在此介绍三种设计模式)Java设计模式单例模式应用场景:spring中bean的作用域用的就是单例模式//基本的单例模式————懒汉式publicclassstudent{//3.创建static修饰的成员变量privatestaticstudentstu;//1.设计私......
  • C# 设计模式之模板方法模式
    总目录前言在日常的工作中,有时候我们做PPT,做合同,做简历,如果我们自己从头去写这些文档,不免有些太过耗时耗力;大多时候都是去找相关的PPT模板,合同模板,简历模板,拿过来直接用。为什么可以使用模板,因此这些资料大部分的信息和信息框架都是一致的,我们只需要将自己差异化的内容填......
  • C# 设计模式之代理模式
    总目录前言其实代理模式,在生活中无处不在;就比如租房,一般都是通过中介或者第三方的App去租房子(此处默认他们都是诚信友好的哈),那么为什么我们不自己找房子呢?租过房子应该都知道,自己找房子太麻烦了,既要找原房东,又要搞合同,出了问题扯皮也扯不清等等一些问题,主要是费时费力。......
  • Java设计模式—责任链模式(Chin of Responsibility)
    目录引言1.职责链设计模式简介1.1定义1.2解决的问题2.设计模式的结构2.1类图2.2示例代码3.优点4.缺点5.实际应用5.1SpringAOP5.2JavaServletFilter5.3ReactorPattern5.4Java中的日志记录库6.结论注意事项引言在软件开发中,设计模式是一......