JavaSE面试题——Singleton单例模式的几种写法

Posted 张起灵-小哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaSE面试题——Singleton单例模式的几种写法相关的知识,希望对你有一定的参考价值。

1.Go!!!

我们都知道,在23种设计模式中,单例模式是最容易理解、最简单的一种了,它也是软件开发中最常用的设计模式之一了。

单例设计模式,即某个类在整个系统中只能有一个实例对象可被获取和使用的代码模式。

主要有以下几个要点:

  1. 这个类只能有一个实例对象:构造函数私有化。
  2. 必须自行创建这个实例:含有一个该类的静态变量来保存这个唯一的实例。
  3. 必须自行向整个系统提供这个实例:直接暴露(new)或者提供实例对象的get方法。

那么这篇文章我来分享一下单例模式的几种常见写法。


2.饿汉式

饿汉式。顾名思义,就是很饿,要吃东西(吃掉这个实例对象),所以不管三七二十一,先把实例对象给new出来。

2.1 直接实例化方式(简洁直观)

/**
 * 饿汉式:在类初始化时,直接创建实例对象,不管你是否需要这个对象
 *
 * (1) 构造函数私有化
 * (2) 自行创建, 并且用静态变量保存
 * (3) 强调这是一个单例,使用final修饰
 * (4) 向外提供这个实例
 */
public class Singleton01 {

    public static final Singleton01 INSTANCE = new Singleton01();

    private Singleton01() {}

}
/**
 *
 */
public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton01 s = Singleton01.INSTANCE;
        System.out.println(s);
    }
}

2.2 枚举方式(最简洁)

/**
 * 枚举类型:表示该类型的对象是有限的几个
 * 我们可以限定为1个, 就成了单例模式
 */
public enum Singleton02 {
    INSTANCE
}
/**
 *
 */
public class SingletonTest02 {
    public static void main(String[] args) {
        Singleton02 s = Singleton02.INSTANCE;
        System.out.println(s);
    }
}

2.3 静态代码块方式(适合复杂实例化)

/**
 * 静态代码块饿汉式(适合复杂实例化)
 */
public class Singleton03 {

    public static final Singleton03 INSTANCE;

    static {
        INSTANCE = new Singleton03();
    }

    private Singleton03() {}
}
/**
 *
 */
public class SingletonTest03 {
    public static void main(String[] args) {
        Singleton03 s = Singleton03.INSTANCE;
        System.out.println(s);
    }
}


3.懒汉式

懒汉式。同样顾名思义,就是很懒,但不至于说很饿,这种方式不会先new对象,而是表现的特别懒,等用到的时候我再new。(延迟创建对象的时机)

3.1 线程不安全方式(适合单线程)

/**
 * 懒汉式:延迟创建这个类的实例对象
 *
 * (1) 构造函数私有化
 * (2) 用一个静态变量保存这个唯一的实例
 * (3) 提供一个静态方法, 获取这个实例对象
 */
public class Singleton04 {

    private static Singleton04 instance;

    private Singleton04() {}

    public static Singleton04 getInstance() {
        if (instance == null) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Singleton04();
        }
        return instance;
    }

}
import java.util.concurrent.*;

/**
 *
 */
public class SingletonTest04 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        Callable<Singleton04> callable = new Callable<Singleton04>() {
            @Override
            public Singleton04 call() throws Exception {
                return Singleton04.getInstance();
            }
        };

        ExecutorService es = Executors.newFixedThreadPool(2);
        Future<Singleton04> f1 = es.submit(callable);
        Future<Singleton04> f2 = es.submit(callable);

        Singleton04 s1 = f1.get();
        Singleton04 s2 = f2.get();

        System.out.println(s1 == s2);
        System.out.println(s1);
        System.out.println(s2);

        es.shutdown();
    }
}

运行上面的测试代码,可以看到在两个线程同时new实例对象时,就因为线程安全问题new了两个实例对象(Singleton04并未重写hashCode、equals方法,所以 == 比较的是对象的内存地址,false表明这是两个不同的对象) 。

3.2 线程安全synchronized方式(适合多线程)

/**
 * 懒汉式:延迟创建这个类的实例对象
 *
 * (1) 构造函数私有化
 * (2) 用一个静态变量保存这个唯一的实例
 * (3) 提供一个静态方法, 获取这个实例对象
 */
public class Singleton05 {

    private static Singleton05 instance;

    private Singleton05() {}

    public static Singleton05 getInstance() {
        if (instance == null) {
            synchronized (Singleton05.class) {
                if (instance == null) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance = new Singleton05();
                }
            }
        }
        return instance;
    }

}

当两个线程同时到来,第一个线程先来getInstance,走到 synchronized 上锁(第二个线程等待),然后睡眠1000ms,让出cpu执行权。第二个线程得到执行权,但是代码块还被第一个线程锁着呢,所以第二个线程进不来。这个时候,第一个线程醒了,进而new了一次实例对象。此时第二个线程判断instance对象是否为null,因为之前已经被第一个线程new过一次了,所以这里instance不为null,直接返回即可。确保了自始至终只new了一次实例对象。

有关synchronized代码块外层的这个 if (instance == null) 是这样的,有这么一种情况:假如不加外层的这个if,那么第一个线程来到 getInstance 方法,上锁执行,进一步完成第一次new实例对象,而这个时候后面的多个线程纷纷到来,他们仍然需要去等待前面的线程依次释放类锁才可以进去判断instance对象是否为空,为空了new 不为空返回,这样效率会偏低。

这就有了一种优化策略,也就是在 synchronized代码块外层再加上一个判断instance是否为空的if,只要有一个线程抢到cpu执行权进去new了一次实例对象,那么后面的线程来到 getInstance 方法,走到第一个if,一看instance对象已经被new过了,直接返回就行了,不需要再傻傻的等待其他线程去释放类锁才可以执行自己的过程了。

import java.util.concurrent.*;

/**
 *
 */
public class SingletonTest05 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        Callable<Singleton05> callable = new Callable<Singleton05>() {
            @Override
            public Singleton05 call() throws Exception {
                return Singleton05.getInstance();
            }
        };

        ExecutorService es = Executors.newFixedThreadPool(2);
        Future<Singleton05> f1 = es.submit(callable);
        Future<Singleton05> f2 = es.submit(callable);

        Singleton05 s1 = f1.get();
        Singleton05 s2 = f2.get();

        System.out.println(s1 == s2);
        System.out.println(s1);
        System.out.println(s2);

        es.shutdown();
    }
}

3.3 静态内部类方式(适合多线程)

/**
 * 在内部类被加载和初始化时,才会创建INSTANCE实例对象
 * 静态内部类不会自动随着外部类的加载和初始化而初始化,它是单独去加载和初始化的
 * 因为是在静态内部类加载和初始化时创建的实例对象,你不调用静态内部类就不会创建实例对象
 * 所以这是线程安全的
 */
public class Singleton06 {

    private Singleton06() {}

    private static class Inner {
        private static final Singleton06 INSTANCE = new Singleton06();
    }

    public static Singleton06 getInstance() {
        return Inner.INSTANCE;
    }

}

以上是关于JavaSE面试题——Singleton单例模式的几种写法的主要内容,如果未能解决你的问题,请参考以下文章

JavaSE面试题之单例模式

经典算法题: 实现Singleton单例模式

面试阿里,字节跳动,腾讯90%会被问到的面试题—— 单例模式

面试阿里,字节跳动,腾讯90%会被问到的面试题—— 单例模式

剑指offer面试题 2. 实现 Singleton模式

JavaSE配置文件java.util.Properties单例模式Singleton