设计模式学习——单例模式
一、单例模式的概念
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 中的容器式单例的实现:
七、单例模式小结
单例模式可以保证内存中只有一个实例,减少了内存开销,避免对资源过多占用。
标签:
相关文章
-
无相关信息