1.什么是单例模式?
确保一个类只有一个实例,并提供一个全局访问点。
这句话什么意思呢? 就是利用这个设计模式可以让指定的类在它的整个使用周期上只生产出一个实例对象并存放在内部,且提供一个公共方法去访问这个对象。
2.根据具体的例子理解单例模式
2.1 单例类设计
员工A在写代码的时候发现自己写一个了类,但是这个类在整个系统中好像只需要一个对象,从业务的角度上,这个类不允许出现第二个对象,不然会产生很恐怖的后果!! 但是他又没办法防止别人使用这个类生成对象,他这时候犯难了,他不知道该怎么办。
员工B听到他的难处了,轻笑一声:还是太年轻了吧! 你不知道有一种设计叫单例模式吗?
先上图:
确保一个类只有一个实例(构造方法私有,声明静态自类型变量),并提供一个全局访问点(提供一个公共方法访问静态变量)。
没错,就是这么简单,下面上代码:
这个类保证了自己只生成一个对象,怎么做到的呢? 其实将构造方法私有化,并在自己内部组合一个自身的引用且提供一个公共方法得到这个引用。
package SingletonPattern.first;
/**
* 普通单例类,应用场景:
* 注册表设置,连接池,线程池等等。利用单件模式对象,我们可以确保程序中使用的全局资源只有一份
*/
public class Singleton {
private static Singleton instance;
private Singleton(){}
/**
* 延迟实例化
* @return
*/
public static Singleton getInstance(){
if(instance==null){
instance = new Singleton();
}
return instance;
}
public void sayHello(){
System.out.println("Hello world!!");
}
}
员工A如获至宝,立即打断了员工B的讲解,并迫不及待的在程序中用上了。 这时员工B只是笑笑不说话。
2.2 多线程下的单例类安全吗?
用了一段时间,员工A发现,上面单例类的设计存在一个缺陷,就是在多线程的情况下不安全!!
如果两个线程同时进入了空判断,这时,可能会发生实例化两次的情况。
如下所示:
package SingletonPattern.third;
/**
* 单例模式确保了一个类只有一个实例,并提供了一个全局访问点
*/
public class SingletonChocolateBoiler {
public static SingletonChocolateBoiler Instance;
/**
* 通过私有化构造方法,达到单一实例的效果
*/
private SingletonChocolateBoiler(){
}
/**
* 获取实例唯一入口
* @return 此类的唯一实例
*/
public static SingletonChocolateBoiler getInstance(){
if(Instance==null){
Instance = new SingletonChocolateBoiler();
System.out.println("构造了一个新SingletonChocolateBoiler对象");
}
return Instance;
}
}
线程安全测试:
package SingletonPattern.third;
/**
* 按道理说,使用Singleton对象时,无论多少个线程使用它,它都应该始终构造一次即可,但是,
* 如果按照second中的做法去设计类,时会出现线程安全问题的,例如如下
*/
public class ThreadProblem {
public static void main(String[] args) {
for(int i=0;i<1000;i++){
new Thread(()->{
SingletonChocolateBoiler.getInstance();
}).start();
}
}
}
这时,员工A想起了员工B在向他介绍这个模式的时候好像还有些话没说。于是,他找到了员工B,员工B欣慰的笑了,说:小子,你终于发现了。 给你个提示: synchronized
员工A茅塞顿开:
package SingletonPattern.third;
public class FirstSafeSingleton {
private static FirstSafeSingleton Instance;
private FirstSafeSingleton(){}
/**
* 通过这样进行同步确实可以解决问题,但是通过同步加锁是一定会增加性能开销的
* 因为我们只需要在第一次构造实例时进行同步即可,其他的情况不需要同步
* @return
*/
public static synchronized FirstSafeSingleton getInstance(){
if(Instance == null){
Instance = new FirstSafeSingleton();
System.out.println("构造一个新的FirstSafeSingleton");
}
return Instance;
}
}
紧接着做出了如下对比测试:
package SingletonPattern.third;
/**
* 按道理说,使用Singleton对象时,无论多少个线程使用它,它都应该始终构造一次即可,但是,
* 如果按照second中的做法去设计类,时会出现线程安全问题的,例如如下
*/
public class ThreadProblem {
public static void main(String[] args) {
for(int i=0;i<10000;i++){
new Thread(()->{
SingletonChocolateBoiler.getInstance();
}).start();
}
//通过synchronized解决后测试
for(int i=0;i<10000;i++){
new Thread(()->{
FirstSafeSingleton.getInstance();
}).start();
}
}
}
果然,安全问题解决了。但是员工A还是觉得不划算,只是为了解决第一次构造实例时的线程安全问题就牺牲整个getInstance方法的性能,太不划算了。
有没有什么方法,只保证在第一次构造对象时的线程安全即可。
员工B欣慰的笑了,随后提示到: 双重检测机制。
这时员工A上百度搜索了一下,茅塞顿开:
package SingletonPattern.third;
public class SecondSafeSingleton {
/**
*volatile保证了此变量的可见性(一个线程对主内存的修改可以及时的被其他线程观察到)
*这样当第一个线程将此变量的实例存入主内存后,其他线程知道了后
*在进行Instance==null判断的时候就会判断正确并获得正确的实例对象
* 防止重排序出现问题
*/
private volatile static SecondSafeSingleton Instance;
private SecondSafeSingleton(){ }
/**
* 双重检查锁机制解决first中引起的性能降低
* @return
*/
public static SecondSafeSingleton getInstance(){
//第一次检查,这里可能会出现多个线程同时进入的情况
if(Instance==null){
//此时采取同步措施,保证多个线程进入下面的代码块是串行的
synchronized (SecondSafeSingleton.class){
//这里第一个线程肯定是满足条件的,此时它进入下面的步骤进行实例构造
//但由于我们使用了volatile,Instance被更新了立刻被其他线程知道了,
//第二个线程在进来的时候这里的判断是过不去的
if(Instance==null){
Instance = new SecondSafeSingleton();
System.out.println("构造了一个新的SecondSafeSingleton");
}
}
}
return Instance;
}
}
然后又做出了对比测试: