单例模式(懒汉式、静态内部类、饿汉式)

单例模式

定义:保证一个类仅有一个实例,并提供一个全局访问点。
类型:创建型。
适用场景:想确保在任何情况下都只有一个实例(程序计数器、数据库连接池、线程池)。
优点:

  1. 在内存里只有一个实例,降低了内存开销;
  2. 可以避免对资源的多重占用;
  3. 设置全局访问点,严格访问控制;

缺点:没有接口,扩展困难。
重点:

  1. 私有构造器,禁止从外部创建对象;
  2. 线程安全;
  3. 延迟加载,使用时再创建;
  4. 序列化和反序列化的安全问题;
  5. 防止反射;

1、懒汉式

public class LazySingleton {

    private static LazySingleton lazySingleton = null;

    private LazySingleton(){}
    public static LazySingleton getInstance(){
        if (lazySingleton==null){  // 线程不安全
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}  

懒汉式改进方案一:
  加synchronized关键字,实现同步锁,比较消耗资源。在静态方法上添加synchronized关键字锁的是类的class文件,如果加锁的不是静态方法,则锁的是堆内存中新生成的对象。

public synchronized static LazySingleton getInstance(){
        if (lazySingleton==null){  
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }  

懒汉式改进方案二:
  使用双重校验(Double-Check)加volatile关键字,既保证了性能,也兼顾了线程安全。需要注意的是对象创建过程中指令重排序的问题,Java语言规范中规定线程执行Java程序过程中必须遵守"intra-thread semantics"规定,保证重排序不会改变单线程内的程序执行结果,重排序可以提高程序的执行性能。volatile关键字可以使所有线程可以知道共享内存的最新状态,保证了内存的可见性。volatile关键字修饰的共享变量在进行写操作时,会多出一些汇编代码,这些汇编代码主要有两个作用:1、首先将当前处理器中缓存行的数据写回到系统内存,会使其他cpu中缓存了该内存地址的数据无效,于是这些cpu又从共享内存中同步数据,这样就保证了内存的可见性,这里面主要使用的是缓存一致性协议。
  对象创建的步骤:1、分配对象的内存空间;2、初始化对象;3、设置instance指向内存空间;但是由于指令重排序现象的存在,2和3的执行顺序有可能颠倒,导致双重校验失败。

public class LazyDoubleCheckSingleton {

    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

    private LazyDoubleCheckSingleton(){}

    public static LazyDoubleCheckSingleton getInstance(){
        if (lazyDoubleCheckSingleton==null){
            synchronized (LazyDoubleCheckSingleton.class){
                if (lazyDoubleCheckSingleton==null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}  

2、静态内部类

静态内部类的优点:
  外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化instance,故而不占内存。即当StaticInnerClassSingleton第一次被加载时,并不需要去加载InnerClass,只有当getInstance()方法第一次被调用时,才会去初始化instance,第一次调用getInstance()方法会导致虚拟机加载InnerClass类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

静态内部类的缺点:
  静态内部类有一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数。

public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }

    private StaticInnerClassSingleton(){}
}

Java虚拟机在有且仅有的5中场景下会对类进行初始化:

  1. 遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new或者实例化一个对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时;
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化;
  3. 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化;
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类;
  5. 当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;

静态内部类如何保证线程安全:
  虚拟机会保证一个类的clinit方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit方法,其他线程都需要阻塞等待,直到活动线程执行clinit方法完毕。如果在一个类的clinit方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行clinit方法后,其他线程唤醒之后不会再次进入clinit方法。同一个加载器下,一个类型只会初始化一次。)在实际应用中,这种阻塞往往是很隐蔽的。

3、饿汉式

  饿汉式在类加载时就完成对象的初始化,不能像懒汉式那样延迟加载,在一定程度上会造成资源的浪费,但是最简单。final关键字修饰的变量必须在类加载结束前完成初始化,类加载时会执行静态代码块,可用于配置文件的初始化。

public class HungrySingleton {
    private static final HungrySingleton hungrySingleton;
    static {
        hungrySingleton = new HungrySingleton();
    }
    
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}
发布了11 篇原创文章 · 获赞 1 · 访问量 263

猜你喜欢

转载自blog.csdn.net/Introncheng/article/details/103078588