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 分钟前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭32 分钟前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫1 小时前
泛型(2)
java
超爱吃士力架1 小时前
邀请逻辑
java·linux·后端
南宫生1 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石1 小时前
12/21java基础
java
李小白661 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp1 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
装不满的克莱因瓶2 小时前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb
n北斗2 小时前
常用类晨考day15
java