确保一个类在任何情况下都绝对只有一个实例,并提供一个全局的访问点,隐藏其所有的构造方法。
比如:皇帝
一、饿汉式单例
在单例首次加载的时候就创建实例
版本一:
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton(); //final 保证不会被其他方式所修改
private HungrySingleton() {
} // 构造方法私有
public HungrySingleton getHungrySingleton() {
return hungrySingleton;
} //提供一个全局访问点
}
版本二:
public class HungryStaticSingleton {
private static final HungryStaticSingleton hungryStaticSingleton; //final
static {
hungryStaticSingleton = new HungryStaticSingleton();
}
private HungryStaticSingleton() {
} //私有的构造函数
public HungryStaticSingleton getHungrySingleton() {
return hungryStaticSingleton;
} //全局的访问点
}
静态字段和静态块都会在类加载的时候被实例化,不管有没有用,都会先被实例化;
缺点:浪费内存空间
二、懒汉式单例
被外部类调用的时候才创建实例
版本一:
public class LazySingleton {
private static LazySingleton lazySingleton = null ;
private LazySingleton(){} // 构造方法私有
public synchronized static LazySingleton getLazySingleton(){ //synchronized 保证线程安全
if(lazySingleton == null) { //保证只创建了一次
lazySingleton = new LazySingleton();}
return lazySingleton;
} //全局访问点
}
存在问题:线程不安全,可能会存在同时有多个线程进入get方法,加了synchronized关键字,可解决此问题,如果有线程进入到该方法,则其他线程会进不去,也就是加上锁了,但是给静态方法加上锁了之后很有可能把整个类都锁上了,会很影响性能问题;
//版本二 双重检查锁
public class LazySingleton {
private volatitle static LazySingleton lazySingleton = null ; //volatitle 解决指令重排序的问题
private LazySingleton(){} // 构造方法私有
public static LazySingleton getLazySingleton(){ //synchronized 保证线程安全
if(lazySingleton == null) {
// 保证所有线程都可以进入,不会因为锁方法而把类锁上,也可以过滤很多,提高性能
synchronized(LazySingleton.class){ //开始锁住
if(lazySingleton == null) { //防止两个线程同城到达if,又出现线程不安全的问题,双重检查锁
lazySingleton = new LazySingleton();}
} }
return lazySingleton;
}
}
volatitle:防止指令重排序
cpu执行时会转成成JVM指令执行,
1、分配内存给这个对象
2、初始化对象
3、将初始化好的对象和内存地址建立关联赋值;
4、用户初次访问
//版本三:静态内部类 - 性能最优的一种方案
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton (){} // 构造方法私有
public static LazyInnerClassSingleton getLazyInnerClassSingleton{
return LazyHolder.lazy;
}
private static class LazyHolder{
private static final LazyInnerClassSingleton lazy = new LazyInnerClassSingleton();
}
}
静态内部类的写法没有用到synchronized关键字,所以性能最优;
利用了静态内部类加载的一个原理,大类加载的时候,首先会加载内部类,等调get的时候,内部类的逻辑就会执行,内部类的对象又是一个final 的大类对象,所以就会只会创建一个对象。利用了内部类的特性,内部类没有初始化就不能调,JVM底层的执行逻辑,完美的避免了线程问题。
扩展1:反射破坏单例
在单例模式的设计过程中,一定要将构造器设计为私有构造器,因为这样可以防止从外部构造对象。但是这样其实并不安全,因为通过反射我们可以获取类中的域、方法、构造器,可以修改他们的访问权限,这样可以通过反射构造对象。如下所示:通过全局访问点得到的和通过反射得到的,并不是同一个对象~
// 反射可能会攻击单例,强制访问私有构造函数
private LazyInnerClassSingleton (){
if(LazyHolder.lazy != null){
throw e;
}
}
对构造函数进行改造,可以有效的防止被反射破坏,看似成功,暗藏bug。
饿汉式单例 --> 在单例首次加载的时候就创建实例
那么,正常的创建单例对象,和反射去强行创建,无论哪个顺序在前,反射创建的时候都会抛异常。
因为就算反射时如果类没有进行初始化,也会先执行类的初始化,然后再执行反射(java规范),
此时单例对象已经存在,反射就会抛出异常。
懒汉式单例 -->被外部类调用的时候才创建实例。
如果反射构造在前,正常的创建单例对象在后,那么反射的异常是抛不出来的,因为这个时候单例对象还不存在,
还是会创建出来两个不同的单例的。
扩展2:被序列化破坏
将一个实例序列化到一个文件中后,再反序列化得到一个实例,这两个实例是不同的,这就违反了单例原则
在反序列化时是通过反射newInstance了一个新的实例,我们可以通过下面方法防止这种序列化与反序列化的破坏:
private Object readResolve(){ return lazySingleton; } // 重写readResolve方法
// 可以重写readResolve方法,让他直接发挥常量对象值,它本身是重新调用构造函数创建一个对象。
private Object readResolve(){
return LazyHolder.lazy;
}
这个方法在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。由于是反射调用,里面的Method对象已经规定了调用时的方法名为readResolve,在这里我们才加这样名称的一个方法。
三、注册式单例
将每个实例都缓存到统一的容器中去,使用唯一标识来获取实例
1、枚举式
public enum EnumSingleton {
INSTANCE; //唯一标识
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
不会被反射破坏:JDK为枚举不被序列化和反射破坏来保驾护航
原因:反射的时候,调用的newInstance方法中对构造的对象的class信息进行了判断,如果构造的是枚举类,则抛出异常,可见我们不能通过反射构造枚举类型的实例,这样就天然的使得枚举类实现的单例模式避免了反射攻击。
不会被序列化破坏:通过类名和枚举值可以确定唯一对象
反序列化的IO类中对枚举类对象的构造是用class类型与名称进行的,同一枚举类类型中名称相同的对象一定是同一个,在内存中只有一份,故反序列化出来的对象与原对象相同。这样就天然抵挡了序列化与反序列化的破坏。
枚举类反编译后,只有一个私有构造器,有一个静态初始化块,在类初始化阶段对枚举类的实例进行初始化,其实也就是饿汉式单例,再加上其在IO类中和反射类中的天然安全检查优势,自然可以防御反射攻击与序列化与反序列化破坏,所以算是单例模式的最佳实践。
2、容器式单例
将每个实例都缓存到统一的容器中去,使用唯一标识来获取实例
public class ContainerSingleton {
private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getBean(String className) {
synchronized (ioc) { //防止线程安全
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance(); //反射
ioc.put(className, obj);
} catch (Exception e) {
}
return obj;
}
return ioc.get(className);
}
}
}
四、总结
单例模式的优点:
在内存中只有一个实例,减少了内存开销;
可以避免对资源的多重占用;
可以设置全局访问点,严格控制访问;
缺点:
没有接口,扩展困难;
扩展单例对象,只能修改代码,没有其他方法,不符合开闭原则的。