剑指Offer - 单例模式

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 地址:该编辑器在多线程情况下测试单例不好使,有兴趣可以复制到本地去运行测试

单例模式.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
 * <p>
 * 静态内部类实现单例
 */
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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • 单例模式(SingletonPattern)一般被认为是最简单、最易理解的设计模式,也因为它的简洁易懂,是项目中最...
    成热了阅读 4,143评论 4 34
  • 概念 确保某一个类只有一个实例,而且自行实例化,并向整个系统提供一个访问它的全局访问点,这个类称为单例类。 特性 ...
    野狗子嗷嗷嗷阅读 524评论 0 2
  • 【学习难度:★☆☆☆☆,使用频率:★★★★☆】直接出处:单例模式梳理和学习:https://github.com/...
    BruceOuyang阅读 620评论 1 2
  • 蔡康永在《康永,给残酷社会的善意短信》中说过这么一段话:15岁觉得游泳难,放弃游泳,到18岁遇到一个你喜欢的...
    遇见胡小菌阅读 469评论 0 0
  • 放任自己一整天沉浸在书中,一房之中历经人世年年、大落大起、大喜大悲。 翻完最后一页,放在书架最上面的格子,心满意足...
    陆由_阅读 236评论 0 0