设计模式--单例最终篇

1.前言

单例模式一直有很多种,什么饿汉式懒汉式啥的,还有线程安全线程不安全,本文结合我自己的理解,把我认为的单例写法都说一遍,并解释为什么要这么写。想写好单例,细节点其实很重要。大家先回顾下很重要的基础知识。

1.1 类加载机制

类的加载过程主要分为五步:

加载、连接(验证、准备、解析)、初始化

加载:

  • 通过类的全限定名获取定义该类的二进制字节流。
  • 将字节流所代表的静态存储结构转换为方法区运行的数据结构,也就是将类信息加载到方法区中。
  • 在方法区生成一个Class对象,作为访问这些数据的入口

验证: 主要是进行验证操作,如文件格式验证,字节码验证等等。

准备: 为类变量设置初始值的阶段(是设置初始值,不是设置值,比如static int count = 0)。

解析: 虚拟机将常量池内的符号引用转换为直接引用。

初始化: 其实就是执行static静态块和static变量赋值的过程,谁先定义,就初始化谁。

其实类加载机制的体现就是ClassLoader里面的loadClass,方法里面通过用synchronized关键字去修饰,表明是线程安全的。

1.2 对象的创建

对象的创建过程主要分为以下几步:

  • 类加载检查:如果类还没有被加载,那么会先去执行类加载
  • 分配内存:为对象分配堆内存
    • 指针碰撞:适用于堆内存完整的情况,也就是在堆内存中,已经使用的内存在一边,没有使用的内存在另一边,那么中间的边界线是用一个指针来表示,那么实际上分配内存空间的过程就是指针移动的过程。
    • 空闲列表:使用于堆内存不完整的情况,那么JVM会维护一个空闲列表,记录着哪块内存空间空闲,大小是多少,那么分配对象内存的时候只要遍历这张表,找到合适的空闲列表就可以了。

    并发情况下,一般是使用TLAB,TLAB的意思是为每一个线程预先在Eden区分配一块内存,那么这个线程为对象分配内存的时候,预先使用这块内存,如果分配失败了再使用CAS+失败重试机制。

  • 初始化零值:Java对象在内存中的布局,对实例数据进行初始化,这里的初始化只是初始化零值而已。
  • 设置对象头:Java对象头分为Mark World以及类型指针,其实就是设置对象的GC分代年龄,HashCode等等。
  • 执行init:执行父类成员变量赋值,再到执行父类构造函数,再执行子类成员变量赋值,执行子类构造函数。

2.饿汉式

public class Singleton {
    public static Singleton singleton = new Singleton();

    private Singleton(){

    }
	
	//
	public static int count = 1;
}

这就是典型的单例模式饿汉式,所谓饿汉式就是通过类加载机制加载该类的时候就会生成单例对象。

先简单回顾一下类加载机制,后面会很有用。

回到单例模式饿汉式,这种写法有个问题,也就是我只想用count变量,但是没办法,你类加载过程中的初始化已经把我的单例对象给初始化了,浪费了内存。

3.Double Check

老规矩,先上代码,然后再逐一解释

public class Singleton {
    private static volatile Singleton singleton ;

    private Singleton(){ }

    public static Singleton getInstance(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                //memory = allocate() 1.分配对象内存空间
                //instance(memory)    2.初始化对象
                //instance = memory   3.设置Instance指向的内存地址,此时instance!=null
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

首先为什么要用Synchronized?

当然也可以把Syncrhonized放在外面,当时这样每次执行getInstance方法的线程数只能有一个,效率大打折扣,因为把synchronized放在里面。

特别要注意为什么要使用volatile?

这里使用volatile主要是对象的创建有关,总的来说就是分配对象内存空间、为对象赋值、对象指向分配好的内存空间。 但是这里可能由于编译器为了提高效率,对指令进行了重排序,也就是改成了分配对象内存空间、对象指向分配好的内存空间、为对象赋值,如果执行到第二步的时候,突然有另外一个线程进来,判断了singleTon != null,那么就直接返回了,但是这个线程拿到的是一个不是完全体的对象,里面没有数据。

因此使用了volatile的防止指令重排序的功能。 因此单例对象得要被volatile修饰。

4.静态内部类

上面的饿汉式提前加载浪费内存、Double check看起来很臃肿。因此有了静态内部类的形式。

public class Singleton {

    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton singleton = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.singleton;
    }
}

其中SingletonHolder 只有调用了getInstance方法才会被初始化,因此实现了懒加载的功能。同时也利用了类加载机制实现了现场安全。

5.枚举实现单例

上面的都有一个问题,哪怕都是线程安全的,但是不一定严格意义上的线程安全,因为都可以通过反射来新健一个对象:

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        System.out.println("传统方法获取:" + Singleton.singleton);


        Class<Singleton> singletonClass = Singleton.class;
        Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Singleton instance = declaredConstructor.newInstance();

        System.out.println("反射方法获取:" + instance);

    }

在这里插入图片描述
所以严格意义来说都不是安全的。

下面给出枚举类实现单例的形式:

public enum SingletonTwo {
    INSTANCE;

    public void doSomething(){
        System.out.println("枚举实现单例");
    }
}

枚举类如何做到线程安全?

枚举类的关键词是enum,就跟class关键字对应的Class类,那么enum也对应一个Enum类。

通过Javap -c -v 反编译大概可以得出以下内容:

public final class Singleton extends Enum
{
    //省略部分内容
    public static final Singleton SPRING;
    public static final Singleton SUMMER;
    public static final Singleton AUTUMN;
    public static final Singleton WINTER;
    private static final Singleton ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}

可以发现我们自定义的枚举继承了Enum,并且我们在枚举里面自定义的变量都用public static final来修饰,并且通过static静态块来进行初始化(因为ClassLoader的loadClass方法是用synchronized修饰的,所以是线程安全的)。

并且在我们原本的无参构造方法多了两个参数:name、order,name表示他们的对象名字,order则表示初始化的顺序,里面的变量初始化完成后,会放在一个valuesOf数组中

枚举类如何破坏反射机制

哪怕用私有构造方法,一样都可以通过反射机制来新建一个对象。

但是你尝试获取枚举类的构造函数时,会给抛出method not pattern的异常。
在这里插入图片描述

因为上面也有说了枚举类的真正实现是会为构造函数加两个参数name、order,那尝试用反射获取到对应的构造函数时,确实能够获取。

但是到新建对象时,就给你抛出异常了。

在这里插入图片描述

这就解释清楚了,枚举已经把反射利用构造函数创建对象这条路封死了。

枚举如何破坏序列化和反序列化

从官方的解释中。在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象,也就是那个values数组。

那我可以自己写一个序列化和反序列化。

可惜的是。反序列化也是通过反射机制获取空的构造函数来新建对象,可惜的是枚举已经把构造函数这条路封死了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值
>