单例设计模式-Singleton

什么是单例?

单例模式是设计模式中最简单的一种,一般通过私有化构造方法的方式避免外部创建类的实例,只提供一个创建好的实例供外部调用,因此成为单例。

单例模式十分常见,例如springmvc中的javabean实例,数据库的连接池;一般需要减少资源消耗,或者需要保证对象内数据的通用性时,会使用单例模式。

如何实现单例?

简单来讲,单例的设计分为三步:

  1. 私有化构造方法,避免外部直接创建实例
  2. 类的内部初始化一个静态的实例
  3. 提供静态方法,供外部调用这个初始化完成的实例

单例的设计种类

面试必考题,单例的设计分为饿汉式和懒汉式,常见的有如下几种具体实现。

1. 饿汉式

最基础的单例模式,适用于90%的系统;在类加载的时候同时初始化实例
优点:代码简单,并且从jvm层级保证线程安全
缺点:不论是否使用,在类加载的时候就会初始化实例,会在项目启动时就消耗资源

public class Singleton01 {
	// 创建静态实例
    private static final Singleton01 instance = new Singleton01();
	// 私有化构造方法,避免外部调用构造方法创建实例
    private Singleton01() {
    }
	// 提供静态方法,供外部调用这个初始化完成的实例
    public static Singleton01 getInstance() {
        return instance;
    }
}

有的地方会将创建实例的代码写到静态代码块中,这本质上和上面的方式是一样的,静态代码块也会在类加载的时候执行;

	private static final Singleton01 instance;
    // 等同于直接new Singleton()
    static {
        instance = new Singleton01();
    }

这也是为什么能让final修饰过的变量放到静态代码块中初始化的原因,因为静态代码块和静态成员变量的加载是同时期执行的。

2. 懒汉式

由于饿汉式单例会在系统启动时就消耗资源,不能做到“按需加载”,因此在庞大的系统中,为了避免启动时消耗的资源过多,导致启动时间过长,会对非核心的实例进行懒加载(Lazy-init)。

public class Singleton02 {

    private static Singleton02 instance;

    private Singleton02() {
    }

    public static Singleton02 getInstance() {
    	// 获取实例之前先判断实例是否被初始化,如果没有,则进行初始化再返回
        if (instance == null) {
            instance = new Singleton02();
        }
        return instance;
    }
}

但是这样的代码会带来一个问题,在多线程中,如果第一个线程已经进入了初始化单例的代码行了,但是还没有将实例初始化完成,此时后面的线程判断instance还是为null,也会去初始化一个实例,这就造成了线程不安全的问题。
我们可以在多线程下通过打印hashcode来查看一下是否有这个问题

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

由于实例初始化速度过快,可能需要多次运行才会出现这个问题,我们可以在instance = new Singleton02()上方加入Thread.sleep(10)来模拟实例初始化耗时较长的情况。

打印结果如下:

1513195109
494962664
494962664

如何避免线程不安全的问题呢?
最简单的,就是在getInstance方法声明上,添加synchronized修饰符,保证同时只有一个线程去执行获取实例的方法。但是当实例初始化完成后,每次访问方法还需要经过同步锁,会白白消耗资源,于是懒汉式加载有了以下的改进方式。

2.1 懒汉式一:DCL单例模式

DCL:Double check lock双重检查锁。

为了避免不必要的资源浪费,我们需要尽可能将锁的粒度缩小,例如我们可以不把synchronized修饰符放在方法声明中,我们可以放在方法内的代码块上。但这还是不够的,为了解决实例被初始化之后每次获取实例还要通过同步锁,我们可以在同步代码块外层先做一次判断,判断实例是否已经初始化了,如果实例已经初始化,则不需要经过锁,直接返回。

public class Singleton03_DCL {

    private volatile static Singleton03_DCL instance;

    private Singleton03_DCL() {
    }

    public static Singleton03_DCL getInstance() {
        // 第一次判断,过滤掉已经初始化过实例的情况,否则每次调用实例都需要加锁
        if (instance == null) {
            synchronized (Singleton03_DCL.class) {
                if (instance == null) {
                    instance = new Singleton03_DCL();
                }
            }
        }
        return instance;
    }
}
双重检查锁模式为什么要加volatile?
防止CPU的指令重排序引起的对象半初始化问题,具体解析请见《深入解析volatile》一文。(我还没写。。)
2.2 懒汉式二:静态内部类模式

DCL检查代码比较复杂,并且由于volatile的存在,可能在使用中引起多核心CPU频繁的数据交换,造成效率问题;
静态内部类方式懒加载单例,是较完美的懒汉式单例的解决方案之一,可以在代码层不使用锁来实现单例的懒加载;
具体实现流程是,在需要创建单例的类中再创建一个静态内部类,由于JVM加载外部类时不会立刻加载其内部的类,我们可以在内部类初始化时创建外部类的单例,由JVM来保证线程安全。

public class Singleton04_InnerClass {
    private Singleton04_InnerClass() {
    }

    public static Singleton04_InnerClass getInstance() {
        return InnerClass.INSTANCE;
    }

    /**
     * 加载外部类时不会加载内部类
     */
    private static class InnerClass {
        /**
         * 如饿汉式单例,线程安全由JVM保证,JVM内部机制保证了每个类只会加载一次
         */
        private static final Singleton04_InnerClass INSTANCE = new Singleton04_InnerClass();
    }
}

3. 枚举单例

单例破坏

以上的单例模式使用java普通类,便无法避免的可以通过反序列化的方式强制调用构造方法,创建新的实例,这种操作被称为单例破坏。

单例破坏的方法后续补充

枚举类单例有如下特点:

  • 由于枚举类本身没有构造方法,可以完美的解决单例破坏的问题
  • 代码十分简洁
  • 枚举类的特性保证了每个成员对象都是单例的

因此枚举单例模式成为了最近几年较为流行的单例模式。

public enum Singleton05_Enum {
    INSTANCE;
}

外部可以通过调用Singleton05_Enum.INSTANCE直接获取实例,并调用实例的方法。

上面这种写法,本质上还是一个饿汉式的单例,枚举对象会随着类的初始化而初始化

而网上常看到的枚举类懒加载单例,是通过内部枚举类来实现的,由于内部枚举类本身就是静态的,因此和静态内部类单例模式类似,只不过换成了枚举类,代码就相对复杂了。

// 无法避免反序列化?待确认
public class Singleton05_Enum {
    private Singleton05_Enum() {
    }

    public static Singleton05_Enum getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton {
        INSTANCE;

        private final Singleton05_Enum instance;

        Singleton() {
            instance = new Singleton05_Enum();
        }

        private Singleton05_Enum getInstance() {
            return instance;
        }
    }
}

作者疑问:

我本人在网上搜索枚举单例时,搜到的都是后面这种内部枚举类的方式,但是我对此有些疑问:
1、在此代码中,枚举类的作用看起来只是保证外部类实例初始化时线程安全,所以是否有必要这么复杂?这种方式相对静态内部类单例有什么优势?
2、外部类仍然可以通过反序列化创建实例,无法保证单例不被破坏,那么使用枚举的意义在哪里?

以上,本人暂时没有查找到准确的资料,如果有了解的朋友解答一下,万分感谢!

结语

任何设计模式,都是服务于系统业务的。正所谓大繁化简,思维越深,越了解如何让设计更简洁,因此,我们不可将了解的所有设计模式强加于一个简单的业务,不要盲目套用。
以单例模式来讲,90%的系统使用饿汉式单例足矣,更多的模式,就留给面试官去吧!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值