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工程师面试高频考点之单例模式的主要内容,如果未能解决你的问题,请参考以下文章