java设计模式- 单例模式

138 阅读7分钟

「这是我参与2022首次更文挑战的第30天,活动详情查看:2022首次更文挑战

什么是单例模式

保证一个类只有一个实例,并且提供一个全局访问点

应用场景

重量级的对象,不需要多个实例,如线程池,数据库连接池

image.png

懒汉模式:

单线程实现

延迟加载, 只有在真正使用的时候,才开始实例化。根据定义以及类图我们大概可以先写出如下代码

package com.jony.designpattern.singleton;

public class LazySingletonTest {
    public static void main(String[] args) {
        Lazysingleton instance1=Lazysingleton.getInstance();
        Lazysingleton instance2=Lazysingleton.getInstance();
        System.out.println(instance1==instance2);
    }
}

class Lazysingleton{
    private static Lazysingleton instance;
    //创建一个私有构造,防止从外部被new
    private Lazysingleton(){};
    //在创建一个共有的方法,为外部提供调用
    public static Lazysingleton getInstance() {
        //如果实例是null,则创建一个
        if(instance==null){
            instance=new Lazysingleton();
        }
        return instance;
    }
}

执行结果如下:

image.png 这就创建了一个懒汉模式,但是当线程的时候,进行并发访问的时候,可能多个线程同时进行判断 if(instance==null) 这样就创建了多个实例。

package com.jony.designpattern.singleton;

public class LazySingletonTest {
    public static void main(String[] args) {
        new Thread(()->{
            Lazysingleton instance=Lazysingleton.getInstance();
            System.out.println(instance);
        }).start();
        new Thread(()->{
            Lazysingleton instance=Lazysingleton.getInstance();
            System.out.println(instance);
        }).start();
        new Thread(()->{
            Lazysingleton instance=Lazysingleton.getInstance();
            System.out.println(instance);
        }).start();
    }
}

class Lazysingleton{
    private static Lazysingleton instance;
    //创建一个私有构造,防止从外部被new
    private Lazysingleton(){};
    //在创建一个共有的方法,为外部提供调用
    public static Lazysingleton getInstance() {
        //如果实例是null,则创建一个
        if(instance==null){
            instance=new Lazysingleton();
        }
        return instance;
    }
}

执行结果:

image.png

可以看到执行的结果会创建多个实例,因此我们需要给代码进行一些一下改造,能够在多线程的情况下也可以完美实现。

多线程实现

通过给方法添加synchronized 添加锁,这样就可以避免多线程下创建多个实例了 image.png

以上代码我们再次分析,这样有N个并发线程进来,不论是否已经创建了实例,所有线程都需要等待锁,这样对我们的性能是有很大损耗的,因此我们需要再次对代码进行改造,只有未创建实例的情况再进行加锁,否则直接获取实例。

class Lazysingleton{
    private static Lazysingleton instance;
    //创建一个私有构造,防止从外部被new
    private Lazysingleton(){};
    //在创建一个共有的方法,为外部提供调用
    public  static Lazysingleton getInstance() {
        //如果实例是null,则创建一个
        if(instance==null){
            synchronized (Lazysingleton.class){
                instance=new Lazysingleton();
            }
        }
        return instance;
    }
}

这样我们就可以提供程序的性能了,只有instance==null的情况下才进行加锁(虽然也有可能多个线程等待锁,但是也比之前性能提高很多),只要不等于null,则直接返回实例。

编译器(JIT),CPU 有可能对指令进行重排序,导致使用到尚未初始化的实例,可以通过添加volatile 关键字进行修饰

jvm加载字节码文件简单分析

到上步看似没问题了,但是实际上以上代码还是会有问题,我们写的java代码在编译后形成.class字节码文件,下面我们编写一个段简单代码,看一下字节码文件
1、首先创建java代码

package com.jony.designpattern.singleton;

public class LazyTest {
    public static void main(String[] args) {
        LazyTest t=new LazyTest();
    }
}

2、编译java代码

image.png 3、查看字节码文件

image.png 4、字节码文件查看

image.png

5、主要分析main函数 image.png 5-1、可以看到构造函数第一行为new 执行#2 然后#2又指向#21,然后我们可以得知,jvm帮我们在堆区以utf8编码创建了一个LazTest内地地址,同时在栈空间创建一个引用这个内存地址的引用。

5-2、dup实际就是将栈空间的对象进行复制

5-3、invokespecial ,将第一步在一开始在栈空间创建的对象删除

5-4、将堆空间的引用赋值到我们实际的LazyTest t上面。

因此最终我们jvm加载字节码文件主要是如下几步
1、分配内存空间
2、初始化
3、引用赋值

最终编译器(JIT),CPU 有可能对指令进行重排序,导致以上三个步骤不是按照1-2-3进行执行,就可能导致,我们的对象指向了一个不存在的地址,造成空指针异常,我们就可以通过添volatile关键字进行修饰,防止执行指令重排。

最终形成如下代码:

class Lazysingleton{
    private volatile static Lazysingleton instance;
    //创建一个私有构造,防止从外部被new
    private Lazysingleton(){};
    //在创建一个共有的方法,为外部提供调用
    public  static Lazysingleton getInstance() {
        //如果实例是null,则创建一个
        if(instance==null){
            synchronized (Lazysingleton.class){
                instance=new Lazysingleton();
            }
        }
        return instance;
    }
}

饿汉模式:

类加载的 初始化阶段就完成了 实例的初始化 。本质上就是借助于jvm类加载机制,保证实例的唯一性(初始化过程只会执行一次)及线程安全(JVM以同步的形式来完成类加载的整个过程)。

实现代码

package com.jony.designpattern.singleton;

public class HungrySingletonTest {
    public static void main(String[] args) {
        HungrySingleton instance=HungrySingleton.getInstance();
        HungrySingleton instance1=HungrySingleton.getInstance();
        System.out.println(instance==instance1);
    }
}

class HungrySingleton{
    //类加载额时候就创建对象
    private static HungrySingleton instance=new HungrySingleton();
    //私有构造,不让外部类new
    private HungrySingleton(){};
    public static HungrySingleton getInstance(){
        return instance;
    }
}

实现原理

饿汉模式,不会发生懒汉模式的相关问题,这归功于类加载机制,来保证单例实例。

类加载过程

1,加载二进制数据到内存中, 生成对应的Class数据结构,

2,连接: a. 验证, b.准备(给类的静态成员变量赋默认值),c.解析

3,初始化: 给类的静态变量赋初值

只有在真正使用对应的类时,才会触发初始化 如(当前类是启动类即main函数所在类,直接进行new 操作,访问静态属性、访问静态方法,用反射访问类,初始化一个类的子类等.)

静态内部类:

1).本质上是利用类的加载机制来保证线程安全

2).只有在实际使用的时候,才会触发类的初始化,所以也是懒加载的一种形式,因为我们在不使用内部类的时候,jvm是不会加载的,只有在调用的时候才会进行初始化。

实现代码

class InnerClassSingleton{
    //创建静态内部类
    private static class InnerClassHolder{
        private static InnerClassSingleton instance=new InnerClassSingleton();
    }
    //私有化构造函数
    private InnerClassSingleton(){};
    public static InnerClassSingleton getInstance(){
        return InnerClassHolder.instance;
    }
}

反射是如何攻击单例的

通过以下代码,我们通过反射机制就可以获得一个实例对象,这样和正常获得的实例就不一样了。

反射创建实例,上面三种创建的单例模式就都不安全,不能保证仅创建一个实例了

package com.jony.designpattern.singleton;

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

public class InnerClassSingletonTest {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        //通过反射获取对象
        Constructor<InnerClassSingleton> declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor();
        //设置可读
        declaredConstructor.setAccessible(true);
        //获得实例
        InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance();

        //使用new获得实
        InnerClassSingleton instance=InnerClassSingleton.getInstance();
        System.out.println(innerClassSingleton==instance);

    }
}

class InnerClassSingleton{
    //创建静态内部类
    private static class InnerClassHolder{
        private static InnerClassSingleton instance=new InnerClassSingleton();
    }
    //私有化构造函数
    private InnerClassSingleton(){};
    public static InnerClassSingleton getInstance(){
        return InnerClassHolder.instance;
    }
}

对饿汉模式和静态内部类,防护反射创建

我们在私有构造方法里面,对实例进行一些判断,如果实例已经存在则抛出异常。

class InnerClassSingleton{
    //创建静态内部类
    private static class InnerClassHolder{
        private static InnerClassSingleton instance=new InnerClassSingleton();
    }
    //私有化构造函数
    private InnerClassSingleton(){
        if(InnerClassHolder.instance!=null){
            throw new RuntimeException("不允许多个实例");
        }
    }
    public static InnerClassSingleton getInstance(){
        return InnerClassHolder.instance;
    }
}

深入反射创建实例

我们进入newInstance方法 image.png

具体方法

image.png 可以看到如果实例是ENUM的时候,是不允许创建实例的,因此ENUM是不是也可以实现一个安全的单例呢?下面我们来测试一下

枚举类型的单例

image.png 通过代码执行结果,我们得知枚举其实也是单例模式的,那么枚举实现单例的原理是怎么样的呢?

1)天然不支持反射创建对应的实例,且有自己的反序列化机制

2)利用类加载机制保证线程安全

单例序列化

首先我们先进入Serializable这个类

image.png 可以看到如下说明

image.png

因此我们可以利用 指定方法来替换从反序列化流中的数据 如下

ANY‐ACCESS‐MODIFIER Object readResolve() throws ObjectStreamException;

下面我们来通过内部类单例来进行序列化

package com.jony.designpattern.singleton;

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class InnerClassSingletonTest {
    public static void main(String[] args) {

    }
}

class InnerClassSingleton implements Serializable {
    static final long serialVersionUID = 42L;
    //创建静态内部类
    private static class InnerClassHolder {
        private static InnerClassSingleton instance = new InnerClassSingleton();
    }

    //私有化构造函数
    private InnerClassSingleton() {
        if (InnerClassHolder.instance != null) {
            throw new RuntimeException("不允许多个实例");
        }
    }

    public static InnerClassSingleton getInstance() {
        return InnerClassHolder.instance;
    }

    Object readResolve() throws ObjectStreamException {
        return InnerClassHolder.instance;
    }
}