Java单例模式的这几种实现方法,你真的了解吗?

一、介绍

采取一定的方法,让软件系统一个类只能创建和使用一个实例对象,并提供一个取得对象的方法

二、作用

单例模式保证系统中这个类只有一个对象,节省了系统资源,适当使用可以提高系统性能

使用场景

需要频繁的创建和销毁对象、创建对象耗时过多但需要经常用到的、业务要求只能有一个实例的

三、方法

1.饿汉式

  • 解析:所谓饿汉式即像恶狼一样,类一加载就迫不及待先创建出实例,私有化构造器使外部无法直接new对象
  • 优点:写法简单,类一加载便实例化,线程安全
  • 缺点:类一加载便实例化,没有懒加载的效果,如果类一直不需要实例,就会造成空间浪费
java 复制代码
public class Pattern1 {

    private Pattern1() {

    }

    // 饿汉式1
    private static final Pattern1 instance = new Pattern1();

    public static Pattern1 getInstance() {
        return instance;
    }
}
java 复制代码
public class Pattern1 {

    private Pattern1() {

    }

//    饿汉式2
    private static final Pattern1 instance;
    static {
        instance = new Pattern1();
    }

    public static Pattern1 getInstance() {
        return instance;
    }
}
java 复制代码
public class Singleton {
    public static void main(String[] args) {
        Pattern1 instance1 = Pattern1.getInstance();
        Pattern1 instance2 = Pattern1.getInstance();
        System.out.println(instance1 == instance2); // true
        System.out.println(instance1); // 非空
        System.out.println(instance1.hashCode() == instance2.hashCode());// true
    }
}

2、懒汉式

(1)懒汉式1(不可用)

  • 解析:所谓懒汉式即像本身懒得创建单例,等到需要实例时再创建出实例
  • 优点:实现懒加载
  • 缺点:当多个线程同时进入instance == null地判断时可能会返回多个实例而不是单例,线程不安全
java 复制代码
public class Pattern2 {
    private static Pattern2 instance;

    private Pattern2() {

    }

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

(2)懒汉式2(不推荐)

  • 目的:为了解决上述地线程安全问题
  • 实现:实现线程安全很自然想到加同步锁,添加synchronized关键字
  • 缺点:既然用到了锁,就容易出现效率问题;我们分析一下,其实只需要第一次获取实例地时候加锁就行,后续每次得到单例只需要返回单例没有线程安全问题,锁却还在,浪费时间
java 复制代码
public class Pattern2 {
    private static Pattern2 instance;

    private Pattern2() {

    }

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

(3)懒汉式3(失败)

  • 目的:为了解决上述地线程安全效率低问题
  • 实现:既然只有instance == null时才会出现线程安全,不如将锁加在判断语句中,当不为null自然不会进入锁
  • 缺点:好像是解决了效率低下,然而杀敌一千自损一万,这种写法还是线程不安全的;当多个线程进入了判断语句内,instance = new Pattern2()这个语句就已经会执行多次,instance并不是单例
java 复制代码
public class Pattern2 {
    private static Pattern2 instance;

    private Pattern2() {

    }

    public static Pattern2 getInstance() {
        if (instance == null) {
            synchronized (Pattern2.class) {
                instance = new Pattern2();
            }
        }
        return instance;
    }
}

(4)懒汉式4(双重判断)

  • 目的:为了解决上述效率高但是又导致线程安全问题
  • 实现:既然将锁加在判断语句中,判断语句会有多个线程进来,不如在判断语句中的锁里面再进行一次判断,即进行双重判断
java 复制代码
public class DoubleCheck {
    private static volatile DoubleCheck instance;

    private DoubleCheck() {

    }

    public static DoubleCheck getInstance() {
        if (instance == null) {
            synchronized (DoubleCheck.class) {
                if (instance == null) {
                    instance = new DoubleCheck();
                }
            }
        }
        return instance;
    }
}

此处instance必须加volatile注解,目的是保证变量的有序性 ,为什么要保证有序性,还得先讲讲new一个对象具体做的事

  1. 首先在常量池中去定位一个类的符号引用,并检查类是否被加载、连接、初始化过,没有就需要进行类加载
  2. 接下来为对象分配内存,有指针碰撞(连续内存使用)和空闲列表(碎片内存使用)两种分配方式
  3. 分配内存后为对象内存空间初始化为0(除对象头),那么对象不经过初始化便可使用
  4. 对对象头进行一些设置,如GC分代年龄,是否有偏向锁,hashcode(哈希码事实上在调用hashcode方法时才设置),线程ID,是否持有锁等设置
  5. 调用构造函数()方法给第三步初始化为0的字段按照用户要求设置
  6. 将引用指向新生成对象的地址

而上述这些步骤由于指令重排序的原因并不一定按顺序进行,在单线程这是没有问题的,因为单线程中执行new操作时这个线程不会同时又去使用它,一次只会执行一个操作,而多线程中如果线程A执行instance = new DoubleCheck()时先执行的new对象第6步再执行初始化对象,此时线程B判断instance == null为false(instance引用已经得到便不为null)则直接返回一个未初始化的对象并使用就出现问题了

这时给instance变量加上volatile保证new的过程不会进行重排序便不会出现问题

(5)懒汉式5(静态内部类)

  • 优点:同样是既能实现懒加载,又是线程安全的
  • 实现:利用类加载机制实现;静态内部类不会在Pattern3初始化时就加载而是会等到第一次调用getInstance方法时才会加载,这就实现了懒加载,而且JVM在加载静态内部类时是线程安全的
java 复制代码
public class Pattern3 {

    private Pattern3() {

    }

    private static class Provider {
        private static Pattern3 instance = new Pattern3();
    }

    public static Pattern3 getInstance() {
        return Provider.instance;
    }
}

3、枚举类实现(官方推荐)

  • 优点:线程安全的,而且不会被反射破坏单例(前面的例子事实上只要你想都能够手动使用反射破坏单例)
  • 缺点:不是懒加载的
  • 实现:使用枚举类实现单例,枚举类是从JVM层面保证单例的
java 复制代码
public enum Pattern4 {
    INSTANCE;

    public static void test() {
        System.out.println("Pattern4");
    }
}

推荐写法

java 复制代码
public class Pattern4 {

    private Pattern4() {

    }

    public static Pattern4 getInstance() {
        return Instance.INSTANCE.getInstance();
    }

    private enum Instance {
        INSTANCE;

        private Pattern4 pattern4;

        // enum类保证了只会使用一次构造函数
        Instance() {
            this.pattern4 = new Pattern4();
        }

        public Pattern4 getInstance() {
            return this.pattern4;
        }
    }
}
java 复制代码
public class Singleton {
    public static void main(String[] args) {
        Pattern4 instance1 = Pattern4.INSTANCE;
        Pattern4 instance2 = Pattern4.INSTANCE;
        instance1.test();// 打印出Pattern4
        System.out.println(instance1.hashCode() == instance2.hashCode());// true
    }
}

四、总结

  • 单例模式保证系统中这个类只有一个对象,节省了系统资源,适当使用可以提高系统性能
  • 使用场景:需要频繁的创建和销毁对象、创建对象耗时过多但需要经常用到的、业务要求只能有一个实例的
  • 对于饿汉式,实现比较简单易懂也线程安全,如果能保证类会至少使用一次完全是可以使用的
  • 对于懒汉式,主要的就是注意线程安全问题

如果您觉得该文章有用,欢迎点赞、留言并分享给更多人。感谢您的支持!

相关推荐
佚先森8 分钟前
2024ARM网络验证 支持一键云注入引流弹窗注册机 一键脱壳APP加固搭建程序源码及教程
java·html
Estar.Lee15 分钟前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
喜欢猪猪17 分钟前
Django:从入门到精通
后端·python·django
一个小坑货17 分钟前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet2721 分钟前
【Rust练习】22.HashMap
开发语言·后端·rust
古月居GYH22 分钟前
在C++上实现反射用法
java·开发语言·c++
uhakadotcom44 分钟前
如何实现一个基于CLI终端的AI 聊天机器人?
后端
儿时可乖了1 小时前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite