ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

单例模式的各种实现方式(Java)

2022-01-26 19:04:31  阅读:166  来源: 互联网

标签:Singleton Java 对象 模式 instance 枚举 private 单例


单例模式的基础实现方式

手写普通的单例模式要点有三个:

  • 将构造函数私有化
  • 利用静态变量来保存全局唯一的单例对象
  • 使用静态方法 getInstance() 获取单例对象

懒汉模式

懒汉模式指的是单例对象的延迟加载,只有在调用 getInstance() 获取单例对象时才会将单例创建出来。懒汉模式适用于对内存要求高的场景。代码如下:

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

饿汉模式

与懒汉模式相对的是饿汉模式,适用于对内存要求不高的场景,在类加载的初始化阶段就完成了单例对象的创建,代码如下:

public class Singleton {
    // 静态变量初始化
    private static Singleton instance = new Singleton();

    private Singleton() {}

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

静态变量的初始化是在类加载阶段的初始化过程进行,在此期间,编译器会自动收集类中所有静态变量的赋值动作和 static 块,生成 <clinit> 方法并执行。比较特殊的一点是,如果多个线程同时初始化 Singleton 类,JVM 会保证只有一个线程能够执行 Singleton 类的 <clinit> 方法,其他线程都必须阻塞等待。而且同一个类加载器下,一个类只会被初始化一次,即 <clinit> 方法只会被执行一次,这就保证了多线程下单例对象只会被创建一次

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15847802.html
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

多线程下的单例模式

单例模式需要保证的一点是,在整个程序运行期间,单例对象只会被创建一次。如果是单线程环境中,这一点很好保证。但如果是多线程环境中,保证这一点并不简单

上面已经说过,饿汉模式的单例模式下,JVM 会保证单例对象只会被创建一次,因此可以保证这一点。而懒汉模式在多线程环境中不能保证这一点,接下来讨论的是对懒汉模式进行改造,让它能够保证这一点

使用synchronized方法

最简单直接的方式就是为 getInstance() 加上 synchronized 关键字,这样确实可以保证多线程环境中,单例对象只会被创建一次。但是 synchronized 方法最大的缺点在于它将获取单例对象这一行为彻底串行化,同一时刻只能有一个线程能执行 getInstance() ,大大降低了并发效率
代码如下:

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

双重检测锁

直接使用 synchronized 方法降低效率的主要原因在于,synchronized 方法的加锁粒度太粗,那么将锁的范围缩小,就可以缓解这一问题,而双重检测锁就是这么实现的。不过为了保证并发的正确性,在内部又加了一道检测,故名为双重检测锁。代码如下:

public class Singleton {
    // 这里的instance一定要定义为volatile变量!!!
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        // 双重锁检测
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

上面代码的关键点有三个:

  • synchronized 加锁的范围更小,这是为了更高的并发效率
  • synchronized 内部还有一道检测,如果线程1进入了同步块,但还未将单例对象创建出来,此时线程2正好绕过了第一道检测,在同步块外等待获取锁定。因此同步块内也要加上一道检测,避免单例对象被重复创建
  • instance 这个变量一定要声明为 volatilevolatile 在这里最大的作用是禁止指令重排序。如果不加 volatile 修饰,由于 instance = new Singleton() 可能被重排序而导致在这条语句执行过程中,instance 率先被分配内存并获得地址,成为非 null,但构造函数却没有真正执行完毕,此时别的线程可能拿到的 instance 就是不完全构造的单例对象

instance = new Singleton() 这条语句正常的执行顺序是:
1、为即将创建的对象分配一块内存
2、执行构造函数中的语句,对内存进行相应的读写操作
3、让 instance 指向这块内存
在重排序情况下顺序可能是 1 -> 3 -> 2,当执行到3时 instance 就成为非 null,此时其他线程如果引用了 instance,拿到的就是一个不完全构造的对象

需要注意的是,在 JDK5 之前,就算加了 volatile 关键字也依然有问题,原因是之前的 JMM 是有缺陷,volatile 变量前后的代码仍然可以出现重排序问题,这个问题在 JDK5 之后才得到解决,所以现在才可以这么使用

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15847802.html
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

其他单例模式的实现方式

基于枚举类

基于枚举类的方式非常简洁,只要简单地编写一个只包含一个元素的枚举类,由 JVM 来保证单例的唯一性和线程安全性,自带私有的构造方法并且序列化和反射都不会破坏单例的唯一性,据说是 JDK5 之后最好的单例创建方式

public enum Singleton {
    instance;
    
    // 定义各种字段、方法
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return instance;
    }
}

其中枚举类的构造器不用特意加上 private 修饰,因为枚举类构造器默认就是 private 的,且只能使用 private 修饰

简单理解枚举实现单例的过程:程序启动时,会自动调用 Singleton 的构造器,实例化单例对象并赋给 instance,之后再也不会实例化,这也是一个饿汉过程,即使没有调用过 getInstance(),也会将单例对象创建出来

使用枚举来创建单例模式的优势有3点:

  • 代码量更少,更加简洁
  • 没有做任何额外的操作,就可以保证单例的唯一性和线程安全性
  • 使用枚举类可以防止调用者使用反射、序列化和反序列化机制强制生成多个单例对象,破坏唯一性

这第三点优势让基于枚举类的单例模式变得“无懈可击”了,枚举类可以保证唯一性的原理如下:

  • 防反射

枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是枚举类,如果是则抛出异常

  • 防反序列化创建多个枚举对象

对于枚举类型,由于枚举类和枚举变量的组合名是唯一的,可以唯一确定对象。因此,序列化只会将枚举类名 + 枚举变量名输出到文件中。反序列化时,读入的就是枚举类名 + 枚举变量名,再根据 Enum 类的 valueOf 方法,在内存中找对已经存在的枚举对象,并不会创建新的对象

类加载器对单例模式的影响

同一个类加载器对一个类只会加载一次,但是不同的类加载器可能会多次加载同一个类,如果程序中有多个类加载器,需要在单例中指定某个特定的类加载器,并保证这个类加载器始终是同一个

标签:Singleton,Java,对象,模式,instance,枚举,private,单例
来源: https://www.cnblogs.com/frankiedyz/p/15847802.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有