首页 > 编程语言 >Java设计模式【单例模式】

Java设计模式【单例模式】

时间:2023-05-12 16:25:19浏览次数:52  
标签:Singleton Java 对象 instance 实例 线程 单例 设计模式

Java设计模式【单例模式】

设计模式

单例模式

单例模式(Singleton Pattern)是一种创建型设计模式,其主要目的是确保一个类只有一个实例,并提供对该实例的唯一访问点。

优缺点

优点:

  1. 提供了对唯一实例的受控访问。

  2. 由于在系统内存中只存在一个对象,因此可以节约系统资源。

缺点

  1. 单例类的扩展有很大的困难。

  2. 单例类的职责过重,在一定程度上违背了“单一职责原则”。

  3. 对象生命周期。 单例模式没有提出对象的销毁,在提供内存的管理的开发语言中,只有单例模式对象自己才能将对象实例销毁,因为只有它拥有对实例的引用。 在各种开发语言中,比如C++,其他类可以销毁对象实例,但是这么做将导致单例类内部的指针指向不明。

单例模式的使用

饿汉模式

  1. 静态成员变量
/**
 * @author Physicx
 * @date 2023/5/12 下午10:13
 * @desc 单例
 * Created with IntelliJ IDEA
 */
public class Singleton {

    //初始化实例对象
    private static final Singleton instance = new Singleton();

    //私有化构造方法
    private Singleton() {
    }

    //提供获取实例对象方法
    public static Singleton getInstance() {
        return instance;
    }

}
  1. 静态代码块
/**
 * @author Physicx
 * @date 2023/5/12 下午10:13
 * @desc 单例
 * Created with IntelliJ IDEA
 */
public class Singleton {

    //实例对象
    private static final Singleton instance;

    static {
        instance = new Singleton();
    }

    //私有化构造方法
    private Singleton() {
    }

    //提供获取实例对象方法
    public static Singleton getInstance() {
        return instance;
    }

}

饿汉式单例的写法适用于单例对象较少的情况,这样写可以保证绝对的线程安全,执行效率比较高。但是缺点也很明显,饿汉式会在类加载的时候就将所有单例对象实例化,这样系统中如果有大量的饿汉式单例对象的存在,系统初始化的时候会造成大量的内存浪费,换句话说就是不管对象用不用,对象都已存在,占用内存。

懒汉模式

public class Singleton {

    //实例对象
    private static Singleton instance;

    //私有化构造方法
    private Singleton() {
    }

    //提供获取实例对象方法(线程安全)
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

}

线程安全的一种懒汉式写法,在类第一次使用的时候初始化,获取实例的静态方法由synchronized修饰,所以是线程安全的。这种方法每次获取实例对象都加锁同步,效率较低。

双重检测机制(DCL)

public class Singleton {

    //实例对象
    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;
    }

}

实例对象必须用 volatile 修饰,否则极端情况可能出现安全隐患。

以上初始化对象代码被编译后会变成以下三条指令:

  1. 分配对象的内存空间。

  2. 初始化对象。

  3. 设置instance指向刚才分配的内存空间。

如果按照上面的执行顺序则不加volatile没有问题,但是CPU或编译器为了提高效率,可能会进行指令重排,最终顺序变为:

  1. 分配对象的内存空间。

  2. 设置instance指向刚才分配的内存空间。

  3. 初始化对象。

当两个线程同时获取实例对象时,线程A已经将instance指向分配空间但未初始化对象,线程B此时第一次判空已不为空,于是返回instance实例,但是此时返回的实例未初始化会导致后续空指针异常。

DCL这种方式同样也是类第一次使用的时候初始化,初始化代码synchronized修饰线程安全,这种方式只会第一次实例对象才会进行同步,因此效率高。

《Java Concurrency in Practice》作者Brian Goetz在书中提到关于DCL的观点:促使DCL模式出现的驱动力(无竞争同步的执行速度很慢,以及JVM启动时很慢)已经不复存在,因而它不是一种高效的优化措施。延迟初始化占位类模式(静态内部类)能带来同样的优势,并且更容易理解。

静态内部类(延迟初始化)

public class Singleton {

    //私有化构造方法
    private Singleton(){}

    //静态内部类(被调用时加载)
    private static class SingletonHandle {
        private static final Singleton instance = new Singleton();
    }

    //提供获取实例对象方法
    public static Singleton getInstance() {
        return SingletonHandle.instance;
    }

}

利用静态内部类被调用时才加载的特性,通过静态初始化初始Singleton对象,由于JVM将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。因此无论是在被构造期间还是被引用时,静态初始化的对象都不需要显式的同步。

线程安全,效率高,使用的时候才会初始化不浪费内存。

《Java Concurrency in Practice》作者Brian Goetz 推荐这种单例实现方式。

枚举实现方式

除了以上几种常见的实现方式之外,Google 首席 Java 架构师、《Effective Java》一书作者、Java集合框架的开创者Joshua BlochEffective Java一书中提到:单元素的枚举类型已经成为实现Singleton的最佳方法

在这种实现方式中,既可以避免多线程同步问题;还可以防止通过反射和反序列化来重新创建新的对象。

public class Singleton {

    //私有化构造方法
    private Singleton() {}

    enum SingletonEnum {
        SINGLETON;
        private final Singleton instance;

        SingletonEnum() {
            instance = new Singleton();
        }
        //提供获取实例对象方法
        public Singleton getInstance() {
            return instance;
        }
    }

}

调用方式如下:

public static void main(String[] args) {
        Singleton instance1 = Singleton.SingletonEnum.SINGLETON.getInstance();
        Singleton instance2 = Singleton.SingletonEnum.SINGLETON.getInstance();
        System.out.println(instance2 == instance1);
    }

普通的单例模式是可以通过反射和序列化/反序列化来破解的,jvm虚拟机会保证枚举类型不能被反射并且构造函数只被执行一次,而Enum由于自身的特性问题,是无法破解的。当然,由于这种情况基本不会出现,因此我们在使用单例模式的时候也比较少考虑这个问题。

总结

实现方式 优点 缺点
饿汉模式 线程安全,效率高 非懒加载
懒汉模式 线程安全,懒加载 效率低
双重检测机制 线程安全,懒加载,效率高
静态内部类 线程安全,懒加载,效率高
枚举 线程安全,效率高 非懒加载

由于单例模式的枚举实现代码比较简单,而且又可以利用枚举的特性来解决线程安全和单一实例的问题,还可以防止反射和反序列化对单例的破坏,因此在很多书和文章中都强烈推荐将该方法作为单例模式的最佳实现方法

参考:单例模式详解(知乎文章)

设计模式相关其他文章:
Java设计模式总结

标签:Singleton,Java,对象,instance,实例,线程,单例,设计模式
From: https://www.cnblogs.com/physicx/p/17394588.html

相关文章

  • Java设计模式简介(总结)
    Java设计模式简介(总结)什么是设计模式Java设计模式是一组经过验证的解决特定问题的编程技术,这些技术可以帮助开发人员快速、有效地开发高质量的软件。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式分类设计模式一般分为三大类:创建型、结构......
  • java 的 JDK JRE , android 的 SDK NDK , native c++ 的概念
       ......
  • Ohm:用 JavaScript 创造你的编程语言
    导读解析器是一种超级有用的软件库。从概念上简单的说,它们的实现很有挑战性,并且在计算机科学中经常被认为是黑魔法。在这个系列的博文中,我会向你们展示为什么你不需要成为哈利波特就能够精通解析器这种魔法。但是为了以防万一带上你的魔杖吧!我们将探索一种叫做Ohm的新的......
  • google-java-format 插件
    GoogleJavaStyleGuidegoogle-java-formatgoogle-java-format插件可用于重新格式化Java源代码,。启用后,它将替换通常的ReformatCode操作。该操作可以通过Code->ReformatCode触发,也可以使用Ctrl+Alt+L触发。IDEA中使用google-java-format插件在File->Settings->Pl......
  • 【转】JavaScript 执行上下文——JS 的幕后工作原理
    转自译文:JavaScript执行上下文——JS的幕后工作原理。译文中图片不显示,要结合原文看,看着不方便。整理了一份含图片的。所以有了此篇的转载,以方便阅读。以下是正文:原文:JavaScriptExecutionContext–HowJSWorksBehindTheScenes,作者:VictorIkechukwu所有JavaScript代......
  • 关于java中execl导入和导出的问题
    前言:最近没有更新,懈怠了。前两天刚写了一个execl导出的需求,过来总结一下这方面的内容大概在很久很久以前,微软的电子表格软件Excel以操作简单、存储数据直观方便,还支持打印报表,在诞生之初,可谓深得办公室里的白领青睐,极大的提升了工作的效率,不久之后,便成了办公室里的必备工具。随......
  • Java使用Font字体的方法
    Java使用Font字体有两种方式:1.直接使用系统已经安装的字体。Fontfont=newFont("SourceHanSerifSC",Font.PLAIN,18); 2.使用流读取外部字体资源,然后创建字体。InputStreamfontInputStream=ResourceUtil.getStream("font/SourceHanSerifSC.ttf");Fontfont......
  • 客户端javascript对象的几何属性(获得大小及坐标)
    在一些客户端javascript对象中,存在着如宽度、高度、坐标类的几何属性,同时这些属性在不同的浏览器下又有不同的属性名。现在将所有的此类对象的几何属性汇总,便于学习,免得搞混。1.浏览器窗口在桌面的坐标(x,y)Window      IE下:window.screenLeft,window.screenTo......
  • java爬虫如何使用代理
    在Java程序中使用代理是爬取网站数据的常见技术之一。代理服务器允许你通过它来访问某个网站,从而让你可以隐藏自己的真实IP地址或者规避一些地理限制等问题。本文章将介绍如何使用Java实现网络爬虫代理。我们首先将介绍Java提供的代理相关类和方法,然后是如何编写代码实现代理网络......
  • java:常用工具类库
    最近一直在减少造轮子的做法,简单总结了一下各个工具类库:排名不分先后,链接的地址为自己找了几个比较合适的例子,供参考。 1、ApacheCommonApacheCommons是对JDK的拓展,包含了很多开源的工具,用于解决经常会遇到的问题,减少重复工作。 2、GoogleGuava Guava工程包含了若干......