你真的会写单例模式吗?

单例模式

单例模式是一种简单的模式,一般作为学习设计模式的起点。

定义: Ensure a class has only one instance, and provide a global point of access to it. (确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例)

单例模式的通用类图:
在这里插入图片描述
下面就说一下单例模式的实现方式

实现方式

主要有两种实现方式:饿汉式懒汉式

饿汉式

当类加载到内存后,就实例化一个单例,JVM保证线程安全。

步骤:

  1. new出来一个使用staticfinal修饰的实例化对象INSTANCE
  2. 将构造方法设为private类型,不让别人再去实例化Singleton01类。
  3. 定义一个静态方法getInstance,返回INSTANCE。外部使用getInstance方法获取实例化对象。
public class Singleton01 {

    private static Singleton01 INSTANCE = new Singleton01();

    private Singleton01(){}

    public static Singleton01 getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) {
        Singleton01 s1 = Singleton01.getInstance();
        Singleton01 s2 = Singleton01.getInstance();
        System.out.println(s1 == s2); // true
    }
}

优点: 简单实用,推荐使用
唯一缺点: 不管是否用得到,类装载时都会完成实例化。如果实例化对象没有被用到就会造成时间和空间资源的浪费。

懒汉式

懒汉式(线程不安全)

当外部调用getInstance方法时先判断实例化对象INSTANCE是否为null。
如果为null则new出来一个新的对象并赋值给INSTANCE并返回;否则直接返回INSTANCE

public class Singleton02 {

    private static Singleton02 INSTANCE;

    private Singleton02(){}

    public static Singleton02 getInstance(){
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton02();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    System.out.println(Singleton02.getInstance().hashCode())
            ).start();
        }
    }
}

输出:

37565919
518461220
518461220
518461220
518461220
518461220
518461220
518461220
37565919
518461220
518461220
433828562
1199730481
287080281
2142643463
619240014
569889152
225543282
...

优点: 懒加载,解决了饿汉式浪费时间和空间资源的问题。
缺点: 线程不安全。多线程访问时,若线程A先判断INSTANCE == null为TRUE,此时还没实例化对象。线程B再次判断INSTANCE == null也为TRUE,再次实例化对象,此时实例化了两次。由输出结果也可以知道此方法创建了多个实例。

Synchronized

全局锁

将整个getInstance方法加锁

public class Singleton03 {

    private static Singleton03 INSTANCE;

    private Singleton03(){}

    public static synchronized Singleton03 getInstance(){
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton03();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    System.out.println(Singleton03.getInstance().hashCode())
            ).start();
        }
    }
}

优点: 解决了线程不安全的问题
缺点: 对整个方法加锁,效率低下

局部锁(不可行)

妄图通过减少同步代码块的方式提高效率,但是依旧线程不安全,不可行
原因和**懒汉式(线程不安全)**的一样。

	public static Singleton04 getInstance(){
        // 妄图通过减小同步代码块的方式提高效率,不可行
        if (INSTANCE == null) {
            synchronized (Singleton04.class) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Singleton04();
            }
        }
        return INSTANCE;
    }

DCL单例

DCL全称为Double Check Lock(双重锁定检查)
在给Singleton加锁后再判断实例是否为空。

public class Singleton05 {

    private static volatile Singleton05 INSTANCE;

    private Singleton05(){}

    public static Singleton05 getInstance(){
        // 业务逻辑代码省略
        if (INSTANCE == null) {
            // 双重检查
            synchronized (Singleton05.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Singleton05();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    System.out.println(Singleton05.getInstance().hashCode())
            ).start();
        }
    }
}

此时INSTANCE必须使用volatile修饰、
volatile关键字有两个作用:

  1. 保持线程可见性
  2. 禁止指令重排序

创建对象的过程有三个步骤:

  1. 半初始化,申请内存,值为默认值
  2. 设初始值
  3. 将INSTANCE对象指向分配的内存空间

如果有一线程A执行到INSTANCE = new Singleton05()语句,发生了指令重排,步骤二和步骤三颠倒了

  1. 半初始化,申请内存,值为默认值
  2. 将INSTANCE对象指向分配的内存空间(此时INSTANCE非null)
  3. 设初始值

先将INSTANCE对象和内存空间建立联系,此时为默认值。线程B又继续进行,发现INSTANCE非空,于是直接返回INSTANCE,造成各种不可预知的后果。因此在DCL单例模式中,必须要在实例上volatile关键字。

静态内部类实现(重点)

静态内部类实现是面试官最想看到的答案,因为其中涉及到JVM装载的问题。

public class Singleton06 {

    // 私有化构造器
    private Singleton06() {}
    
    private static class SingletonInstance {
        private static final Singleton06 singleton06 = new Singleton06();
    }
    
    // 提供静态公有获取方法
    public static synchronized Singleton06 getInstance() {
        return SingletonInstance.singleton06;
    }
}

程序启动时静态内部类并不会被装载,而且装载内部类是线程安全的,所以这个单例模式真正意义上实现了懒加载线程安全节省了内存,必须要掌握。

枚举

因为枚举天生单例,故可以使用枚举类实现单例模式,是一种比较特殊冷门的单例模式的实现方式,了解即可。

enum Singleton07 {

    INSTANCE;

    public void updateInstance() {}
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值