Java的单例模式

我的公众号:不颓废的程序员

一、什么是单例模式?

一句话概括: 保证一个类有且仅有一个实例,并提供该实例的全局访问方法

所以为了达到单例的效果,一般要包含三个要素

  • 私有的静态的实例对象 private static Singleton instance

  • 私有的构造方法 private Singleton(){} ,使得在类的外部无法通过new的方式来创建对象

  • 公有的、静态的、访问该实例对象的方法 public static Singleton getInstance(){}

一般单例模式分为:饿汉式单例和懒汉式单例

举个小栗子:我有两条🐟,一条给饿汉,一条给懒汉

饿汉是先把🐟烤熟,不管吃不吃,先烤熟再说,等到饿的时候就直接拿来吃(相当于类加载的时候,就创建实例对象,使用的时候就直接拿来使用,不用再去创建对象)

而懒汉则是非常懒惰,他是等到自己饿的时候才去烤🐟,然后再拿来吃(相当于类加载的时候,不去创建实例对象,等到需要使用实例对象时,才去创建对象)

通过这个例子,大概了解了一下饿汉式单例和懒汉式单例,接下来使用代码来正式讲解单例模式吧


二、饿汉式单例

public class Singleton {

    //私有构造方法使得:在类的外部无法通过new的方式来创建对象
    private Singleton(){}

    //在类加载的时候就创建实例对象了
    private static Singleton instance = new Singleton();

    //提供一个入口,也就是提供该实例的全局访问方法,使得外界能访问到该实例
    public static Singleton getInstance(){
        return instance;
    }

    public static void main(String[] args) {
        System.out.println(Singleton.getInstance());
    }
}

三、懒汉式单例

第一版

public class Singleton{

    //私有构造方法使得:在类的外部无法通过new的方式来创建对象
    private Singleton(){}

    //初始值设置为null
    private static Singleton1 instance = null;

    public static Singleton getInstance(){
        //线程不安全,因为当两个线程(A和B)同时访问if这段代码时,都返回true,就会创建两个不同的对象
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

在多线程下,可能会导致创建多个实例对象,因为当多条线程同时访问 if(instance == null) 这行代码时,都会返回true,就会导致多条线程访问到 instance = new Singleton(); 从而创建多个实例对象

第二版

public class Singleton {

    //私有构造方法使得:在类的外部无法通过new的方式来创建对象
    private Singleton(){}

    //初始值设置为null
    private static Singleton instance = null;

    public static Singleton getInstance(){
        //双重检测机制(DCL,Double Check Lock)
        if(instance == null){  //第一次判空操作
            //使用同步锁
            synchronized(Singleton.class){
                if(instance == null){  //第二次判空操作
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

使用双重检测机制,在第一版的基础上改进了,但也不是绝对的线程安全,还会存在指令重排的问题
那指令重排又是什么意思呢?

比如在Java中的 instance = new Singleton();,在我们看来,它只是一句简单的代码,可是它会被编译器编译成如下JVM指令:

memory =allocate(); //1:分配对象的内存空间

ctorInstance(memory); //2:初始化对象

instance =memory; //3:设置instance指向刚分配的内存地址

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

memory =allocate(); //1:分配对象的内存空间

instance =memory; //3:设置instance指向刚分配的内存地址

ctorInstance(memory); //2:初始化对象

当线程A执行完1,3,时,instance对象还未完成初始化,但此时instance已经不再指向null。如果线程B此时抢占到CPU资源,执行if(instance == null)的结果会是false,这样就会出现一个问题:返回一个没有初始化完成的instance对象。
在这里,我看到有些人会有疑问?

截取某博客的评论:虽然书里也是说要加volatile关键字,但是还是和你有同样的疑问。new Instance()确实非原子操作,但是synchronized不正是为了保证非原子操作的线程安全才用的么?同一时刻,能进到synchronized代码块里的不应该只有一个线程么,怎么会有线程A没执行完new Instance()指令,线程B就进到了synchronized代码块里的情况呢?

这里说一下,线程B是在执行第一个 if(instance == null) 的时候,得到结果为false,于是就直接执行了return instance; 返回了一个没有初始化完成的instance对象,而不是进到 synchronized 代码块里,去执行第二个 if(instance == null) ,所以说线程B没有进入 synchronized 代码块里,线程B是在第一次判空操作时就已经返回 instance 了。

第三版

就是将第二版的 private static Singleton instance = null;

改为 private volatile static Singleton instance = null;

也就是加了volatile关键字修饰

volatile关键字在此不展开讲述,改天专门写篇文章介绍一下volatile,在这里简单说说它的几个特性吧

  • 保证变量的可见性
  • 不保证变量的原子性
  • 禁止指令重排

第三版中添加了 volatile ,就可以避免指令重排的问题了,这时候线程是安全的了,可是使用Java的反射,照样可以创建多个实例对象,等会我们再聊聊反射

第四版

这一版不是对第三版的改进,只是提供另一种方法去实现懒汉式单例,这种方法是使用静态内部类去创建实例对象

public class Singleton {
  
    //静态内部类
    private static class LazyHolder{
        private static final Singleton instance = new Singleton();
    }

    //私有构造方法
    private Singleton(){}

    public static Singleton getInstance(){
        return LazyHolder.instance;
    }
}

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

这里简单用几行代码演示一下 :利用反射来破坏单例

//利用反射获取类的构造方法
Constructor<Singleton> d = Singleton.class.getDeclaredConstructor();
//设置构造方法为可访问的
d.setAccessible(true);
//创建对象
Singleton singleton1 = (Singleton)d.newInstance();
Singleton singleton2 = (Singleton)d.newInstance();
//判断两个利用反射创建的对象是否相等
//结果为false,不相等
System.out.println(singleton1.equals(singleton2));

第五版

使用枚举来实现单例,可以阻止反射去构造对象

public enum Singleton {
    INSTANCE;

    public Singleton getInstance(){
        return INSTANCE;
    }
}

使用枚举实现单例之后,我们再来测试一下,利用反射创建对象会出现什么情况?

1.执行下面的代码

//利用反射获取类的构造方法
Constructor<Singleton> d =Singleton.class.getDeclaredConstructor(String.class,int.class);
//设置构造方法为可访问的
d2.setAccessible(true);
//创建对象
Singleton instance = d.newInstance();

我们会发现代码会报错

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.hgq.singleton.Test.main(Singleton.java:39)

我们点击Constructor.java:417,进入到Constructor中的newInstance方法中,看到这两行代码

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");

newInstance方法的完整代码如下:

@CallerSensitive
public T newInstance(Object ... initargs)
    throws InstantiationException, IllegalAccessException,
           IllegalArgumentException, InvocationTargetException{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
    //从这里可以看到,如果使用了枚举,当我们使用反射来创建对象时,会抛出异常           
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
               
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

2.如果执行这段代码

//利用反射获取类的构造方法
Constructor<Singleton> d =Singleton.class.getDeclaredConstructor();
//设置构造方法为可访问的
d2.setAccessible(true);
//创建对象
Singleton instance = d.newInstance();

我们发现代码报了另一个错 java.lang.NoSuchMethodException

Exception in thread "main" java.lang.NoSuchMethodException: com.hgq.singleton.Singleton_enum.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.hgq.singleton.Test.main(Singleton.java:34)

到这里,我们知道了,如果是使用枚举来创建单例,我们使用反射是无法创建新的对象的,也就是使用反射无法破坏单例,所以使用枚举实现单例也是一个不错的选择哦!

在这里插入图片描述

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值