剑指Offer - 单例模式

剑指Offer - 单例模式_第1张图片
Future.png

单例模式

We all want to as a 有追求的程序猿,这不将早已尘封的《剑指Offer》給拿出来重新拜读一下。该书中面试题2就是单例模式,可见其重要性(Maybe just for Interview).同时也为了这次更加系统地阅读&总结。
PS :之前也是随便翻了几下就束之高阁啦。


前言

我们在面试中经常遇到单例模式(However you as 面试者or面试官),关于单例模式的优秀文章,网上也是俯首皆是。本文Just for me to 心得体会or笔记。如果有幸能帮助到其他人,那我将会更加高兴...

1-饿汉式单例

这个写法就类似于解决了单例模式中的“温饱问题”

public class Singleton {
  // JVM加载该类时,单例对象就会自动创建
    private static Singleton instance = new Singleton();

    private Singleton() {
        System.out.println("构造函数Running....");
    }

    public static Singleton getInstance(){
        return instance;
    }

    /**
     * 证明了没有对instance做延时加载...
     */
    public static void doSomething(){
        System.out.println("Just for fun...");
    }

    public static void main(String[] args) {
        /*
        * 这里没有用到该实例,But 照样给我创建了其实例
        */
        Singleton.doSomething();
    }
}

"Talk is cheap,show me the code"

JVM类的加载原理
  1. JVM在执行类的初始化期间,JVM会获得一把锁,该锁可以同步多个线程对同一个类的初始化

There is no doubt that 该种方案实现简单,且线程安全。但是其没有对instance做相应的延时加载,只要初始化该类就创建其实例,这样就造成了资源浪费。


2-懒汉式单例

“懒汉式”---顾名思义,就是你要我才給,按需分配

/**
 * “懒汉式”,用到实例才加载,否则不加载
 *
 * 缺点:线程不安全...
 */
public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
        System.out.println("构造函数Running...");
    }

    public  static Singleton getInstance(){
        /**
         * 避免重复创建...
         */
        if (instance == null) {
            instance = new Singleton();
        }
       return instance;
    }

    /**
     * HashCode相等说明是同一个实例
     * @return
     */
    @Override
    public int hashCode() {
        return super.hashCode();
    }

    public static void main(String[] args) {
        /**
         * 模拟多线程环境,会发现不是同一个实例...
         */
        for (int i = 0; i < 5; i++) {
            new Thread(() -> System.out.println(getInstance().hashCode())).start();
        }
    }
}

Code 地址:该编辑器在多线程情况下测试单例不好使,有兴趣可以复制到本地去运行测试

剑指Offer - 单例模式_第2张图片
单例模式.png

(PS:原图片链接)

线程不安全,那我们只能去使用同步机制来保证线程安全

3-同步锁的懒汉式

/**
 *
 * 保证线程安全的“饿汉式”单例
 *
 * 即:加入synchronized 同步关键字...
 *
 * 下面的格式将会造成:每次来调用getInstance()都要进行线程同步(即调用synchronized锁)
 *
 * 而实际上, 只需要在第一次调用的时候才需要进行同步,只要单例存在,就没必要进行同步啦...
 *
 */
public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
        System.out.println("构造函数running...");
    }


    public static synchronized Singleton getInstance(){
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    /**
    * 也可以写成这种格式
    * */

//    public static Singleton getInstance(){
//        synchronized (Singleton.class){
//            if (instance != null) {
//                instance = new Singleton();
//            }
//        }
//        return instance;
//    }
    
    @Override
    public int hashCode() {
        return super.hashCode();
    }
    
    public static void main(String[] args) {
        for (int i = 0; i <5 ; i++) {
            new Thread(() -> System.out.println(Singleton.getInstance().hashCode())).start();
        }
    }
}

Source code

代码中的注释已经很清楚了,这里就闲话不扯...

4-双重校验锁(Double-Check)单例

这个是重点...


/**
 * "Double-Check"
 * "双重校验锁"的单例...
 *
 *
 * 其实就是在"同步锁"的基础上,外层 + if 判断...
 *
 * 作用:若单例存在,就不需要进行同步加锁操作synchronized。直接返回实例。从而提高程序性能...
 *
 *
 * PS: 到这里还没有完工,主要原因在于 instance = new Singleton2();
 * 这并非是个原子操作,该句事实上在JVM中大概有三个过程:
 * 1. 給instance 分配内存;
 * 2. 调用Singleton2的构造函数来初始化成员变量,生成实例;
 * 3. 将singleton对象指向分配的内存空间(此时,instance 才是非null的)
 *
 *
 * 但是在JVM的即时编译器中存在指令重排的优化,so 上述的2,3顺序不能保证。
 * 假如执行序列为1-3-2.,当3执行完毕,而2未执行之前,被其他线程抢占了,此时instance已经是非null(但是没有初始化)
 * 线程直接返回了instance,然后使用就报错...
 *
 * 所以需要在instance声明为volatile 就可以啦...
 *
 *
 * volatile关键字的两个功能:
 * 1. 这个变量不会在多个线程中存在副本,直接从内存中读取...
 * 2. 禁止指令重排序优化。
 *
 * 但是这个只在Java 1.5之后有效,因为之前的Java内存模型有缺陷...
 *
 * 总结:
 * 该单例版本有点复杂...
 *
 */
public class Singleton {
    /**
     * 注意这里...volatile关键字
     */
    private volatile static Singleton instance = null;

    private Singleton() {
        System.out.println("running...");
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    @Override
        public int hashCode() {
        return super.hashCode();
    }

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

双重校验锁Source Code

双重校验锁是满足要求,But 有局限性... 别着急,还有更好的

5- 静态内部类实现单例

package offer;

/**
 * @author king
 * @date 2018/5/6
 * 

* 静态内部类实现单例 */ public class Singleton { /** * 创建静态内部类 */ private static class InnerSingleton { /** * 在静态内部类里创建单例 */ private static Singleton instance = new Singleton(); } /** * 私有化构造函数 */ private Singleton() { System.out.println("构造函数Running..."); } public static Singleton getInstance() { return InnerSingleton.instance; } @Override public int hashCode() { return super.hashCode(); } public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Thread(() -> System.out.println(getInstance().hashCode())).start(); } } }

静态内部类单例Source Code


关于单例的小插曲

曾经在面试过程中,问到面试者饿汉式&懒汉式的区别:

曾有面试者告诉我,饿汉式每次加载类都会 new 一次对象,将造成资源浪费。我当时没反应过来,只注意到它回答的“资源浪费”,后来我才明白过来,instance是static的,只会初始化一次,何来多次new 之谈???

  1. 有一次让面试者写个懒汉式单例(先不考虑多线程情况),代码大意如下:
public class Singleton {
  public static  Singleton instance = null;
  private Singleton(){
  }
  //  注意...
  public static Singleton getInstance(){
    instance = new Singleton();
    return instance;
  }

}

少个判空,已非“单例”啊...

总结

无论是剑指offer这本书,还是我们面试中高级岗位时,考察点基本都会设在双重校验锁上,毕竟面试造核弹...

参考文章

单例模式
最全面的单例讲解
深入浅出Singleton

你可能感兴趣的:(剑指Offer - 单例模式)