引言:单例设计模式是我们设计模式学习中很重要的一个,在实际开发中用处也非常多,今天把单利设计模式详细的总结一下,大家一起交流分享一下。
定义:单例设计模式就是保证一个类仅有一个实例,并提供一个访问他的全局访问点。
用处:有时候对于系统过来说一个实例很重要,例如:一个系统可以有多个打印任务,但是只能有一个正在工作的任务,一个系统只能有一个窗口管理器或者文件系统;一个系统只能打开一个任务管理器,如果不使用单利机制对窗口对象唯一化,将弹出多个窗口,如果这些窗口的像是内容完全一样,则是重复的,浪费资源,如果这些窗口内容不一致,则意味着某一瞬间系统有多个状态,与实际不符,也给用户代理误解,不知道哪一个才是真实的状态,因此有时确保系统中某个对象的唯一性非常重要。
那么如何保证一个实例并且这个实例易于被访问呢?
思路:
1.我们知道一个类通常都是根据构造函数来实例化的,那么假如将这个类的构造函数给私有化了,就不能直接new这个类,来创建对象了,后续具体如何操作全都放在这个类中,我们只负责调用其具体实例就好。
2.那么如何提供一个全局的访问点呢?我们知道static关键字的作用,如果把某一字段设置为静态的话,那么全局都可以访问它,他在类加载的时候就已经存在了,正好适合我们。
下面我们写出想像中的代码:
package test0930;
public class Singleton {
//一个静态的实例
private static Singleton singleton;
//私有化构造函数
private Singleton(){}
//给出一个公共的静态方法返回一个单一实例
public static Singleton getInstance(){
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
好了,现在我们实现了单例类的编写,可以总结为:
1.静态实例,带有static关键字的属性在每一个类中都是唯一的。
2.限制客户端随意创造实例,即私有化构造方法,此为保证单例的最重要的一步。
3.给一个公共的获取实例的静态方法,注意,是静态的方法,因为这个方法是在我们未获取到实例的时候就要提供给客户端调用的,所以如果是非静态的话,那就变成一个矛盾体了,因为非静态的方法必须要拥有实例才可以调用。
4.判断只有持有的静态实例为null时才调用构造方法创造一个实例,否则就直接返回。
正常来讲这个类已经可以称之为单例类了,但是如果是多线程的时候上述代码是有问题的,假如线程A和B,在线程A进入getInstance()方法内部的时候,判断实例是否为空,这时候不为空,就在这时线程B抢占了资源,判断实例也不为空,创建实例,那么当线程A在次获得执行权时候,就会在创见一个实例,造成了多个实例,与想要结果不符。
优化:我们知道,如果想要对线程进行同步的话,一般可以踩用synchronized 关键字,所以改进之后的代码如下:
package test0930;
public class SingletonDouble {
//一个静态的实例
private static SingletonDouble singleton;
//私有化构造函数
private SingletonDouble(){}
//给出一个公共的静态方法返回一个单一实例
public static SingletonDouble getInstance(){
if (singleton == null) {
synchronized(SingletonDouble.class){
if (singleton == null) {
singleton = new SingletonDouble();
}
}
}
return singleton;
}
}
改进之后,引入了同步代码块,对SingletonDouble 类进行同步,这样同步之后,只能有一个线程进入同步块,其他线程只能排队等待,避免了没有同步时候线程抢占造成的创建多个实例。
这里的两个判断主要用处:外层的if主要是在同步块之前判断,如果不为空,那么完全没必要进入同步块,避免了浪费是资源,如果为空,说明当前时候实例为空,这时会涉及到多线程的问题,第二个if就是为了解决这个问题的。
经过刚才的分析,貌似上述双重加锁的示例看起来是没有问题了,但如果再进一步深入考虑的话,其实仍然是有问题的。如果我们深入到JVM中去探索上面这段代码,它就有可能(注意,只是有可能)是有问题的, 因为虚拟机在执行创建实例的这一步操作的时候,其实是分了好几步去进行的,也就是说创建一个新的对象并非是原子性操作。在有些JVM中上述做法是没有问题的,但是有些情况下是会造成莫名的错误。
首先要明白在JVM创建新的对象时,主要要经过三步:
1.分配内存
2.初始化构造器
3.将对象指向分配的内存地址
如果是按照上面的三步一次执行,那么是没有问题的,因为jvm是将构造完的完整的对象分配了内存的地址,但是如果2和3步反了就会出现问题了(那么2和3步真的会反了么?会的,因为jvm会对字节码进行调优,其中一项调优就是指令顺序调优,那么2和3顺序就有可能相反,先执行3后执行2),此时如果线程A执行完了3,但是没有执行2步就退出了同步块,这时线程B进入同步块,if判断实例不为空(因为这时实例已经分配了内存空间,但是没有赋值,线程B会认为实例已经存在,进而直接返回),直接返回,造成返回的是还没有赋值的实例,会造成未知的错误,这个问题在jdk1.5之后就得到了解决,就是volatile关键字,他的主要是作用是防止指令重排,如果用这个关键字修饰实例对象的话,就可以避免上述错误。
第一种:private static volatile SingletonDouble singleton; //就是在这里加一个关键字,其他不变,起到防止指令重排的作用
第二种(更好用的单例模式):
package test0930;
public class SingletonInnerClass {
// 私有化构造函数
private SingletonInnerClass() {
}
// 给出一个公共的静态方法返回一个单一实例
public static SingletonInnerClass getInstance() {
SingletonInnerClass singleton = Singleton.singleton;
return singleton;
}
// 一个静态内部类
private static class Singleton {
static SingletonInnerClass singleton = new SingletonInnerClass();
}
}
这种方法利用静态内部类,来初始化实例对象,避免了多线程导致的并发,因为一个类的静态成员,实在类的初始化的时候就已经创建好的,并且jvm会自动同步这个初始化过程,不会在初始化过程中其他线程抢占的情况,也由于只初始化一次,那么就是单例的。通常单例通常就是最后两种,其中最后一种更加简洁一点,建议后一种。