之前详细总写过(单例模式)的己种写法,今天对这几种方法进行更深层次的总结
文章目录
单例模式(Singleton Pattern)
是指一个类确保在任何情况下都只有一个实例,并提供一个全局访问点.
属于创建型模式
单例模式重点总结
1. 私有化构造器
指无法从自身之外的类来创建对象
2. 保证线程安全和延迟加载
饿汉模式: 在类初始化的时候就创建出类的实例(线程安全
),但是同时由于对象不是即用既得,会带来内存上的开销
懒汉模式: 懒汉式避免了内存上的开销,但是同时带来了多线程的并发创建的问题,正所谓**鱼和熊掌不可兼得**
- 再通过静态工厂方法获取对象的时候要保证对象不能被重复创建,可以
对静态工厂方法加Synchronized锁
- 为了避免锁的粒度太大,造成性能损耗.一般采用Double Check Lock[双重检查锁]的方式
public static lazyDoubleCheckSingleton getInstance() {
if (lazy == null) {
synchronized (lazyDoubleCheckSingleton.class) {
if (lazy == null) {
lazy = new lazyDoubleCheckSingleton();
}
}
}
return lazy;
}
上面的Synchronized总归是要带来性能损耗的, 有没有更好的方案呢?
我既要懒加载,又要没有性能损耗**我全都要**
- 利用静态内部类实现单例
public class LazyInnerClassSingleton {
//虽然构造方法私有,但是,逃不过反射
private LazyInnerClassSingleton() {
if(LazyHolder.LAZY != null){
throw new RuntimeException("不允许构建多个实例"); // 可以防止通过反射来创建实例,破坏单例
}
}
//LzayHolder 里面的需要等到外部方法调用才执行
// 巧妙的利用了内部类的特性
// JVM底层执行逻辑,完美避免了线程安全问题
public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY;
}
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
为什么主类中的静态块执行,而静态内部类中的静态方法不执行呢?
因为一旦程序运行,所有涉及的类[import导入的类和内部类]都会被加载到内存中,在整个程序的运行过程中类加载只会发生一次,如果这个类没有被加载到内存之中,那么之后就不会使用这个类.
只有类初始化之后,才会调用它的静态代码块
,在这里我们只有对类进行引用的时候,才会进行初始化
.
这里就是利用了内部类加载的特性,完美避免了线程安全问题
3. 防止反射破坏单例
public class LazyInnerClassSingletonTest {
public static void main(String[] args) {
Class<?> clazz = LazyInnerClassSingleton.class;
try {
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Object o1 = c.newInstance(); //强制创建对象
Object o2 = LazyInnerClassSingleton.getInstance();
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出的结果是false
,
由于调用者开了上帝模式有了金手指,通过反射创建对象,但是我们可以给他搞个障眼法,让他不能简单的就能创建成功.
在构造方法加个限制,创建多个对象直接跑出异常
private LazyInnerClassSingleton() {
if(LazyHolder.LAZY != null){
throw new RuntimeException("不允许构建多个实例"); // 可以防止通过反射来创建实例,破坏单例
}
}
4. 防止序列化破坏单例
当我们将一个对象创建好,有时候需要将对象序列化之后存入磁盘,下次使用时候再从磁盘中读取对象,反序列化为内存对象.
反序列化后的对象会重新分配内存.如果序列化的对象为单例对象,就违背了单例对象的初衷,相当于破坏了单例.
public class SeriableSingleton implements Serializable {
//序列化就是说把内存中的状态通过转换成字节码的形式
//从而转换一个 IO 流,写入到其他地方(可以是磁盘、网络 IO)
//内存中状态给永久保存下来了
//反序列化
//讲已经持久化的字节码内容,转换为 IO 流
//通过 IO 流的读取,进而将读取的内容转换为 Java 对象
//在转换过程中会重新创建对象 new
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton(){}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
}
public static void main(String[] args) {
SeriableSingleton s1 = null;
SeriableSingleton s2 = SeriableSingleton.getInstance();
FileOutputStream fos = null; // 输出流
try {
//将对象序列化并写入到文件中
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SeriableSingleton) ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
结果:
很明显单例被破坏
问题来了,序列化怎么破坏单例的?如何避免这种破坏?
接下来就是本文的重头戏了
深入源码抽丝剥茧
- 从这开始读取对象
综上所述 :
我们只能重写一个叫做ReadREsolve()的方法,并返回单例对象来避免单例被破坏.
//重写readResolve方法,只不过覆盖了反序列化出来的对象
//还是创建了两次,发生在JVM层面,相对来说比较安全
//之前反序列化出来的对象会被GC回收
public SeriableSingleton ReadResolve(){
return INSTANCE;
}
注册式单例
注册式单例 : 就是将每一个实例都登记到某一个地方,使用唯一的标识获取单例.
注册式单例有两种写法:
- 一种为容器式缓存
- 一种为枚举登记
枚举式单例
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 ;
}
}
测试反序列化破坏单例
使用jad反编译工具 :
利用了static实例化对象,没有线程安全问题
问题又来了,它是怎么避免反射破坏单例的呢?反序列化破坏单例的呢?
序列化
反射
深入源码分析
从JDK层面就为Enum不被序列化和反射破坏单例保驾护航
容器式单例
- 便于管理对象,同时也是懒加载
- 存在线程安全问题
ublic class ContainerSingleton {
private 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 (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return obj;
}
return ioc.get(className);
}
}
}
到此为止了吗?
还有ThreadLocal实现的单例(以后再说)