java面试必问之单例模式

单例模式你真的掌握了吗?

单例模式:

​ 确保某个类只有一个实例对象。

单例模式的三要素
  • 定义私有的静态成员
  • 构造函数私有化
  • 提供一个公有的静态方法以构造实例
单例模式第一版(饿汉模式)
package demo01;
//饿汉模式

public class Singleton00 {
    private Singleton00(){
    };
    private static Singleton00 instance = new Singleton00();
    public static Singleton00 getInstance(){
        return instance;
    }
}

饿汉式 :在单例类被加载时,就会实例化一个对象。

优点: 基于classloader机制,避免了多线程的同步问题。

缺点:不能延迟加载,内存效率低,容易产生垃圾对象。

单例模式第二版(懒汉式)
package demo01;
//单例模式第一版
public class Singleton01 {
    private Singleton01(){};   //私有的构造函数
    //instance 是单例对象,也是类静态成员。
    private static Singleton01 instance = null;
    //静态工厂方法
    public static Singleton01 getInstance(){
        if(instance==null){
            instance = new Singleton01();
        }
        return instance;
    }
}

懒汉模式:调用实例的方法时才会实例化对象。

优点:实现了懒加载,节约了空间。

缺点:在不加锁的情况,线程不安全,可能出现多份实例。

​ 在加锁的情况下,程序串行化,使得系统有严重的性能问题。

上面的代码是线程安全的吗?

​ 假设Singleton类刚刚被初始化,instance对象还是空,这时候两个线程同时访问getInstance方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-78iLCbCb-1590127368182)(C:\Users\hcg2018\AppData\Local\Temp\1590124292037.png)]

因为instance是null,所以线程A,B都通过了条件判断,开始进行new操作,这样显然instance被构建了2次。

单例模式第二版(加锁版)
package demo01;
//单例模式第二版
public class Singleton02 {
    private Singleton02(){};  //私有的构造函数
    private static Singleton02 instance = null;  //单例对象,
    public static Singleton02 getInstance(){
        if(instance==null){     //双重检测机制
            synchronized (Singleton02.class){   //同步锁
                if(instance==null){   //双重检测机制
                    instance=new Singleton02();
                }
            }
        }
       return instance;
    }
}

关键点

  1. 为了防止多线程访问时出现执行多次new Singleton02的问题,因此在new操作之前加上Sychronized同步锁,锁住整个类(这里不能是对象锁)。
  2. 进入Synchronized 临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,假如线程A先获取得到类锁,执行代码,当构建完对象instances时(instance不为空啦),线程B也已经通过了最初的判空验证,不做第二次判空的话,线程B还是会再次构建instance对象。
然而这样就一定安全了吗?Really?

在此先复习一个JVM知识点:jvm编译器的指令重排。

比如说 instance = new Singleton02();会被编译器编译成如下编译指令:

Memory = allocate(); (1)分配对象的内存空间

ctorlnstance(memory); (2)初始化对象

instance = memory; (3) instance指向刚分配的内存地址

但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,重排:

Memory = allocate(); (1)分配对象的内存空间

instance = memory; (3) instance指向刚分配的内存地址

ctorlnstance(memory); (2)初始化对象

所以,当线程A执行完1,3时,instance对象还没完成初始化,但已经不在指向null。此时如果线程B获得时间片,执行if(instance==null)的结果会是false,从而返回一个没有初始化完成的instance对象。

so,聪明的你知道该如何解决吗?

可以在instance 对象前增加一个volatile。volatile修饰符阻止了变量访问前后的指令重排,保证了指令的执行顺序。


单例模式第三版(静态内部类版)
package demo01;
//静态内部类
public class Singleton03 {
    
    private static class LaxyHolder{
        private static final Singleton03 instance = new Singleton03();
    }
    private Singleton03(){};
    public static Singleton03 getInstance(){
        return LaxyHolder.instance;
    }
}

关键点

  1. 从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象instance。
  2. instance 对象始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。

静态内部类就无懈可击了吗?你太天真了!

一个"老大难"的问题:无法防止利用反射来重复构建对象。

那么反射是如何破坏单例的呢?

import java.lang.reflect.InvocationTargetException;

//静态内部类
public class Singleton03 {

    private static class LaxyHolder{
        private static final Singleton03 instance = new Singleton03();
    }
    private Singleton03(){};
    public static Singleton03 getInstance(){
        return LaxyHolder.instance;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        /**
         * 利用反射破坏单例模式
         */
        //获得构造器
        Constructor  con = Singleton03.class.getDeclaredConstructor();
        //设置为可以访问
        con.setAccessible(true);
        //构造2个不同的对象
        Singleton03 singleton1 = (Singleton03)con.newInstance();
        Singleton03 singleton2 = (Singleton03)con.newInstance();
        System.out.println(singleton1.equals(singleton2));
    }

}

在这里插入图片描述
显然结果返回了false。

那么如何阻止反射的构建方式?

用枚举实现单例模式

package demo01;

public enum SingletonEnum {
    instance;
}

关键点

有了enum语法糖,jvm会阻止反射获取枚举的私有构造方法。**

唯一的缺点

就是没有使用懒加载,其单例对象是在枚举类被加载的时候进行初始化的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值