单例设计模式的五种实现

目录

饿汉式

枚举实现饿汉式单例

懒汉式

双检索懒汉式

静态内部类实现懒汉单例

单例在jdk的体现

Runtime类

system中的Console对象

collections中的REVERSE_ORDER

Comparators中的枚举单例


饿汉式

/**
 * 懒汉式  --->  只要类一加载就会创建实例
 */
public class Singleton1 {

    //1.单例的构造必须私有
    private Singleton1(){
        System.out.println("private Singleton1");
    }
    //2.这个实例变量要用static和final修饰   【给静态变量进行赋值】是线程安全的,【静态代码块】的执行由JVM保证它的线程安全
    private static final Singleton1 INSTANCE = new Singleton1();

    //3.【静态变量】一般都是私有的,私有的外界是不能直接访问,所以一般会提供一个公共的静态方法给外界来访问
    public static Singleton1 getInstance(){
        return INSTANCE;
    }
    
}

注意:

  • 如果饿汉式的单例类实现了Serializable接口,那么这个单例是可以被反序列化破坏的。

  • //重写readResolve,在反序列化的时候发现你重写了readResolve方法,那么就会把readResolve方法中的返回值作为反序列化的返回结果
        public Object readResolve(){
            return INSTANCE;
        }
  • 私有的构造方法还可以被人用反射破坏 ---> 这个可以在构造器中加入代码判断来进行避免

private Singleton1(){
        if (INSTANCE != null){   //用来避免反射破坏单例,因为反射是通过再次调用这个构造器来创建新的对象实例的,所以我们可以提前做好判断,如果实例已经存在,那么构造方法就不能再被调用了(因为我们的实例是用final和【static】修饰的,所以在类初始化的时候这个实例就会完成赋值,后面你再通过反射来调构造器就抛异常不让你调用了)
            throw new RuntimeException("单例对象不能重复创建");
        }
        System.out.println("private Singleton1");
}

  • 使用Unsafe类破坏单例

    注意:使用这种方式破坏单例,我们预防不了。

枚举实现饿汉式单例

枚举类是一种语法糖的写法,是编译器在帮我们实现这个单例。

枚举类中声明的变量在编译的时候编译器会帮我们把它变成static,final修饰的变量

/**
 * 枚举实现的单例是饿汉式的
 */
public enum Singleton2 {
  INSTANCE;
  
  public static Singleton2 getInstance(){
   return INSTANCE;
  }
  
}

好处:

  • 可以防止反序列化来破坏单例(因为反序列化的时候ObjectInputStream 会对枚举做特殊的处理,帮我们直接把枚举中的单例直接返回).

  • 可以避免反射来破坏单例。(使用反射破坏枚举会抛异常)

  • 但是不能防止unsafe来破坏这个单例。

懒汉式

/**
 * 懒汉式创建单例
 */
public class Singleton3 {
    private Singleton3(){
        
    }

    private static Singleton3 INSTANCE = null;  //没有final修饰,多线程会不安全

 //该方法可以在并发环境中被多个线程调,用为了保证在并发环境中保证单例的安全,所以在该方法加synchronized,
 //但是直接在方法上加锁,会导致线程每次来都要阻塞
    public static synchronized Singleton3 getInstance(){ 
        if (INSTANCE==null){
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }
}

双检索懒汉式

这是对懒汉式的优化。

public class Singleton4 {
    private Singleton4(){

    }

    private static volatile Singleton4 INSTANCE = null; //为了保证有序性,因为在执行INSTANCE = new Singleton4();这行代码的时候其对应的字节码指令一共有4步(new,dup,invokespecial,putstatic),而new,dup这两步是有因果关系的所以这两步的顺序是确定的,但是invokespecial,putstatic都是赋值操作并且没有因果关系,所以这两步在CPU看来先执行谁都一样,所以就会导致指令重排,在单线程情况下这两步发生顺序调换是不会产生线程安全的,但是在多线程的情况下就会出现:获取到的实例是未完全初始化的实例(未经过构造器初始化赋值的实例)。

    public static  Singleton4 getInstance(){
        if (INSTANCE==null){//加锁之前先判断一次,可以尽可能的避免线程进入synchronized代码块
            synchronized (Singleton4.class){
                if (INSTANCE==null){//在最开始的时候实例还没有创建好,但是【有多个线程同时进入了第一个if】,那么进入这个synchronized代码块的线程肯定有先后了,先进来的已经创建了实例,那么后进来这个synchronized的当然不需要再创建实例了
                    INSTANCE = new Singleton4();
                }
            }
        }
        return INSTANCE;
    }
}

静态内部类实现懒汉单例

我们可以发现饿汉式创建单例是不用考虑线程安全的,我们把饿汉式和懒汉式进行对比,发现主要的原因是实例对象的赋值是不是在静态代码块中完成,饿汉式完成对象的赋值操作是在静态代码块中完成的,静态代码块的线程安全是由JVM帮我们保证的。 所以我们只要保证实例的赋值是在静态代码块中完成,就可以保证线程安全。所以静态内部类创建单例就出现了。

注意:

  • 静态内部类是可以访问外部类的所有变量的。

  • 类只有在第一次使用它,它才会加载。所以我们可以利用这个特性间接的控制静态内部类的加载时机。

public class Singleton5 {

    private Singleton5(){}
    
    public static Singleton5 getInstance(){
        return Holder.INSTANCE;
    }
    
    //创建静态内部类,只有在调用getInstance的时候才会加载这个内部类
    public static class Holder{ 
        static Singleton5 INSTANCE = new Singleton5();  //使用静态代码来保证线程安全
    }
    
}

单例在jdk的体现

注意:自己在项目中不要随便使用单例模式,是非常容易出错误的。如果面试官问你在哪里用到单例模式,最好不要说在项目中使用到了单例! 说jdk中的库就行。

Runtime类

Runtime类就是使用的单例,其实我们在很多地方都用过,比如使用system的exit方法的时候,使用垃圾回收的gc方法的时候,调用的也是这个Runtime。

system中的Console对象

system中的Console对象: 懒汉式的双检索单例。

collections中的REVERSE_ORDER

collections类中的reverseOrder方法(反序比较方法)中的 REVERSE_ORDER 也是单例的:

Comparators中的枚举单例

在Comparators中的使用到了枚举实现单例。

猜你喜欢

转载自blog.csdn.net/weixin_53142722/article/details/125668081