java工程师面试高频考点之单例模式

Posted 小牛儿帥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java工程师面试高频考点之单例模式相关的知识,希望对你有一定的参考价值。

教你玩转单例模式

在这里插入图片描述
今天来给大家讲讲设计模式中的单例模式,首先明白什么叫单例模式,单例模式简单的理解就是只能new一个对象,不能创建多个对象的一种设计模式。这样的目的是为了节约内存资源,保证数据内容的一致性。

单例模式的定义

定义: 一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,在Windows中只能打开一个任务管理器,这样可以避免因打开多个任务管理器而造成资源的浪费,避免了可能会出现各个窗口显示内容不一致的错误。
在这里插入图片描述
在计算机系统当中,还有Windows的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web应用的配置对象、应用程序的对话框、系统中的缓存等等常常设计为单例。

特点

  • 单例类只有一个实例对象;
  • 该单例对象必须由单例类自行创建;
  • 单例类对外提供一个访问该单例的全局访问点

优点和缺点:

单例模式的优点:

  • 单例模式保证了只有一个实例对象,减少了内存开销
  • 避免了内存的多重占用
  • 单例模式设置了全局的访问点,可以优化和共享资源的访问

单例模式的缺点:

  • 单例模式没有接口,扩展困难。如果要扩展,除了修改源代码,没有其他途径,违背了开闭原则。
  • 在并发测试中,单例模式不利于代码的调试。调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
  • 单例模式的功能代码通常在一个类里,功能设计不合理,很容易违背单一职责原则。

单例模式的应用场景

对于 Java 来说,单例模式可以保证在一个 JVM 中只存在单一实例。单例模式的应用场景主要有以下几个方面。

  • 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
  • 某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
  • 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
  • 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
  • 频繁访问数据库或文件的对象。
  • 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
  • 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等

Singleton的结构与实现

单例模式是设计模式中最简单的模式之一。通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。

单例模式的结构

1.单例类:包含一个实例且能自行创建这个实例的类
2.访问类:使用单例的类
在这里插入图片描述

在这里插入图片描述

单例模式的实现

Singleton模式通常有两种形式,一种懒汉式另一种饿汉式,其实还有双重检测锁式、静态内部类式和枚举单例等等,接下来会一一实现。

1.懒汉式
public class Singleton {
    private static Singleton s ;//加static目的在于优先main方法加载,且是自动声明!
    private Singleton(){//在本类外不允许其他类创建该类的实例,所以只有将构造方法设置为私有
    }
    public static Singleton getSingleton(){
        if(s==null){//如果没有被创建,就先创建
            s = new Singleton();
        }
        return  s;
    }
    
}

优点: 会延迟加载并不会一开始就加载生成Singleton的实例,提高了资源的利用率。
缺点: 存在并发访问的问题
测试类代码:

 public static void main(String[] args) {
        for(int i=0;i<10;i++){//10条线程并发访问
            new Thread(() -> {
                Singleton.getSingleton();
            }).start();

        }
    }

测试结果:
在这里插入图片描述
理论上应该只会出现一个创建Singleton实例,但是在并发的情况下并没有只创建一个实例。

2.双重检测锁式

目的就是为了解决懒汉式的并发访问的问题,加入了sychronized关键字

package com.hello.world;

public class Singleton {
    private static Singleton s ;//加static目的在于优先main方法加载,且是自动声明!
    private Singleton(){//在本类外不允许其他类创建该类的实例,所以只有将构造方法设置为私有
    System.out.println("创建一个Singleton实例");
    }
    public static Singleton getSingleton(){
        if(s==null){//如果没有被创建,就先创建
           synchronized (Singleton.class){
               if(s==null)
                   s = new Singleton();
              }
        }
        return  s;
    }

}

测试类代码:

 public static void main(String[] args) {
        for(int i=0;i<10;i++){//10条线程并发访问
            new Thread(() -> {
                Singleton.getSingleton();
            }).start();

        }
    }

测试结果:
在这里插入图片描述
从结果不难看出已经解决了懒汉式的并发问题。但是这样任然会有问题,因为我们在创建对象时并不是一个完整的原子性操作,而是分为1.分配内存空间;2.执行构造方法;3.把这个对象指向这个空间;单个线程执行的情况下可以是123的顺序执行;但是如果线程one按132的顺序执行到3的时候来了个线程two,此时该对象已经指向分配空间,因此判断two对象不是null,就会直接返回对象,但没有实例化,就会出错。因此指令重排也会导致错误,完整的双重检测锁还要加入Volatile关键字来避免指令重排,完整代码如下:

package com.hello.world;

public class Singleton {
    private volatile static Singleton s ;//加static目的在于优先main方法加载,且是自动声明!
    private Singleton(){//在本类外不 允许其他类创建该类的实例,所以只有将构造方法设置为私有
    System.out.println("创建一个Singleton实例");
    }
    public static Singleton getSingleton(){
        if(s==null){//如果没有被创建,就先创建
           synchronized (Singleton.class){
               if(s==null)
                   s = new Singleton();
              }
        }
        return  s;
    }

}
3.饿汉式

类加载时初始化,不会存在并发访问的问题,会有资源的浪费

package com.hello.world;

public class Singleton {

    private  static  Singleton s= new Singleton();
    private Singleton(){//私有构造器
    }
    public static Singleton getSingleton(){
        return  s;
    }
}

优点: static变量会在类加载时初始化,不涉及多个线程访问该对象的问题,可以省略synchronized关键字
缺点: 类初始化创建对象。如果只是加载本类,而不需要调用getSingleton(),会造成资源浪费。

4.静态内部类式

兼并发高效调用和延时加载的优势

package com.hello.world;

public class Singleton {
  private Singleton(){
  }
    public static class inner{
      private  static final Singleton s = new Singleton();
    }
    public static Singleton getSingleton(){
        return inner.s;
    }

}

延时加载,只有正真调用getSingleton()方法,才会加载静态内部类。线程安全,instance是static final类型,保证内存中只有这样一个实例存在,而且只能赋一次值,从而保证线程安全。兼备了并发高效调用和延迟加载的优势。

5.枚举单例

其实双重检测锁式会被反射破坏,例如以下代码:

package com.hello.world;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Singleton {
     private volatile static Singleton s;//加static目的在于优先main方法加载,且是自动声明!
     private Singleton(){//在本类外不 允许其他类创建该类的实例,所以只有将构造方法设置为私有
    System.out.println("创建一个Singleton实例");
     }
    public static Singleton getSingleton(){
        if(s==null) {//如果没有被创建,就先创建
            synchronized (Singleton.class) {
                if (s == null)
                    s = new Singleton();
            }
        }
        return  s;
    }

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Singleton instance1 = Singleton.getSingleton();
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        Singleton instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

结果如下:
在这里插入图片描述
根据结果能看出单例模式已经被反射破坏,怎么解决呢?在私有构造中加锁!

package com.hello.world;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Singleton {
     private volatile static Singleton s;//加static目的在于优先main方法加载,且是自动声明!
     private Singleton(){//在本类外不 允许其他类创建该类的实例,所以只有将构造方法设置为私有
         synchronized (Singleton.class){
             if(s!=null){
                 throw new RuntimeException("不要试图使用反射破坏异常");
             }
         }
         System.out.println("创建一个Singleton实例");
     }
    public static Singleton getSingleton(){
        if(s==null) {//如果没有被创建,就先创建
            synchronized (Singleton.class) {
                if (s == null)
                    s = new Singleton();
            }
        }
        return  s;
    }

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Singleton instance1 = Singleton.getSingleton();
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        Singleton instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

结果如下:
在这里插入图片描述
这种情况是属于一个实例通过单例获取,另外一个通过反射获取,通过给构造方法加锁的方式解决此问题,要是两个都是反射获取的呢?这种情况留给大家思考了!提示一点采用红绿灯!
枚举单例
优点:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免反射和反序列化的漏洞!
缺点:无延迟加载!
枚举·本身就是一个class,继承了Enum类,而且没有无参构造。

package com.hello.world;

import java.lang.reflect.Constructor;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Singleton {
     private volatile static Singleton s;//加static目的在于优先main方法加载,且是自动声明!
     private Singleton(){//在本类外不 允许其他类创建该类的实例,所以只有将构造方法设置为私有
         synchronized (Singleton.class){
             if(s!=null){
                 throw new RuntimeException("不要试图使用反射破坏异常");
             }
         }
         System.out.println("创建一个Singleton实例");
     }
    public static Singleton getSingleton(){
        if(s==null) {//如果没有被创建,就先创建
            synchronized (Singleton.class) {
                if (s == null)
                    s = new Singleton();
            }
        }
        return  s;
    }

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Singleton instance1 = Singleton.getSingleton();
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        Singleton instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}
enum EnumSingle {
    INSTANCE;

    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

结果:
在这里插入图片描述

总结

每一种创建单例模式各有优势和不足,针对不对问题选择不同的模式是至关重要的,在单例模式中有几个性能指标:
1.消耗资源;2.并发引发的问题;3.反射破坏单例模式;都对应了相应的解决方案!

以上是关于java工程师面试高频考点之单例模式的主要内容,如果未能解决你的问题,请参考以下文章

java工程师面试高频考点之类的加载顺序

Java 面试之单例模式

java工程师面试高频考点之内部类

Java设计模式之单例模式

JAVA就业面试题之单例模式

Java设计模式——创建型模式之单例模式