Java设计模式(二)单例模式

写在前面

​ 首先单例模式在面试中是经常用到的+最近复习期末,如果面试官问你你最先可能想到的1.1中描述的静态常量饿汉式,但是除了内存浪费的潜在情况外,如果后面项目更改需求需要给instance设置一些参数属性的,就很不好去改变了。因为我们声明它的类型为final!而且由于一开始我们就写为static类型,类加载就会实例化,万一用不到,占用内存就很不划算!如果考虑到了这一步,那么我们给出的答案可能是1.3节中不安全的懒汉式,但显然当多个线程并行调用 getInstance() 的时候,就会创建多个实例,这样的回答肯定也不是最佳答案。既然非要线程安全,那就上锁好了,于是我们可以想到synchronized加在getInstance()方法上,这样实现安全是没什么问题的,但这样的实现方法也不是最好的,因为加锁操作是很费时间的,于是就有人说那就双重检验锁呗。可是这个双重检验锁就有两种情况,到底哪一种是最好的选择呢。一个个的看,第一种也是大家最先容易想到的(1.6.1节),另外一种(1.6.2节)是在第一种的基础上将instance 变量声明成 volatile,具体哪一种更适合实际开发大家可以仔细阅读下面的内容。 以及还有Effective Java中推荐的枚举方式,为了尽可能的多学点东西,我把能想到的东西和难理解的都做了汇总,如下如下。

  • 使用单例模式优点和缺点?
    • 单例设计模式一共有9种写法,后面我们会依次讲到 1) 饿汉式 两种 2) 懒汉式 三种 3) 双重检查 2种 4) 静态内部类 5) 枚举
  • 单例模式使用的场景?
    • 需要频繁进行创建和销毁的对象
    • 创建对象时耗时过多或资源过多,但又经常用到的对象
    • 频繁访问数据库或文件的对象
1.单例模式
1.1 静态常量饿汉式
1.1.1 步骤

​ 1)构造器私有化

​ 2)类的内部创建对象

​ 3)向外暴露一个静态的公共方法

​ 4)代码实现:

public class SingletonTest01 {

	public static void main(String[] args) {
		//测试
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
	}

}

//饿汉式(静态变量)

class Singleton {
	
	//1. 构造器私有化, 外部不能new
	private Singleton() {
		
	}

	//2.本类内部创建对象实例
	private final static Singleton instance = new Singleton();
	
	//3. 提供一个公有的静态方法,返回实例对象
	public static Singleton getInstance() {
		return instance;
	}
	
}

​ 回顾一下刚刚说过的静态常量饿汉式的的写法,首先构造器私有化,设置为private,外部无法new一个该类的对象,为了与之对应我们提供一个共有的静态方法,可以返回我们创建的实例对象。实例对象如何创建呢,需要在该类的内部创建,但是注意类型是private + final +static!

​ 当类加载的时候实例就创建完了,如果我们在程序中始终没有使用这个实例,那么就会有内存浪费的问题。

1.2 静态代码块饿汉式

​ 和1.1中的类似。即使是变成静态代码块的方式,也是在类装载的时候,执行静态代码块中的代码,去初始化实例。

public class SingletonTest02 {

	public static void main(String[] args) {
		//测试
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
	}

}


class Singleton {
	
	//1. 构造器私有化, 外部能new
	private Singleton() {
		
	}
	
	//2.本类内部创建对象实例
	private  static Singleton instance;
	
	static { 
      	// 在静态代码块中,创建单例对象
		instance = new Singleton();
	}
	
	//3. 提供一个公有的静态方法,返回实例对象
	public static Singleton getInstance() {
		return instance;
	}
	
}
1.3 线程不安全懒汉式

​ 提供一个静态的公有方法,如果用到该方法,采取创建instance。这种懒汉式的一个好处就是第一次用到了才会实例化,用不到就不会,但是在返回实例的时候需要考虑线程安全问题。怎么说呢,假设在一个多线程的情况下(此前还没有创建过实例),线程A进入了if判断语句,还没有接着往下执行,线程B和线程C也来了,他们执行if判断语句发现singleton==null成立,那么就会实例化,等到了A接着执行的时候,已经创建了N个实例了,所以多线程情况下不能使用,实际开发中也不这么用。

public class SingletonTest03 {

	public static void main(String[] args) {
		System.out.println("懒汉式1 , 线程不安全~");
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
	}

}

class Singleton {
	private static Singleton instance;
	
	private Singleton() {}
	
	//提供一个静态的公有方法,当使用到该方法时,才去创建 instance
	//即懒汉式
	public static Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}
1.4 线程安全懒汉式

​ 上面提到的线程安全问题主要是在public static Singleton getInstance()中出现的,那么为了解决这个问题,我们可以加上synchronized来解决线程不安全的问题。但是也带了新的问题,比如效率低。通俗的说,加上了synchronized之后,如果想要去调用这个静态方法,是不是需要一个一个线程的去用?线程A结束,线程B才能用,此时线程CDE的需要排队等着。但实际上,实例化在整个过程只执行1次,剩下的线程如果再想要获取这个实例,该方法都会return。所以synchronized同步带来的问题就是同步效率低,同样的,实际开发中不推荐使用。

public class SingletonTest04 {

	public static void main(String[] args) {
		System.out.println("懒汉式2 , 线程安全~");
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
	}

}

// 懒汉式(线程安全,同步方法)
class Singleton {
	private static Singleton instance;
	
	private Singleton() {}
	
	//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
	//即懒汉式
	public static synchronized Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}
1.5 同步代码块懒汉式

​ 本意是想对第四种实现方式的改进,因为前面同步方法效率低,改为同步产生实例化的代码块,但是这些同步并不能起到线程同步的作用。

1.6 DoubleCheck【使用volatile和不使用volatile】
1.6.1 不使用volatile
public class SingletonTest07 {

	public static void main(String[] args) {
		System.out.println("DoubleCheck 不使用volatile");
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
		
	}

}

class Singleton {
	private static Singleton instance;
	
	private Singleton() {}
	
	private static class SingletonInstance {
		private static final Singleton INSTANCE = new Singleton(); 
	}
		
	public static synchronized Singleton getInstance() {
		
		return SingletonInstance.INSTANCE;
	}
}

​ 单纯运行上面这个例子没啥问题,但是如果在多线程情况下是有问题的。主要在于 instance = new Singleton() 这句代码,改进方式如1.6.2,具体为什么会出错可以看看3.1节。

1.6.2 使用volatile

​ 主要是进行了两次if判断,保证线程安全。实例化代码只有一次,后面再访问的时候,判断if直接返回实例化的对象,此外,线程安全,延迟加载。

public class SingletonTest07 {

	public static void main(String[] args) {
		System.out.println("DoubleCheck 使用volatile");
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
		
	}

}

class Singleton {
	private static volatile Singleton instance;
	
	private Singleton() {}
	
	private static class SingletonInstance {
		private static final Singleton INSTANCE = new Singleton(); 
	}
	
	public static synchronized Singleton getInstance() {
		
		return SingletonInstance.INSTANCE;
	}
}
1.7 静态内部类

​ 静态内部类提供了一种既有懒加载还能保证线程安全以及很简单的方法!首先静态内部类只会被加载一次(保证线程安全),而且通过静态内部类的方法就不会出现Singleton类加载的时候就实例化对象的问题(就是说内存浪费是不可能的!)。

1.8 枚举方式

​ 枚举方式是在Effective Java这本书中推荐使用的,不得不说看了关于单例模式的各种介绍啥的,感觉枚举方式是最简单的,根本不用考虑这么多的事儿。枚举方式不仅能避免多线程同步问题,还可以防止反序列化重新创建新的对象。通过Singleton.INSTANCE来访问实例,创建枚举默认就是线程安全的,所以不用考虑太多了。

2 单例模式JDK源码

​ java.lang.Runtime就是使用的单例模式。

3 关于一些别的

volatile第一次接触实在某个培训机构的某个课程中看到的,其实是比较陌生的,以及在学习枚举方式中对防止反序列化的理解还不够深入。看了几篇博客,在此做下总结。

3.1 volatile

​ volative是java中提供的一种轻量级的同步机制,synchronized通常称为重量级锁,volatile则是个轻量级的,当我们使用了volatile,我们可以保证共享变量对所有线程的可见性(当一个线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的),但是我们之前在单例模式中使用到的并不是volatile的可见性,而是禁止指令重排!重排很好理解,就是重新排列嘛,一段代码块有四条语句,按理说是应该按照1-2-3-4的顺序执行,如果它实际上按照2-3-1-4去执行,那么这个顺序就是重排了,解释的高大上写就是编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。

​ 其实到这儿可能还是不太明白跟重排有啥关系…再来进一步解释下。

    public static Singleton getInstance() {
        if (instance == null) {                  
            synchronized (Singleton.class) {
                if (instance == null) { 
                    instance = new TestInstance(); //row 5 is here
                }
            }
        }
        return instance;//6
    }

​ 首先来说第五行instance = new TestInstance();到底是咋执行的,分成3步:

​ 1) memory=allocate(); // 分配内存 相当于c的malloc

​ 换言之:给 instance 分配内存

​ 2) ctorInstanc(memory) //初始化对象

​ 换言之:调用 Singleton 的构造函数来初始化成员变量

​ 3) instance=memory //设置instance指向刚分配的地址

​ 换言之:将instance对象指向分配的内存空间,执行完这一步骤instance就不是null了

​ 但是在编译的时候,存在指令重排的情况,就不一定按照写的这个顺序执行,假设这样一个场景,线程A在执行instance = new TestInstance();的时候,来了个B线程,这个时候A已经完成了分配内存和设置instance指向刚分配的空间,那么B线程一判断发现instance不是null,然后直接返回的对象还没有初始化,此时就会出现问题!

3.2 防反序列化

​ 单例模式在不考虑序列化的情况下,无论勤加载还是懒加载,均是安全的。将单例序列化再反序列化,变成了2个单例。明明是将一个对象写文件,然后又将文件中的对象读出,按照预期singleton和singleton2应该是是同一个对象,但从测试结果可知,并不是像我们预期的那样。

​ 为什么会出现这样的情况,我们可以看下readOrdinaryObject方法的部分代码块:

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        ...

        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        ...

        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

​ 关键在这儿:obj = desc.isInstantiable() ? desc.newInstance() : null;,如果desc这个对象(在这里我们理解为一个serializable/externalizable的类)可以在运行时被实例化,那么该方法就返回true,根据这个运算规则返回true的话就会执行obj=desc.newInstance(),通过反射的方式调用无参构造方法新建一个对象,readOrdinaryObject方法最后返回的obj对象就是我们需要读出来的对象。所以这就是为啥序列化后反序列化可以破坏单例模式的原因。不过除了枚举方式以外,还有别的方法可以解决这个问题。

两种方式解决此问题:

​ 1, 对单例声明 transient Connection connection;然后实现readObject(ObjectInputStream in) 方法,复用原来的单例。

​ 2, 如果想实现写入文件和从文件中读取的对象为同一个,只需放开单例类的readResolve()方法即可。实现readResolve()方法,返回原来单例。

private Object readResolve() throws ObjectStreamException{
		return instance;
}

关于readResolve()方法解决该问题,在Java单例模式(适合在面试时写)一文中,作者给出了这样的解释。

ObjectInputStream类的readOrdinaryObject方法在调用readSerialData()方法后,就调用了 ObjectStreamClass类的Object invokeReadResolve(Object obj)方法,通过反射调用了我们自己写的readResolve方法。此方法防止反序列化获取多个对象。 无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。

关于反射、序列化破解单例模式,推荐阅读单例模式的漏洞,通过反射和序列化、反序列化来破解单例,以及如何避免这些漏洞

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值