zl程序教程

您现在的位置是:首页 >  后端

当前栏目

可能是最全的Java单例模式讨论

JAVA模式 可能 最全 单例 讨论
2023-09-14 08:57:59 时间
new 最常用的,直接使用构造器创建。 每new一次都会产生新的实例。所以单例中应该只new一次,当再想用对象时都返回该对象的值 Class.newInstance() 该方法会调用public 的无参构造器。
为了防止这个方式创建,只要把构造器设置为private的就可以了。这是如果再用这个方法创建会报错.同时私有构造器也可以解决四处new的问题。 反射
Constructor ctt = c.getDeclaredConstructor();
ctt.setAccessible(true);
T t1 = ctt.newInstance();
这样私有构造器也不行了。解决的办法是使用抽象类,这样就会抛出异常了,不能创建了。或者在构造器中加入判断如果是第二次构建就抛出异常。 clone
这个主要由clone()方法的具体行为决定的。如果没有实现Cloneable接口是不用管这个问题的。 反序列化
反序列化的时候也会打破单例,解决的方式是写一个readResolve。这个方法的规则是在反序列化的时候勇气返回值来代替反序列化的返回值
还有一个更简单的办法是不要实现Serializable接口,这样序列化的时候就会报错了 先写个验证工具,来验证这个类是否是单例的
public class SingletonTester {

 public static T void checkClassNewInstance(Class T c){

 try {

 T t1 = c.newInstance();

 T t2 = c.newInstance();

 if(t1 != t2){

 System.out.println("Class.newInstance校验失败,可以创建两个实例");

 }else{

 System.out.println("Class.newInstance校验通过");

 } catch (Exception e) {

 System.out.println("不能用Class.newInstance创建,因此Class.newInstance校验通过");

 public static T void checkContructorInstance(Class T c){

 try {

 Constructor T ctt = c.getDeclaredConstructor();

 ctt.setAccessible(true);

 T t1 = ctt.newInstance();

 T t2 = ctt.newInstance();

 if(t1 != t2){

 System.out.println("ContructorInstance校验失败,可以创建两个实例");

 }else{

 System.out.println("ContructorInstance校验通过");

 } catch (Exception e) {

 System.out.println("不能用反射方式创建,因此ContructorInstance校验通过");

 public static T void testSerializable(T t1){

 File objectF = new File("/object"); 

 ObjectOutputStream out = null;

 try {

 out = new ObjectOutputStream(new FileOutputStream(objectF));

 out.writeObject(t1);

 out.flush();

 out.close();

 ObjectInputStream in = new ObjectInputStream(new FileInputStream(objectF)); 

 T t2 = (T) in.readObject(); 

 in.close();

 if(t1 != t2){

 System.out.println("Serializable校验失败,可以创建两个实例");

 }else{

 System.out.println("Serializable校验通过");

 } catch (Exception e) {

 System.out.println("不能用反序列化方式创建,因此Serializable校验通过");

 public static void main(String[] args) {

 checkClassNewInstance(Singleton3.class);

 checkContructorInstance(Singleton3.class);

 testSerializable(Singleton3.getInstance());

}

这个工具验证了Class.newInstance攻击,反射攻击,反序列化攻击,能够屏蔽着三种攻击的才是好的单例。


最普通懒汉模式的单例, 私有构造器,静态方法获取实例,获取的时候先判空。
测试结果:

不能用Class.newInstance创建,因此Class.newInstance校验通过

ContructorInstance校验失败,可以创建两个实例

不能用反序列化方式创建,因此Serializable校验通过

这个类因为不能被序列化,因此不会受到反序列化攻击
因为私有构造器避免了Class.newInstance
但是会被反射攻击
另外其不是线程安全的


不能用Class.newInstance创建,因此Class.newInstance校验通过

ContructorInstance校验失败,可以创建两个实例

不能用反序列化方式创建,因此Serializable校验通过

同样不会有反序列化及Class.newInstance的问题。
并且没有并发的问题。
不过其会在不同的时候也初始化一个实例出来。个人感觉实际上影响不大

上面的都会有反射攻击的问题。来解决它。


不能用Class.newInstance创建,因此Class.newInstance校验通过

不能用反射方式创建,因此ContructorInstance校验通过

不能用反序列化方式创建,因此Serializable校验通过

通过加入计数器来解决,这样虽然解决了反射攻击,但是却不是线程安全的,另外引入了新的变量也不优雅。下面换个方式:


private static class SingletonHolder{ private static final Singleton4 INSTANCE = new Singleton4() { private Singleton4(){}; public static Singleton4 getInstance(){ return SingletonHolder.INSTANCE; }

这个推荐使用


如果没有 //1 的检查,那么所有的getInstance()都会进入锁争夺,会影响性能,因此加入了检查。
此外其会被反射攻击

上面的会有线程安全问题,是由于JVM的重排序机制引起的:
重排序:
JVM在编译的时候会保证单线程模式下的结果是正确的,但是其中代码的顺序可能会进行重排序,或者乱序,主要是为了更好的利用多cpu资源(乱序), 以及更好的利用寄存器,。
比如1 a = 1; b = 2; a=3;三个语句,如果b执行的时候可能会占用a的寄存器位置,JVM可能会把a=3语句提到b=2前面,减少寄存器置换次数。
比如上面的 instance = new Singleton5()这部分代码的伪字节码为:
1. memory = allocate() // 分配内存
2. init(memory) // 初始化对象
3. instance = memory // 实例指向刚才初始化的内存地址。
4. 第一次访问instance
在JVM的时候有可能2.3的位置进行了重新排序,因为JVM只保证构造器执行完之后的结果是正确的,但是执行顺序可能会有变化。 这个时候并发调用getInstance的时候就有可能出现如下的情况:


为了解决这个问题,我们可以从两个方向考虑:制止重排序,或者使重排序对其他线程不可见。

制止重排序的方式单例:
使用JDK1.5之后提供的volatile关键字。这个关键字的意义在于保证变量的可见性。保证变量的改变肯定会回写主内存,并且关闭java -server模式下的一些优化,比如重排序:


public abstract class Singleton6 {

 private static volatile Singleton6 sington = null;

 private Singleton6(){};

 public static Singleton6 getInstance(){

 if(sington == null){ // 1

 synchronized (Singleton6.class) {

 if(sington == null){ // 2

 sington = new Singleton6(){};;

 return sington;

}

还可以,但是代码有些长,不如Singleton4

使重排序对其他线程不可见的单例:

public abstract class Singleton7 {

 private static Singleton7 sington = null;

 private Singleton7(){};

 public static Singleton7 getInstance(){

 if(sington == null){ // 1

 synchronized (Singleton7.class) {

 if(sington == null){ // 2

 Singleton7 temp = new Singleton7(){};

 sington = temp;

 return sington;

}

另外单例4页是这样的,重排序对其他的线程是不可见的

如果有必要序列化,那么就需要实现Serializable接口,下面说下这种情况如何解决反序列化攻击的问题


public abstract class Singleton8 implements Serializable{

 private static class SingletonHolder{

 private static final Singleton8 INSTANCE = new Singleton8() {

 private Singleton8(){};

 public static Singleton8 getInstance(){

 return SingletonHolder.INSTANCE;

 public Object readResolve() {

 return SingletonHolder.INSTANCE;

}

测试结果:


不能用Class.newInstance创建,因此Class.newInstance校验通过

不能用反射方式创建,因此ContructorInstance校验通过

Serializable校验通过

这个主要在于方法readResolve, 其返回结果会用来代替反序列化的结果


枚举单例,effectiveJava中推荐的
最后一个了。就是使用枚举单例了。可以看一下,是极好用的


不能用Class.newInstance创建,因此Class.newInstance校验通过

不能用反射方式创建,因此ContructorInstance校验通过

Serializable校验通过

它也成功的避免了各种可能存在的问题:


没有实现clone 不会有反序列化的问题, 这个使用javap 仍然没有看到类似于readObject的源代码,应该是jdk内部生成字节码的时候做了某些操作。

好了,综上,尽量用枚举单例,或者是Holder单例吧


Java设计模式之单例模式 当你需要一个类只能创建一个对象的时候,例如数据库连接时,服务端只需要一个连接对象便能处理很多查询工作,如果此时一个连接一个查询势必会造成内存的浪费,造成服务器的卡顿,所以此时就出现了一个需求,怎样让一个类只创建一个对象呢