单例模式
定义:保证一个类仅有一个实例,并提供一个全局访问点。
类型:创建型。
适用场景:想确保在任何情况下都只有一个实例(程序计数器、数据库连接池、线程池)。
优点:
- 在内存里只有一个实例,降低了内存开销;
- 可以避免对资源的多重占用;
- 设置全局访问点,严格访问控制;
缺点:没有接口,扩展困难。
重点:
- 私有构造器,禁止从外部创建对象;
- 线程安全;
- 延迟加载,使用时再创建;
- 序列化和反序列化的安全问题;
- 防止反射;
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中场景下会对类进行初始化:
- 遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new或者实例化一个对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时;
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化;
- 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化;
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类;
- 当使用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;
}
}