单例模式你真的掌握了吗?
单例模式:
确保某个类只有一个实例对象。
单例模式的三要素
- 定义私有的静态成员
- 构造函数私有化
- 提供一个公有的静态方法以构造实例
单例模式第一版(饿汉模式)
package demo01;
//饿汉模式
public class Singleton00 {
private Singleton00(){
};
private static Singleton00 instance = new Singleton00();
public static Singleton00 getInstance(){
return instance;
}
}
饿汉式 :在单例类被加载时,就会实例化一个对象。
优点: 基于classloader机制,避免了多线程的同步问题。
缺点:不能延迟加载,内存效率低,容易产生垃圾对象。
单例模式第二版(懒汉式)
package demo01;
//单例模式第一版
public class Singleton01 {
private Singleton01(){}; //私有的构造函数
//instance 是单例对象,也是类静态成员。
private static Singleton01 instance = null;
//静态工厂方法
public static Singleton01 getInstance(){
if(instance==null){
instance = new Singleton01();
}
return instance;
}
}
懒汉模式:调用实例的方法时才会实例化对象。
优点:实现了懒加载,节约了空间。
缺点:在不加锁的情况,线程不安全,可能出现多份实例。
在加锁的情况下,程序串行化,使得系统有严重的性能问题。
上面的代码是线程安全的吗?
假设Singleton类刚刚被初始化,instance对象还是空,这时候两个线程同时访问getInstance方法:
因为instance是null,所以线程A,B都通过了条件判断,开始进行new操作,这样显然instance被构建了2次。
单例模式第二版(加锁版)
package demo01;
//单例模式第二版
public class Singleton02 {
private Singleton02(){}; //私有的构造函数
private static Singleton02 instance = null; //单例对象,
public static Singleton02 getInstance(){
if(instance==null){ //双重检测机制
synchronized (Singleton02.class){ //同步锁
if(instance==null){ //双重检测机制
instance=new Singleton02();
}
}
}
return instance;
}
}
关键点
- 为了防止多线程访问时出现执行多次new Singleton02的问题,因此在new操作之前加上Sychronized同步锁,锁住整个类(这里不能是对象锁)。
- 进入Synchronized 临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,假如线程A先获取得到类锁,执行代码,当构建完对象instances时(instance不为空啦),线程B也已经通过了最初的判空验证,不做第二次判空的话,线程B还是会再次构建instance对象。
然而这样就一定安全了吗?Really?
在此先复习一个JVM知识点:jvm编译器的指令重排。
比如说 instance = new Singleton02();会被编译器编译成如下编译指令:
Memory = allocate(); (1)分配对象的内存空间
ctorlnstance(memory); (2)初始化对象
instance = memory; (3) instance指向刚分配的内存地址
但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,重排:
Memory = allocate(); (1)分配对象的内存空间
instance = memory; (3) instance指向刚分配的内存地址
ctorlnstance(memory); (2)初始化对象
所以,当线程A执行完1,3时,instance对象还没完成初始化,但已经不在指向null。此时如果线程B获得时间片,执行if(instance==null)的结果会是false,从而返回一个没有初始化完成的instance对象。
so,聪明的你知道该如何解决吗?
可以在instance 对象前增加一个volatile。volatile修饰符阻止了变量访问前后的指令重排,保证了指令的执行顺序。
单例模式第三版(静态内部类版)
package demo01;
//静态内部类
public class Singleton03 {
private static class LaxyHolder{
private static final Singleton03 instance = new Singleton03();
}
private Singleton03(){};
public static Singleton03 getInstance(){
return LaxyHolder.instance;
}
}
关键点
- 从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象instance。
- instance 对象始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。
静态内部类就无懈可击了吗?你太天真了!
一个"老大难"的问题:无法防止利用反射来重复构建对象。
那么反射是如何破坏单例的呢?
import java.lang.reflect.InvocationTargetException;
//静态内部类
public class Singleton03 {
private static class LaxyHolder{
private static final Singleton03 instance = new Singleton03();
}
private Singleton03(){};
public static Singleton03 getInstance(){
return LaxyHolder.instance;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
/**
* 利用反射破坏单例模式
*/
//获得构造器
Constructor con = Singleton03.class.getDeclaredConstructor();
//设置为可以访问
con.setAccessible(true);
//构造2个不同的对象
Singleton03 singleton1 = (Singleton03)con.newInstance();
Singleton03 singleton2 = (Singleton03)con.newInstance();
System.out.println(singleton1.equals(singleton2));
}
}
显然结果返回了false。
那么如何阻止反射的构建方式?
用枚举实现单例模式
package demo01;
public enum SingletonEnum {
instance;
}
关键点
有了enum语法糖,jvm会阻止反射获取枚举的私有构造方法。**
唯一的缺点
就是没有使用懒加载,其单例对象是在枚举类被加载的时候进行初始化的。