素材巴巴 > 程序开发 >

设计模式学习——单例模式

程序开发 2023-09-09 21:16:47

一、单例模式的概念

1.1 概念

单例模式是指 确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。

单例模式的特点是隐藏其所有的构造方法。

属于创建型模式。

1.2 单例模式的适用场景

确保任何情况下都绝对只有一个实例。

例如ServletContext、ServletConfig、ApplicationContext

1.3 单例模式的常见写法

1.饿汉式单例

2.懒汉式单例

二、饿汉式单例

饿汉式单例,指在类刚刚加载还没有实例化的时候就被创建实例。

2.1 实现方式一

 构造方法私有,通过static成员的特点,在类刚刚加载还没有实例化的时候就被创建实例:

/*** @Auther: jesses* @Description: 饿汉式单例实现方式一*/
 public class HungrySingleton {private static final HungrySingleton hungrySingleton=new HungrySingleton();private HungrySingleton(){}public static HungrySingleton getInstance(){return hungrySingleton;}
 }
 

2.2 实现方式二

构造方法私有化,通过static代码块,在类刚刚加载还没有实例化的时候就被创建实例:

/*** @Auther: jesses* @Description: 饿汉式单例实现方式二*/
 public class HungrySingleton2 {//使用final是为了避免有人通过反射机制将它改变private static final HungrySingleton2 hungrySingleton;static{hungrySingleton = new HungrySingleton2();}private HungrySingleton2(){}public static HungrySingleton2 getInstance(){return hungrySingleton;}
 }
 

饿汉式单例的特点

构造方法私有化 类加载时创建实例,不会出现线程安全的问题。

浪费内存空间,因为不管是否用到,在类加载时就会创建实例。

因此需要改进,这就出现了懒汉式单例。

三、懒汉式单例

被外部调用时才会创建这个单例的实例。

3.1 实现方式一  simple实现方式:

/*** @Auther: jesses* @Description: simple实现方式*/
 public class LazySimpleSingleton {private static LazySimpleSingleton lazy=null;private LazySimpleSingleton(){}public static LazySimpleSingleton getInstance(){if (lazy == null){//!!存在线程安全的问题lazy=new LazySimpleSingleton();}return lazy;}
 }
 
/*** @Auther: jesses* @Description: 线程中创建单例对象*/
 public class ExectorThread implements Runnable {public void run() {LazySimpleSingleton instance = LazySimpleSingleton.getInstance();System.out.println(Thread.currentThread().getName()+" : "+instance);}
 }
/*** @Auther: jesses* @Description: 测试类*/
 public class LazySimpleSingletonTest {public static void main(String[] args) {Thread thread1 = new Thread(new ExectorThread());Thread thread2 = new Thread(new ExectorThread());thread1.start();thread2.start();System.out.println("Exec End .");}
 }

 运行结果可以看到,两个线程会创建出不同的实例。

在demo中单例的实现存在线程安全的问题,若两个线程同时通过了if判断,进入if内部,会创建两个不同的对象。

因此,优化方案是在getInstance()方法上加synchronize关键字。但是synchronize性能不好,尤其修饰在static方法上,会造成整个类都被锁定。

因此,更优解是,将synchronized关键字只修饰方法内部的代码块。

/**
 * 将synchronized关键字只修饰方法内部的代码块
 **/
 public class LazyDubboCheckSingleton {private static LazyDubboCheckSingleton lazy=null;private LazyDubboCheckSingleton(){}public static LazyDubboCheckSingleton getInstance(){if (lazy == null){//synchronized修饰在静态方法上可能造成整个类都被锁定,将synchronized设置在方法内部,这样线程至少可以进入方法。//但是两个线程都同时执行到此处时,都会得到lazy都是空,将会顺序执行到synch中的代码,又出现多次创建不同实例的情况。这种情况又需要进行改进synchronized (LazyDubboCheckSingleton.class){lazy=new LazyDubboCheckSingleton();}}return lazy;}
 }
 

synchronized修饰在静态方法上可能造成整个类都被锁定,将synchronized设置在方法内部,这样线程至少可以进入方法。

但是两个线程都同时进入方法,又会出现得到的lazy都是空,顺序执行synchronized片段,又出现了多次创建不同实例的情况。

故而需要双重检查锁,在synchronized代码块中再加一次判断,即下面的双重检查锁的方式。

3.2 实现方式二 双重检查锁实现:

/*** @Auther: jesses*/
 public class LazyDubboCheckSingleton {private static LazyDubboCheckSingleton lazy=null;private LazyDubboCheckSingleton(){}public static LazyDubboCheckSingleton getInstance(){if (lazy == null){//此判断如删除则无法进入方法synchronized (LazyDubboCheckSingleton.class){//为避免出现多次创建不同实例的线程安全问题,所以需要再多加一层判断,也就是双重检查if (lazy == null){lazy=new LazyDubboCheckSingleton();}}}return lazy;}
 }
 

3.3 实现方式三 内部类实现方式:

/*** @Auther: jesses* @Description: 内部类实现懒汉式单例*/
 //没有使用到synchronized,性能最高
 public class InnerClassLazySingleton {private InnerClassLazySingleton() {}//懒汉式单例//在LazyHolder里的逻辑要等到外部方法getInstance调用时才执行。//同时利用加载类前先加载静态内部类的特性,加载LazyHolder的空对象,但不执行其中内容。//因为类加载机制只会加载一次,实现了单例。//LazyHolder的空对象在类加载时已经生成,在调用getInstance()时,则转变成实质对象。因而,是线程安全的。public static final InnerClassLazySingleton getInstance() {return LazyHolder.LAZY;}private static class LazyHolder {private static final InnerClassLazySingleton LAZY = new InnerClassLazySingleton();}
 }
 

这种方式虽然解决了线程安全问题和性能问题。

但是它还是可能被反射攻击。

四、反射破坏单例

/*** @Auther: jesses* @Description:*/
 public class InnerClassLazySingletonTest {public static void main(String[] args) {try {Class clazz = InnerClassLazySingleton.class;Constructor c = clazz.getDeclaredConstructor(null);c.setAccessible(true);//该类构造方法被私有,通过此设置进行强制访问InnerClassLazySingleton obj1 = c.newInstance();InnerClassLazySingleton obj2 = InnerClassLazySingleton.getInstance();System.out.println(obj1==obj2);} catch (Exception e) {e.printStackTrace();}}
 }

InnerClassLazySingleton类的构造方法已经被私有,但还是可以通过反射的方式,直接构造出对象实例,破坏单例。

对比两种方式创建的对象,可以发现结果是false。单例确实被破坏了。

要避免反射破坏单例的漏洞,就可以在构造方法内加入校验:

五、反序列化破坏单例

5.1 通过反序列化破坏单例:

/*** @Auther: jesses* @Description: 自定义单例类,构造方法私有,提供getInstance方法获取单例实例*/
 public class SeriableSingleton implements Serializable {private final static SeriableSingleton INSTANCE = new SeriableSingleton();private SeriableSingleton() {}public static SeriableSingleton getInstance() {return INSTANCE;}//private Object readResolve(){//    return INSTANCE;//}
 }
 
/*** @Auther: jesses* @Description: 入口函数,测试通过反序列化得到的多个实例是否单例*/public class SeriableSingletonTest {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();}}
 }

运行结果: 

可以看到,通过反序列化获取运行得到的不是同一个单例对象。

5.2 如何解决反序列化破坏单例?

将SeriableSingleton类中被注释的readResolve()方法解开,

再次运行test类,

在加上readResolve方法后,test类中两次获取到的是相同的单例对象了。

5.3 原因

现在查看源码 看看为何添加了resolve方法后,就不会创建不同的实例.

点击进入流转对象的readObject()方法:

可以看到调用了readObject0()方法:

继续进入readObject0()方法,可以看到调用了readOrdinaryObject():

进入readOrdinaryObject()方法,可以看到判断了对象是否可以实例化,可以就创建新实例obj:

 进入isInstantiable()方法,得出结论,是根据这个对象是否有构造方法来判断是否可以实例化的。

 有构造方法则true,创建新的实例:

 接着readOrdinaryObject()方法继续向下深入,可以看到源码中判断该对象中是否存在ReadResolve方法,

如果存在该方法,就代理该ReadResolve()方法,将ReadResolve方法的返回 重新赋值给obj对象。

而我们的ReadResolve()方法中直接返回了单例的实例,因此两次创建都是同一个实例:

通过追踪源码,可以了解到实际上还是实例化了两次,只不过有ReadResolve方法时,新创建的对象obj没有返回,而是把单例赋值给了新对象的引用obj。

六、注册式单例

注册式单例又称为登记式单例,就是将每个实例都登记到某处,用唯一的标识获取实例。

注册式单例有两种:枚举登记、容器缓存。

6.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;}
 }
 

测试类:

/*** @Auther: jesses* @Description: 注册式单例-枚举式*/
 public class EnumSingletonTest {public static void main(String[] args) {try {/** 构建该枚举类的实例instance2,并设置属性data为new Object() */EnumSingleton instance2 = EnumSingleton.getInstance();instance2.setData(new Object());//将该属性输出到 EnumSingleton.obj 文件FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(instance2);oos.flush();oos.close();//读取 EnumSingleton.obj 文件,使用一个新的枚举实例instance1接收EnumSingleton instance1 = null;FileInputStream fis = new FileInputStream("EnumSingleton.obj");ObjectInputStream ois = new ObjectInputStream(fis);instance1 = (EnumSingleton) ois.readObject();ois.close();//对比两个实例中的data属性System.out.println(instance1.getData());System.out.println(instance2.getData());System.out.println(instance1.getData()==instance2.getData());} catch (Exception e) {e.printStackTrace();}}
 }

运行结果,两个实例的data属性相同:

为什么枚举式单例可以避免反序列化破坏单例呢?

接下来,分析源码了解其原理。

使用XJad、Jad等反编译工具对EnumSingleton.class文件进行反编译:

可以看到,枚举式单例实际上在静态代码块中就对INSTANCE进行了初始化,

很显然,这是饿汉式单例,在加载时就初始化实例,因此不会出现线程问题。

接着跟踪反序列化的过程源码,

进入readObject()方法:

其中调用了readObject0()方法,继续深入readObject0方法: 

 在枚举类型的处理逻辑中,调用了readEnum()方法,进入readEnum()方法查看其实现:

 可以看到,通过Enum.valueOf(Class,name)获取了实例,再进入valueOf()方法看看:

终于发现,这里从枚举类的一个enumConstantDirectory通过枚举中的name(值项)获取,

而enumConstantDirectory是一个Map的数据结构,

我们知道,Map中的key是不能重复的,因此通过"INSTANCE"这个name只能从改map中获取到同一个对象实例,

这也就是枚举式单例不能被反序列化破坏的原因了:

那么,枚举式单例能否被反射破坏呢?

 测试反射创建枚举式单例的实例:

public class EnumSingletonTest2 {public static void main(String[] args) {try{Class clazz = EnumSingleton.class;Constructor constructor = clazz.getDeclaredConstructor();constructor.newInstance();}catch (Exception e){e.printStackTrace();}}
 }
 

运行发现报错,找不到无参构造方法: 

 查看Enum源码,发现Enum只有唯一的一个带参构造方法,并且访问修饰符是protect的。

那么修改代码,再次测试:

public class EnumSingletonTest2 {public static void main(String[] args) {try{Class clazz = EnumSingleton.class;Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);constructor.setAccessible(true);EnumSingleton enumSingleton = constructor.newInstance();}catch (Exception e){e.printStackTrace();}}
 }
 

运行,发现报错,不能用反射创建枚举式单例的实例:

为什么会限制枚举式代理用反射创建实例呢?

再到源码中寻找原因,进入newInstance()方法:

可以看到,在Constructor.newInstance()方法中做了判断,如果类的修饰符是Enum,就直接抛出异常。 

原来是在JDK层面就已经替我们避免了反射破坏单例。

枚举式单例是《Effective Java》书中推荐的一种单例写法。

因JDK枚举的特性,避免了反射破坏单例,枚举式单例成为较好的一种实现。

6.2 容器式单例

/*** @Auther: jesses* @Description: 注册式单例-容器缓存*/
 public class ContainerSingleton {private ContainerSingleton() {}private static Map singletonMap = new ConcurrentHashMap<>();public static Object getBean(String className) {// ConcurrentHashMap是线程安全的,但只能保证map内部线程安全,无法保证此getBean方法线程安全。//加synchronized以保证创建单例实例线程安全。synchronized (singletonMap) {//通过className从Map容器中取实例,若不存在,则使用创建后加入Map容器。if (singletonMap.containsKey(className)) {return singletonMap.get(className);} else {Object object = null;try {object = Class.forName(className).newInstance();singletonMap.put(className, object);} catch (Exception e) {e.printStackTrace();}return object;}}}}
容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的,需对获取实例的方法做加锁处理。 容器式单例在Spring中常被使用到,下图为 Spring 中的容器式单例的实现:

七、单例模式小结

单例模式可以保证内存中只有一个实例,减少了内存开销,避免对资源过多占用。 


标签:

素材巴巴 Copyright © 2013-2021 http://www.sucaibaba.com/. Some Rights Reserved. 备案号:备案中。