单例模式--还没从工厂中逃脱出来?看来是注定单身了..

前言

上次我们聊了聊一个略微重量级的工厂模式,不知道你是否消化完从工厂中逃脱出来了呢?不是我说,今天的单例模式,恰恰好相反了,孤孤单单,看来是注定单身了..

先来看看单例模式在jdk中的应用

在jdk中Runtime用到,饿汉式

image.png

知识点

总共8种方式

1)饿汉式(静态常量)
2)饿汉式(静态代码块)
3)懒汉式(线程不安全)
4)懒汉式(线程安全,同步方法)
5)懒汉式(线程安全,同步代码块)
6)双重检查
7)静态内部类
8)枚举

饿汉式(静态常量)

非常勤快,在对象还没使用到的时候就先创建出来了

1)构造器私有化(防止new)
2)类的内部创建对象
3)向外暴露一个静态的公共方法 getInstance()
4)代码实现

public class Singleton1 {

    private  Singleton1() {
    }

    private final static Singleton1 instance = new Singleton1();

    public static Singleton1 getInstance(){
        return instance;
    }
}

优缺点

1)优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
2)缺点:在类装载的时候就完成实例化,没有达到LazyLoading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
3)这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance就没有达到lazyloading的效果
4)结论:这种单例模式可用,可能造成内存浪费

饿汉式(静态代码块)

优缺点跟上边是一样的,可以说是等效于上边的普通饿汉式

package com.melo.design.单例模式.饿汉式_静态代码块;


public class Singleton2 {

    private Singleton2() {
    }

    private static Singleton2 instance ;
    
    static {
        instance = new Singleton2();
    }

    public static Singleton2 getInstance(){
        return instance;
    }
}

懒汉式

等到要用到了,再把对象创建出来

/**
 * 懒汉式
 *  线程安全
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}

    //在成员位置创建该类的对象
    private static Singleton instance;

    //对外提供静态方法获取该对象
    public static synchronized Singleton getInstance() {

        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

懒汉式(同步代码块)--不能使用

实际上做不到线程同步,因为在if的时候就会出现线程混乱问题
比如if时A进来,B再进来,A先使用class,然后new了,B后续没有再次判断,还是会再次去new

image.png

**懒汉式双检索(双重检查)

刚好解决了上边懒汉式同步代码块的问题,再多一步if判断

  • 注意双检索是说双重检查,而不是说加了双重锁!!!!
package com.melo.design.单例模式.双检索;

public class Singleton {
    
    //注意volatile修饰
    private volatile static Singleton singleton;
    
    private Singleton (){
    }
    
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

**注意: volatile

在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。

  • 那这里那里可能会出现指令重排序呢,既然涉及到多条指令会重排序,那么说明这里的 **new Singleton() **操作并不是原子性的
    1. 先给singleton分配内存空间
    2. 调用构造函数来初始化成员变量singleton
    3. 让singleton引用指向所分配好的内存空间(此时instance就!=null了)
  • 那什么情况下指令重排序后会出现问题呢?
    • 比如说** c - a - b 这种情况下,先走c , 然后此时别的线程调用了 getSingleton ,注意此时他判断 singleton不等于null后,就会直接return instance** 了,然而得到的singleton却是不可用的,因为该对象还没有初始化,状态还处于不可用的状态,这样会导致异常的发生。
  • 所以我们就得加volatile关键字来修饰该变量,便可避免指令重排序

有关volatile相关的知识,目前还在整理,希望可以早点把并发和锁相关的知识一并整理出来

**静态内部类

里边定义一个final静态常量
使用时直接返回静态内部类的final静态常量就好了!

public class Singleton {  
    
    //静态内部类,里边定义一个final静态常量
    private static class SingletonHolder {  
    	private static final Singleton INSTANCE = new Singleton();  
    }  
    
    private Singleton (){}  
    
    public static final Singleton getInstance() {  
    	return SingletonHolder.INSTANCE;  
    }  
}

1)这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
2)静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
3)类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
4)优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
5)结论推荐使用.

**枚举(防反射和反序化)

  • 可以继承也可以实现接口(似乎跟普通的class没有什么区别)
  • 要用直接StuUserService.INSTANCE.方法即可
public enum StuUserService implements StuUserServiceInter {
    /**
     * 该类的唯一实例
     */
    INSTANCE;

    @Override
    public void test1(){
        System.out.println("111");
    }

    public static void main(String[] args) {
        StuUserService.INSTANCE.test1();
    }
}



扩展知识

image.png
首先,枚举类似类,一个枚举可以拥有成员变量,成员方法,构造方法。

每一个枚举量看作是这个类的对象
同时可以设置成员变量,成员方法
然后可以根据成员变量设置相应的构造方法

反射和序列化破坏单例

反射

私有化构造器并不保险。它抵御不了反射的攻击!!!

破坏一下引以为傲的"双检索"

image.png

package com.melo.mydesign.单例模式.双检索;

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

public class Singleton {
    private volatile static Singleton singleton;

    private Singleton (){}

    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    //开始搞事情
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //调用单例方法生成的对象1
        Singleton singleton1 = Singleton.getSingleton();
        //获得所有构造器(包括私有的)
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        //设置可访问
        constructor.setAccessible(true);
        //构造器生成
        Singleton singleton2 = constructor.newInstance();
        //判断是否相等,发现false,已然破坏了单例
        System.out.println(singleton1==singleton2);
    }
}

序列化

同样破坏双检索

public static void main(String[] args) throws Exception {
        Singleton s = Singleton.getInstance();

        byte[] serialize = SerializationUtils.serialize(s);
        Object deserialize = SerializationUtils.deserialize(serialize);

        System.out.println(s);
        System.out.println(deserialize);
        System.out.println(s == deserialize);

    }

image.png

来看看枚举大法

反射一运行就报错

Exception in thread "main" java.lang.NoSuchMethodException: com.fsx.bean.EnumSingleton.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.fsx.maintest.Main.main(Main.java:19)

主要是在源码处有对枚举特判,如果该类是枚举修饰,则抛出异常

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

同样序列化也是可以防范的

package com.melo.mydesign.单例模式.枚举;


import com.melo.mydesign.单例模式.双检索.Singleton;
import org.springframework.util.SerializationUtils;

public enum StuUserService implements StuUserServiceInter {
    /**
     * 该类的唯一实例
     */
    INSTANCE;

    @Override
    public void test1(){
        System.out.println("111");
    }

    public static void main(String[] args) {
//        StuUserService.INSTANCE.test1();

        StuUserService s = StuUserService.INSTANCE;

        byte[] serialize = SerializationUtils.serialize(s);
        Object deserialize = SerializationUtils.deserialize(serialize);

        System.out.println(s);
        System.out.println(deserialize);
        System.out.println(s == deserialize);
    }
}

image.png

总结

  • 一个类有static变量instance,以及私有构造方法,要访问这个类对象的话有两种情况

懒汉式: 用到了再来创建对象(就得加锁,判断是不是null,是null得加锁才来生成,防止多次实例化)
饿汉式(比较勤快):类加载时就先生产好了对象,保证了线程安全,但浪费了内存空间


posted @ 2021-10-26 08:34  Melo~  阅读(664)  评论(0编辑  收藏  举报