首页 > 编程语言 >Java 单例模式

Java 单例模式

时间:2024-10-26 12:58:06浏览次数:6  
标签:Singleton Java 模式 class instance static private 单例 public

原文:Java 单例模式的 7 种写法中,为何用 Enum 枚举实现被认为是最好的方式?

1、懒汉(线程不安全)

public class Singleton {
    private static Singleton instance;
    private Singleton() {}  // 私有构造函数

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  1. 懒加载:是
  2. 线程安全:否
  3. 说明:多线程时禁止使用。

2、懒汉(线程安全)

public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  1. 懒加载:是
  2. 线程安全:是
  3. 说明:使用同步方法保证线程安全,但效率太低。在创建对象之后,不应该限制多线程读取 instance。不推荐用来解决线程安全问题。

3、懒汉(双重校验锁)

public class Singleton {
    private volatile static Singleton singleton;
    private Singleton() {}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                // 注意此处还得有次判空
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
  1. 懒加载:是
  2. 线程安全:是
  3. 说明:使用 volatile 修饰变量保证可见性。这个是第二种方式的升级版。既保证了效率,又保证了安全,代码相比之下较复杂。相比于第二种方式,它的思路是使得对象创建好后,直接返回,不再给线程加锁(通过第一次非空判断实现)。synchronized 代码块内部之所以需要加非空判断,是因为多线程竞争时,可能有多个线程被阻塞,此时只需要第一个进入 synchronized 代码块内部的线程完成对象创建,其他线程即可直接获取,不用重复创建。

4、饿汉

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}
  1. 懒加载:否
  2. 线程安全:是
  3. 说明:在类加载的时候创建对象。

5、饿汉(变种)

public class Singleton {
    private static Singleton instance = null;
    static {
      instance = new Singleton();
    }
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}
  1. 懒加载:否
  2. 线程安全:是
  3. 说明:和上面差不多,都是在类加载的时候创建对象。

6、静态内部类

public class Singleton {
    // 静态内部类
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton() {}

    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
  1. 懒加载:是
  2. 线程安全:是
  3. 说明:注意这种方式和上面两种方式的不同。方式 3、4 都没有懒加载效果。而这种方式Singleton类被装载了,instance不会被立马初始化,因为SingletonHolder类没有被主动使用,只有通过显式调用getInstance方法时,才会显式装载 SingletonHolder 类,达到了懒加载的效果。

7、枚举

public enum Singleton {
    INSTANCE;
    public void whateverMethod() {
    }
}
  1. 懒加载:是
  2. 线程安全:是
  3. 说明:使用枚举方式实现,这种方式是Effective Java作者Josh Bloch提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。这是实现单例模式的最佳方法,不过这种实现方式还没有被广泛采用。

为何枚举方式是最好的单例实现方式?

前几种方式实现单例都有如下 3 个特点:

  1. 构造方法私有化
  2. 实例化的变量引用私有化
  3. 获取实例的方法共有

这类实现方式的问题就在第一点:私有化构造器并不保险。因为它抵御不了反射攻击

以大家最为常用的饿汉式为例,下面使用反射创建多个实例

public class Singleton implements Serializable {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}

public class Main {

    public static void main(String[] args) throws Exception {
        Singleton s = Singleton.getInstance();

        // 拿到所有的构造函数,包括非 public 的
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        // 使用空构造函数 new 一个实例。即使它是 private 的
        Singleton sReflection = constructor.newInstance();

        System.out.println(s); //com.fsx.bean.Singleton@1f32e575
        System.out.println(sReflection); //com.fsx.bean.Singleton@279f2327
        System.out.println(s == sReflection); // false
    }

}

运行输出:

com.fsx.bean.Singleton@1f32e575
com.fsx.bean.Singleton@279f2327
false

通过反射,竟然给所谓的单例创建出了一个新的实例对象。所以这种方式也还是存在不安全因素的。怎么解决?其实Joshua Bloch说了:可以在构造函数在被第二次调用的时候抛出异常。具体示例代码,可以参考枚举实现的源码。

再看看它的序列化、反序列时会不会有问题。如下:

注意:JDK 的序列化、反序列化底层并不是反射。

public class Main {

    public static void main(String[] args) throws Exception {
        Singleton s = Singleton.getInstance();

        byte[] serialize = SerializationUtils.serialize(s);
        Object deserialize = SerializationUtils.deserialize(serialize);

        System.out.println(s);
        System.out.println(deserialize);
        System.out.println(s == deserialize);
    }

}

运行结果:

com.fsx.bean.Singleton@452b3a41
com.fsx.bean.Singleton@6193b845
false

可以看出,序列化前后两个对象并不相等。所以它序列化也是不安全的。

那么枚举呢?

使用枚举实现单例极其的简单:

public enum EnumSingleton {
    INSTANCE;
    public void whateverMethod() {
    }
}

首先看看是否防御反射攻击

public class Main {

    public static void main(String[] args) throws Exception {
        EnumSingleton s = EnumSingleton.INSTANCE;

        // 拿到所有的构造函数,包括非 public 的
        Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        // 使用空构造函数 new 一个实例。即使它是 private 的
        EnumSingleton sReflection = constructor.newInstance();

        System.out.println(s);
        System.out.println(sReflection);
        System.out.println(s == sReflection);
    }

}

结果运行就报错:

Exception in thread "main" java.lang.NoSuchMethodException: com.fsx.bean.EnumSingleton.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at com.fsx.maintest.Main.main(Main.java:19)

这个看起来是因为没有空的构造函数导致的,还并不能说明防御了反射攻击。那它有什么构造函数呢,可以看它的父类 Enum 类:

// @since 1.5  它是所有 Enum 类的父类,是个抽象类
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
    // 这是它的唯一构造函数,接收两个参数
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
    ...
}

既然它有这个构造函数,那我们就先拿到这个构造函数再创建对象试试:

public class Main {

    public static void main(String[] args) throws Exception {
        EnumSingleton s = EnumSingleton.INSTANCE;

        // 拿到所有的构造函数,包括非 public 的
        Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);// 拿到有参的构造器
        constructor.setAccessible(true);
        // 使用空构造函数 new 一个实例。即使它是 private 的
        System.out.println("拿到了构造器:" + constructor);
        EnumSingleton sReflection = constructor.newInstance("testInstance", 1);

        System.out.println(s);
        System.out.println(sReflection);
        System.out.println(s == sReflection);
    }

}

运行打印:

拿到了构造器:private com.fsx.bean.EnumSingleton(java.lang.String,int)
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at com.fsx.maintest.Main.main(Main.java:22)

第一句输出了,表示我们是成功拿到了构造器Constructor对象的,只是在执行newInstance时候报错了。并且也提示报错在Constructor的 417 行,看看Constructor的源码处:

public final class Constructor<T> extends Executable {
    ...
    public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        ...
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ...
    }
    ...
}

主要是这一句:(clazz.getModifiers() & Modifier.ENUM) != 0。说明:反射在通过 newInstance 创建对象时,会检查该类是否 ENUM 修饰,如果是则抛出异常,反射失败,因此枚举类型对反射是绝对安全的。

那么,枚举对序列化、反序列化是否安全?

public class Main {

    public static void main(String[] args) {
        EnumSingleton s = EnumSingleton.INSTANCE;

        byte[] serialize = SerializationUtils.serialize(s);
        Object deserialize = SerializationUtils.deserialize(serialize);
        System.out.println(s == deserialize); //true
    }

}

结果是:true。因此:枚举类型对序列化、反序列也是安全的。

综上,可以得出结论:枚举是实现单例模式的最佳实践。毕竟使用它全都是优点:

  1. 线程安全
  2. 反射安全
  3. 序列化 / 反序列化安全
  4. 写法简单

ref: 单例模式 | 菜鸟教程

标签:Singleton,Java,模式,class,instance,static,private,单例,public
From: https://www.cnblogs.com/Higurashi-kagome/p/16444190.html

相关文章

  • Java EasyExcel 导出报内存溢出的原因与解决方案
    JavaEasyExcel导出报内存溢出的原因与解决方案在现代企业级应用开发中,数据导出功能是一项常见且重要的任务。随着数据量的不断增长,如何高效、稳定地完成数据导出成为开发者面临的一大挑战。EasyExcel是阿里巴巴开源的一款基于Java的Excel处理工具,它以其高效、简洁的特性,广泛......
  • java计算机毕业设计高校毕业设计选题管理系统(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容一、研究背景随着信息技术的迅猛发展,高校教育管理面临着新的挑战与机遇。在毕业设计选题管理方面,传统的管理模式多依赖于人工操作,例如教师手动发布课题、学生......
  • java计算机毕业设计TT手机销售平台(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容一、研究背景随着信息技术的飞速发展,手机已经成为人们生活中不可或缺的一部分。手机市场规模不断扩大,众多品牌和型号的手机不断涌现。在这样的市场环境下,TT手......
  • java计算机毕业设计高校竞赛信息管理系统(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容一、研究背景随着高校教育的不断发展,各类竞赛活动日益增多且规模不断扩大。传统的竞赛管理方式多依赖人工操作,例如以纸质文件记录竞赛信息、手动统计报名情况......
  • java计算机毕业设计打车平台的设计与实现(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容一、研究背景随着移动互联网技术的飞速发展,人们的出行方式发生了巨大的变革。传统的出租车行业面临着诸多问题,如司机绕路、拒载等现象频繁发生,打车难、打车贵......
  • Java 时间戳 获取当前时间 可读格式
    学习笔记1.时间戳的定义在Java中,时间戳通常表示自1970年1月1日00:00:00UTC以来的毫秒数。Java提供了多种方式来处理时间戳。2.获取当前时间戳你可以使用System.currentTimeMillis()方法来获取当前的时间戳(以毫秒为单位)。longtimestampMillis=System.currentTi......
  • SLF4J 中的适配器模式
    什么是适配器模式适配器模式中,适配器包装不兼容指定接口的对象,来实现不同兼容指定接口。SLF4J中的适配器模式SLF4J是一个日志门面系统,其中提供了统一的Logger等接口,许多框架都会面向SLF4J打印日志,这样就不会和具体的日志框架耦合在一起,框架使用者也就能够很方便地在不同......
  • Java面试真题之中级进阶(线程,进程,序列化,IO流,NIO)
    前言本来想着给自己放松一下,刷刷博客,慕然回首,线程、程序、进程?Java序列化?Java中IO流?JavaIO与NIO的区别(补充)?似乎有点模糊了,那就大概看一下Java基础面试题吧。好记性不如烂键盘***12万字的java面试题整理***简述线程、程序、进程的基本概念。以及他们之间关系是什......
  • Java学习-答题判断程序1-3
    1.前言(1)答题判题程序-1题目:设计实现答题程序,模拟一个小型的测试,要求输入题目信息和答题信息,根据输入题目信息中的标准答案判断答题的结果。知识点总结1.基本语法与结构:学习和应用Java的基本语法,类的定义与实例化。理解如何使用控制流(如循环和条件语句)。2.数据封装与类设......
  • 详解Java之Spring MVC篇一
     目录SpringMVC官方介绍MVCRequestMapping传递参数 无参数单个参数针对String类型 针对Integer类型针对int类型针对自定义类型多个参数参数重命名参数强制一致参数不强制一致传递数组​编辑传递List​编辑传递JSON​编辑从路径中获取参数获取单个......