剑指offer题解(Java实现)—— 面试题2:实现Singleton模式

前言

本系列的文章为笔者学习《剑指offer第二版》后的笔记整理以及Java实现,解法不保证与书上一致。

另外,我最近在系统整理一些 Java 后台方面的面试题和参考答案,有找工作需求的童鞋,欢迎关注我的 Github 仓库,如果觉得不错可以点个 star 关注 :

题目描述

设计一个类,我们只能生成该类的一个实例。

解题思路

根据题目要求可知,只能生成一个实例的类是实现了 Singleton(单例)模式的类型。一般来说,单例模式需要满足如下条件:

  • 将构造函数设为私有方法,禁止通过 new 直接创建实例;
  • 通过 new 在代码内部创建一个唯一的实例对象;
  • 定义一个公有静态方法,返回上一步中创建的实例对象(由于是在静态方法中,可推断出上一步创建的实例对象也是静态的)。

通常来说,在解决单例模式的问题时,主要有懒汉式饿汉式两种方式。所谓懒汉式指的是实例为 null 时才创建,而饿汉式则正好相反,甭管这个实例为不为 null,先创建出来再说。

代码实现

不好的解法一:只适用于单线程环境

1、懒汉式

在懒汉式的代码实现中,只有当实例对象 instance 为 null 时才会去创建一个实例以免重复创建。同时将构造方法定义为私有的(private),这样就能确保只创建一个实例。

/**
 * 懒汉模式,实例为空时才创建
 * 线程不安全
 * 
 * @author Rotor
 * @since 2019/9/21 0:33
 */
public class Singleton1 {
    private Singleton1(){

    }
    private static Singleton1 instance = null;
    public static Singleton1 getInstance() {
        // 实例为空才创建
        if (instance == null) {
            instance = new Singleton1();
        }
        return instance;
    }

    public static void main(String[] args) {
        System.out.println(Singleton1.getInstance());
    }
}

2、饿汉式

饿汉式,不管三七二十一,无论你需不需要这个静态属性实例 instance,先创建再说。与懒汉式一样,都要将构造方法定义为私有的。

/**
 * 饿汉式,线程安全
 *
 * @author Rotor
 * @since 2019/9/21 0:33
 */
public class Singleton2 {
    // 饿汉式,不管实例为不为空,先直接 new 出来再说
    private static Singleton2 instance = new Singleton2();
    // 实例私有化(Java中构造函数默认就是私有的)
    private Singleton2() {

    }

    public static Singleton2 getInstance() {
        return instance;
    }

    public static void main(String[] args) {
        System.out.println(Singleton2.getInstance());
    }

解法一在单线程的环境下没问题,但是如果是多线程环境下就会有问题。以懒汉式为例,如果有两个线程同时运行到if (instance == null)这句判空语句,并且 instance 的确没有创建时,那么两个线程都会创建一个实例,此时将不再满足单例模式的要求。

所以,为了解决这个问题,在多线程环境下确保只创建一个实例,我们需要加上同步锁,具体往下看。

不好的解法二:虽然在多线程环境中能工作,但效率不高

/**
 * 在多线程环境下为了确保只创建一个实例,我们需要加上同步锁。
 *
 * @author Rotor
 * @since 2019/9/21 0:33
 */
public class Singleton3 {
    private Singleton3() {

    }

    private static Singleton3 instance = null;
    // 懒汉式,实例 instance 为空时才创建
    public static Singleton3 getInstance() {
    	// 加上同步锁
        synchronized (Singleton3.class) {
            if (instance == null) {
                instance = new Singleton3();
            }
            return instance;
        }
    }

    public static void main(String[] args) {
        System.out.println(Singleton3.getInstance());
    }

加上了同步锁之后,由于同一时刻只有一个线程能得到同步锁,所以多个线程之间重复创建实例的问题得以解决。但是,这个方法时不够高效的。因为每次通过 getInstance()获取 Singleton3的实例,都会试图加上一个同步锁,这是非常耗时的。

可行的解法三:加同步锁前后两次判断实例是否存在

为了解决方法二中频繁加锁耗时的问题,我们可以在方法二的基础上,加同步锁之前前后两次判断实例是否存在。之所以要连续两次判空操作,是因为在实例还没创建之前需要加锁操作,以确保只有一个线程可以创建实例。但是当实例已经创建出来之后,就不再需要执行加锁操作了。

/**
 * 可行的解法:加同步锁前后两次判断实例是否存在
 * 
 * @author Rotor
 * @since 2019/9/21 0:34
 */
public class Singleton4 {
    private static Singleton4 instance;
    private Singleton4() {

    }

    // 懒汉式
    public static Singleton4 getInstance() {
        // 只在第一次创建时,instance 实例为 null 才加锁
        if (instance == null) {
            synchronized (Singleton4.class) {
                if (instance == null) {
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        System.out.println(Singleton4.getInstance());
    }
}

墙裂推荐的解法四:利用静态代码块

在 Java 中,静态代码块会在用到该类的时候(比如类加载,调用了构造方法或者静态方法等)进行唯一的调用,所以我们可以在静态代码块中创建实例对象。

/**
 * 墙裂推荐的解法四:利用静态代码块
 *
 * @author Rotor
 * @since 2019/9/21 0:34
 */
public class Singleton5 {
    private Singleton5() {

    }

    private static Singleton5 instance;

    static {
        instance = new Singleton5();
    }

    public static Singleton5 getInstance() {
        return instance;
    }

    // 类中任意的静态方法
    public static String helloWord() {
        return "Hello World!";
    }

    public static void main(String[] args) {
        // 调用类中任意的静态方法,都会创建 instance 实例,导致过早创建
        Singleton5.helloWord();
        /**
         * 直接通过 Singleton5.instance 就能得到 instance 实例,因为调用任意的构造方法,都会触发
         * 静态代码块的调用,从而会在静态代码块中创建 instance 实例。
         */
        System.out.println(Singleton5.instance);
        System.out.println(Singleton5.getInstance());
    }
}

但是,这种方法也有局限性。因为实例 instance 并不是在第一次调用 getInstance() 方法的时候被创建的。事实上,在第一次用到类 Singleton5 的时候就会去创建 instance 实例。比如,上面的代码中,当我们试图去调用类中的一个静态方法 helloWord() 的时候,静态代码块就会去执行 instance 实例的创建。从而导致了过早创建实例的问题,降低了内存的使用率。

墙裂推荐的解法五:使用静态代码块,按需创建 instance 实例

现在,我们想要的效果是:只有调用 getInstance() 方法时,实例才会创建,调用其他静态方法或其他任何时候都不会创建 instance 实例对象,即完全是按照我们的意愿在合适的时间才会创建 instance 实例。

解法五中,因为定义了一个私有的静态内部类 Nested,并且在该嵌套类中才创建 instance 实例。这也就意味着,只有调用 Nested.instance我们才能获得 instance 实例。而类中只有 getInstance()方法中才会去调用Nested.instance,在其他的静态方法中并没有去调用,自然也就无法得到 instance 实例了。

/**
 * 使用静态代码块,按需创建 instance 实例
 *
 * @author Rotor
 * @since 2019/9/21 0:34
 */
public class Singleton6 {
    private Singleton6() {

    }

    public static Singleton6 getInstance() {
        return Nested.instance;
    }

    public static String helloWorld() {
        return "Hello World!";
    }

    // 专门用于创建 instance 实例的静态内部类
    private static class Nested {
        private static Singleton6 instance;
        static {
            instance = new Singleton6();
        }
    }

    public static void main(String[] args) {
        Singleton6.helloWorld();
        /**
         * 这么调用会出错,只能通过 getInstance()方法才能获取到 instance 对象,因为 instance 对象
         * 只有调用 Nested.instance 才会创建,而在其他静态方法中因为没有调用 Nested.instance,所以
         * 不会创建 instance 实例。
         */
        // System.out.println(Singleton6.instance);
        // 静态方法 getInstance() 中由于调用了 Nested.instance,所以可以正常创建 instance 实例
        System.out.println(Singleton6.getInstance());
    }
}

总结

上面物种解法中,第一种方法在多线程环境中不能正常工作,第二种解法虽然能在多线程环境中 work,但效率不高,第三种解法在加锁前后两次判断实例是否为空,可以高效工作,但多个条件判断嵌套容易出错。

第四、第五种解法都可以高效的工作。但不同的是,第五种解法利用私有嵌套类的特性可以做到真正需要时才会去创建实例,大大提高了内存的使用率。

后记

如果你同我一样想要努力学好数据结构与算法、想要刷 LeetCode 和剑指 offer,欢迎关注我 GitHub 上的 LeetCode 题解:awesome-java-notes

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值