单例模式( Singleton Pattern )是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛,例如,总统,班主任等。J2EE标准中的ServletContext 、ServletContextConfig 等、Spring框架应用中的。
特点:构造方法私有,提供一个全局访问点。
实现方式:有很多,1.饿汉式 2.懒汉式 3.注册式 4.ThreadLocal
优点:内存中只有一个实例,减少内存开销;避免对资源多重占用;设置全局访问点,严格控制访问。
缺点:没有接口,扩展困难;如果要扩展单例对象,只有修改代码,没有其他途径,不符合程序的开闭原则。
饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题。
总结:
final:防止反射破坏单例。
饿汉式缺点:可能会造成内存空间的浪费。
饿汉式单例模式适用于单例对象较少的情况。这样写可以保证绝对线程安全、执行效率比较高。但是它的缺点也很明显,就是所有对象类加载的时候就实例化。这样一来,如果系统中有大批量的单例对象存在,那系统初始化是就会导致大量的内存浪费。
通过私有构造器,防止外部进行实例创建;通过属性在类加载时实例化对象,提供全局访问方法取得实例。利用代码的执行先后顺序,在线程还没有出现前就完成了实例化。
public class HungrySingleton {
// 静态实例代码段,饿汉实现类加载初始化时调用构造方法
private static final HungrySingleton hungrySingleton = new HungrySingleton();
// 私有方法防止外部调用创建对象
private HungrySingleton() {}
// 外部类获得单例对象方法
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
该单例实现方式可以被反序列化和反射破坏:
(1)反射破坏方式如下:该方式可以通过构造方法创建出一个全新的实例对象。
public static void reflect() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
System.out.println(Test.getInstance());
//反射破坏
//得到类
Class c = Test.class;
Constructor> constructor = c.getDeclaredConstructor();
//设置私有可调用
constructor.setAccessible(true);
// 打印创建的实例对象
System.out.println(constructor.newInstance());
}
可见该方法是通过调用构造方法创建出一个新的对象。
(2)反序列化破坏单例方式如下:
public static void ser() throws IOException, ClassNotFoundException {
//反序列化
ByteArrayOutputStream outputStream=new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream=new ObjectOutputStream(outputStream);
//将类转化
objectOutputStream.writeObject(Test.getInstance());
System.out.println(Test.getInstance());
ObjectInputStream objectInputStream=new ObjectInputStream(new ByteArrayInputStream(outputStream.toByteArray()));
//读出类,变为一个新的类
Test test= (Test) objectInputStream.readObject();
System.out.println(test);
}
该方式可以看出反序列化构造出的对象并不是通过构造方法。
由此针对上面两种破坏方式做出优化得到以下的代码:
public class Test implements Serializable {
//静态实例代码段,饿汉实现类加载初始化时调用构造方法
private static Test Instance=new Test();
//私有方法防止外部调用创建对象
private Test(){
if(Instance!=null)// 此处方式反射调用破环单例对象,抛出异常
throw new RuntimeException("单例模式不能创建");
System.out.println("构造方法");
}
//外部类获得单例对象方法
public static Test getInstance(){
return Instance;
}
//其他方法
public static void otherMethod(){
System.out.println("other");
}
//防止反序列化破坏单例
public Object readResolve(){
return Instance;
}
}
public class HungryStaticSingleton {
// 静态志方式饿汉式单例
private static final HungryStaticSingleton hungrySingleton ;
static {
hungrySingleton = new HungryStaticSingleton();
}
/**
* 私有构造
*/
private HungryStaticSingleton() {}
//取实例方法
public static HungryStaticSingleton getInstance() {
return hungrySingleton;
}
}
public enum Test_1 {
Instance;
//枚举类默认构造方法私有
Test_1(){
System.out.println("构造方法");
}
//获取对象
public static Test_1 getInstance(){
return Instance;
}
//其他方法
public static void otherMethod(){
System.out.println("other");
}
}
懒汉式类被加载的时候,没有立刻被实例化,第一次调用getInstance的时候,才真正的实例化。
如果要是代码一整场都没有调用getInstance,此时实例化的过程也就被省略掉了,又称“延时加载”
一般认为“懒汉模式” 比 “饿汉模式”效率更高。
懒汉模式有很大的可能是“实例用不到”,此时就节省了实例化的开销。
public class LazySingleton {
private LazySingleton() {
}
private volatile static LazySingleton instance;
//加入了同步代码,解决线程不安全问题
public synchronized static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
这种设计明显的一个问题就是执行效率低,无论是否已经存在实例,在多线程的情况下都会发生阻塞。
对以上代码进行改进,首先让当程序中实例存在的时候,直接返回实例,不需要抢占锁。当程序中不存在实例时,再抢占锁进行创建。根据以上的思想,出现了第二种懒汉式方式:
public class LazyDoubleCheckSingleton {
private LazyDoubleCheckSingleton() {
}
private volatile static LazyDoubleCheckSingleton instance;
public static LazyDoubleCheckSingleton getInstance() {
//确定是否需要阻塞
if (instance == null) {
// 线程安全:双重检查锁(同步代码块)
synchronized (LazyDoubleCheckSingleton.class) {
//确定是否需要创建实例
if (instance == null) {
//这里在多线程的情况下会出现指令重排的问题,所以对共有资源instance使用关键字volatile修饰
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
}
对于第二种方式,较第一种方式而言,性能提高了,但是代码的可读性差了。
DCL(Double Check Lock双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排。
原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。
instance = new LazyDoubleCheckSingleton();可以分为以下3步完成(伪代码)
memory = allocate(); // 1.分配对象内存空间
instance(memory); // 2.初始化对象
instance=memory; // 3.设置instance指向刚分配的内存地址,此时instance != null
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate(); // 1.分配对象内存空间
instance=memory; // 3.设置instance指向刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
instance(memory); // 2.初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
/**
* 使用静态内部类,性能最优
*/
public class LazyInnerClassSingleton {
//虽然构造方法私有了,但是逃不过反射的法眼
private LazyInnerClassSingleton(){};
// 懒汉式单例
// LazyHoler里面的逻辑需等外部方法调用时候才执行
// 巧妙运用了内部类的特性
// JVM底层逻辑,完美避免了线程安全问题
public static final LazyInnerClassSingleton getInstance(){
return LazyHoler.LAZY;
}
public static class LazyHoler{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
为防止调用者反射破坏,可以这么写:
public class LazyInnerClassSingleton {
//虽然构造方法私有了,但是逃不过反射的法眼
private LazyInnerClassSingleton(){
// 防止调用者反射攻击;
if(LazyHoler.LAZY != null){
throw new RuntimeException("禁止创建多个实例!"); // 其他写法也可加上
}
};
// 懒汉式单例
// LazyHoler里面的逻辑需等外部方法调用时候才执行
// 巧妙运用了内部类的特性
// JVM底层逻辑,完美避免了线程安全问题
public static final LazyInnerClassSingleton getInstance(){
return LazyHoler.LAZY;
}
public static class LazyHoler{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
分析:静态内部类相对来说更优,LazyHoler里面的逻辑需等外部方法调用时候才执行,所以也属于懒汉式,巧妙运用了内部类的特性,JVM底层逻辑,完美避免了线程安全问题,