单例模式

1.1模式定义

保证一个类只有一个实例,并且提供一个全局访问点。最重要的就是保证构造器私有。

1.2实现方式

1.2.1懒汉模式

public class LazySingleton {
    private static LazySingleton instance;
    //构造器私有
    private LazySingleton(){
    }

      //静态方法返回对象
    public static  LazySingleton getInstance() {
        if (instance == null){   
            instance = new LazySingleton();
        }
        return instance;
    }
}

存在一个问题,如果是多线程环境下,会有可能出现实例化两个对象。因此可以通过加锁的方式解决.

public class LazySingleton {
    private static LazySingleton instance;
    //构造器私有
    private LazySingleton(){
    }
      //加synchronized锁
    public static synchronized LazySingleton getInstance() {
        if (instance == null){
            instance = new LazySingleton();
        }
        return instance;
    }
}

加锁解决了多线程条件下实例化多个对象的问题,但是牺牲了效率。可以进行优化。

public class LazySingleton {
    private static LazySingleton instance;
    private LazySingleton(){
    }
	//双重检测锁模式
      //1、检查变量是否被初始化(不去获得锁),如果已被初始化则立即返回。2、获取锁。3、再次检查变量是否已经被初始化,如果还没被初始化就初始化一个对象
    public static  LazySingleton getInstance() {
        if (instance == null){
            synchronized (LazySingleton.class) {
               if(instance == null){
                   instance = new LazySingleton();//java中new对象不是原子性操作
                 
               }
            }
        }
        return instance;
    }
}

但是这种模式下还存在问题,java的new操作并不是一个原子操作,大致分三步:1.在堆中开辟对象所需空间,分配内存地址、2.根据类加载的初始化顺序进行初始化、3.将内存地址返回给栈中的引用变量。如果发生指令重排,将后两步顺序颠倒,在多线程环境中就会发生使用未初始化的对象的问题。因此需要加volatile关键字,禁止指令重排。

1.2.2饿汉模式

本质上借助jvm类加载机制,保证实例的唯一性。但是比较浪费内存资源,因为一开始,无论是否需要都会加载。

//类加载过程
//1.加载二进制数据到内存中,生成对应的二进制的Class数据结构
//2.连接:a、验证 b、准备(给静态成员变量赋默认值)c、解析
//3.初始化:给静态变量赋初值
public class HungrySingleton {
    private static HungrySingleton instance = new HungrySingleton();
    //构造器私有
    private HungrySingleton(){
    }
    public  static HungrySingleton getInstance(){
        return instance;
    }
}

关于饿汉模式中是否需要加final需要看情况而定,声明final的变量,必须在类加载完成时已经赋值。存在释放资源的情况下,如果需要重新使用这个单例,就必须存在重新初始化的过程,就不能加final,对于不需要释放资源的情况,可以加final。final static修饰的成员变量必须直接赋值或者在静态代码块中赋值。

1.2.3静态类部类

//结合了懒汉模式和饿汉模式的优点:不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
public class InnerClassSingleton {
    //构造器私有
     private InnerClassSingleton(){
    }
    private static class InnerClassHolder{
        private static InnerClassSingleton instance = new InnerClassSingleton();
    }
   
    public static InnerClassSingleton getInstance(){
        return InnerClassHolder.instance;
    }
}

首先,我们getInstance()函数并不直接new创建对象,而是取的是InnerClassHolder里的instance对象。所以无论多少线程一起调用getInstance函数都只会返回同一个单例。当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建。那在这里又是怎么保证单例的呢?主要是虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步。只会有一个线程去执行类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。(而且唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次)。但是无法传递外部参数。
通过反射方式可以破坏单例模式,通过反射获得单例类的构造函数,由于该构造函数是private的,通过setAccessible(true)指示反射的对象在使用时应该取消 Java 语言访问检查,使得私有的构造函数能够被访问,这样使得单例模式失效。可以对实例化次数进行统计,当大于一次时就就抛出异常,(当然也不安全,可以通过反射修改这个值)。然后对构造函数加上synchronized关键字防止多线程情况下实例出多个对象。clone()不会破坏单例模式,虽然clone()方法,是直接从内存区copy一个对象,但是单例的类不能实现cloneable接口,普通对象直接调用clone()会抛出异常。

1.2.4枚举

  1. 天然不支持反射创建对应实例
    单例类的修饰是abstract的,所以没法实例化。
  2. 自己的反序列化机制
    枚举序列化的时候只将名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。
  3. 利用类加载机制保证线程安全
    被声明为static,类加载时只会初始化一次.
posted @ 2020-07-10 13:12  大嘤熊  阅读(104)  评论(0编辑  收藏  举报