《面试宝典》二 如何实现一个单例及优化

前言

社长,一个爱学习,爱分享的程序猿,始终相信,付出总会有回报的。知识改变命运,学习成就未来。爱拼才会赢!
程序猿学社的GitHub,已整理成相关技术专刊,欢迎Star:
https://github.com/ITfqyd/cxyxs

社长,4年api搬运工程师,之前做的都是一些框架的搬运工作,做的时间越长,越发感觉自己技术越菜,有同感的社友,可以在下方留言。现侧重于java底层学习和算法结构学习,希望自己能改变这种现状。
为什么大厂面试,更侧重于java原理底层的提问,因为通过底层的提问,他能看出一个人的学习能力,看看这个人的可培养潜力。随着springboot的流行,大部分的开发,起步就是springboot。也不看看springboot为什么简单?他底层是如果实现的。为什么同样出来4年,工资差别都很大?这些问题都值得我们深思(对社长自己说的)。
api工程师,顶天了,最多达到某些公司中级工资上限,很难突破高级这道坎。希望我们大家成就更好的自己。回想起往事,不会后悔。

1 每日一面:

面试官隔壁小王:自我介绍一下
社长:面试官,您好!我叫社长….
面试官隔壁小王:你说说如何实现一个单例?都有几种实现方式
社长: 我知道两种,一种是懒汉式,还有一种饿汉式。

懒汉式:

package com.fyqd.test;

public class Singleton {
    private static Singleton instance;  
    private Singleton (){}  

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

饿汉式:

package com.fyqd.test;

/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/1/11 23:46
 * Modified By:
 */
public class HungrySingleton {
    private static HungrySingleton instance = new HungrySingleton();
    private HungrySingleton (){}
    public static HungrySingleton getInstance() {
        return instance;
    }
}

面试官隔壁小王:不错,能回答上来二种。实际上还有很多种。你回去后,可以去了解一下。
社长:嗯嗯,我回去了解一下。

 

 

2 什么是单例?

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意
• 1、单例类只能有一个实例。
• 2、单例类必须自己创建自己的唯一实例。
• 3、单例类必须给所有其他对象提供这一实例。

应用实例:

  1. 一个人只能有一个对象,在这里社长得提醒某些人动不动就new一个对象。
  2. Window所有的文件操作,都是只有一个实例的。大家应该经常遇到某个文件打开后,我们再去删除这个文件,会提示删除失败,这就是应为window只有一个实例的原因。

3 单例模式的几种实现方式?

3.1 懒汉式:

package com.fyqd.test;

/**
 * Description:懒汉式:见名思意,很懒的意思
 *  优点:应用程序一运行的时候,不会直接加载对象,减少不必要的内存开销
 *  缺点:不支持多线程。
 * Author: 程序猿学社
 * Date:  2020/1/11 23:46
 * Modified By:
 */
public class Singleton {
    private static Singleton instance;  
    private Singleton (){
    }

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

3.1.1如何验证懒汉式是线程不安全的?

社友纷纷议论说,这也太简单勒,直接调用两次getIntance方法,比较他们的值是不是相等就行
社友提供的想法:

package com.fyqd.test;

/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/1/12 0:29
 * Modified By:
 */
public class SingletonTest {
    public static void main(String[] args) {
       System.out.println(Singleton.getInstance() == Singleton.getInstance());
       System.out.println(Singleton.getInstance() == Singleton.getInstance());
       System.out.println(Singleton.getInstance() == Singleton.getInstance());
    }
}

对应输出结果

 


看,这不就是小case,为了怕社长你赖皮,我还特意多对比几次,这你服不服。

 

划重点:
我们大多的写法,在单线程里面是不会出现问题的。有问题,也不会让我们学习。一般出问题,就出现多线程里面。

反方:社长

package com.fyqd.test;

/**
 * Description:懒汉式:见名思意,很懒的意思
 *  优点:应用程序一运行的时候,不会直接加载对象,减少不必要的内存开销
 *  缺点:不支持多线程。
 * Author: 程序猿学社
 * Date:  2020/1/11 23:46
 * Modified By:
 */
public class Singleton {
    private static Singleton instance;  
    private Singleton (){
        System.out.println("实例化");
    }

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

在构造方法中打印一句话。看他打印多少次,我们通过控制台观察他的次数,是不是就是实例化的次数
测试类:

package com.fyqd.test.test;

import com.fyqd.test.Singleton;

/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/1/12 11:14
 * Modified By:
 */
public class Test {
    public static void main(String[] args) throws InterruptedException {
       for(int i=0;i<1000;i++){
           new Thread(){
               @Override
               public void run() {
                   Singleton s1 = Singleton.getInstance();
               }
           }.start();
       }
    }
}

测试结果:
社友更加代码验证的时候,记得把次数设置大一点,多运行几次,再下结论。养成良好的学习习惯,成为一个严谨的程序员

 

咦,咋回事,社友们是不是觉得很奇怪,怎么会打印多次,说好的单例模式,一个应用程序中只会实例化一次,一个人只能有一个对象,而不能无限的new,这样是要不得的。

分析:

原因出在这块

 

为了更好地理解消化,我们可以假设有两个线程A,B
可能存在这样一种线程A运行到判断这一行时,线程B抢到执行的权力,先一步创建勒对象,而A还不知道这回事,拿到执行权后,也new对象,这样就会造成多次实例化对象,造成内存不必要的开销。

3.1.2如何解决懒汉式不线程安全?

第一种,加synchronized

 


分析:在多线程中加锁,性能低下,synchronized就是上锁,可以简单的理解为有一个大门,每个人来,都需要从上一个人哪里拿到钥匙,才能进来。还有一种就是大门直接打开。加锁和不加锁,这两种懒汉式写法,实际上就是安全和效率,两者取其一。想安全就效率低,想效率高,就不安全。

 

第二种:Volatile+双重校验锁法

package com.fyqd.test;

/**
 * Description:懒汉式:见名思意,很懒的意思
 *  优点:应用程序一运行的时候,不会直接加载对象,减少不必要的内存开销
 *  缺点:不支持多线程。
 * Author: 程序猿学社
 * Date:  2020/1/11 23:46
 * Modified By:
 */
public class Singleton {
    private  volatile static  Singleton instance;
    private Singleton (){
        System.out.println("实例化");
    }

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

Volatile关键字的作用:

禁止进行指令的重排序。
指令重排包含编译器优化的重排序,指令级并行的重排序,内存系统的重排序。
增加volatile实际上就是增加一个内存屏障,告诉虚拟机,你不要给我多事,按我程序写的来。

是不是有点懵,下面通过一个小例子,我们来简单的说说把

 


指令重排序对单线程没有什么影响,他不会影响程序的运行结果,但是会影响多线程。假设我下面这段代码是在多线程环境里面。他在底层执行的顺序可能是?

 

通过行号来标识
按从上到下跑11 12 13,输出1
可能还存在 11 13 12,输出0 这种情况为什么会存在,就是因为多线程过程中,为了性能更好,会有重排,这也是为什么我在单例哪里加入volatile关键字的原因。就是告诉虚拟机,你不要跟我重排,老老实实的根据我写的代码顺序执行。
注意:不要随便用volatile。这里我只是举例子。实际上还得考虑场景。

3.2 饿汉式:

package com.fyqd.test;

/**
 * Description:饿汉式
 *
 * Author: 程序猿学社
 * Date:  2020/1/11 23:46
 * Modified By:
 */
public class HungrySingleton {
    //应用程序启动的时候就创建,并用静态变量保持
    private static HungrySingleton instance = new HungrySingleton();

    /**
     * 构造器是时间私有,划重点,这就是单例实现的一个重要要素,其他人没有办法实例化对象
     */
    private HungrySingleton (){}
    public static HungrySingleton getInstance() {
        return instance;
    }
}

直接创建对象,不存在线程安全问题。
记忆小技巧:听名字,就知道,他很饿,所以他就不会管很多,会直接创建对象,可以这样记忆。
描述:这种方式比较常用,但容易产生垃圾对象。因为他被加载时,就会被实例化,如果没有一直没有被调用,就会造成内存的浪费。

3.3枚举

/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/1/12 0:22
 * Modified By:
 */
public enum EnumSingleton {
    INSTANCE;
    public void whateverMethod() {
    }
}

描述:jdk1.5版本后引入的,这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制。

每日一问环节:

package com.fyqd.test.test;

/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/1/12 12:51
 * Modified By:
 */
public class Test2 {
    public static void main(String[] args) {

    }
}

运行main方法,社友们觉得这里启动了几个线程?欢迎在下方评论,留上你的答案,正确的答案,会在下篇文章中分享出来。

后记

程序猿学社的GitHub,欢迎Star:
https://github.com/ITfqyd/cxyxs
觉得有用,可以点赞,关注,评论,留言四连发。

 

©️2020 CSDN 皮肤主题: 程序猿惹谁了 设计师: 上身试试 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值