单例模式构造的目的:尽可能的节约内存空间,减少无谓的GC消耗,并且使应用可以正常运作。
这些类,在整个应用中,同一时刻,有且只能有一种状态。
最原始的单例模式的构造方式(也成饱汉式),不考虑并发访问的情况下标准的单例模式的构造方式。
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时才调用构造方法创造一个实例,否则就直接返回。
但是上面的单例模式,是有可能创建出多个实例的。情况是当一个线程执行到判断实例为空时,进入到if语句块内部开始创建实例,这个时候另一个线程也判断了实例为空,也进入到if语句块内部创建实例。
这个时候,一个简单粗暴的方法是给获取实例的方法加上synchronized关键字,保证同一时间内只有一个线程执行该方法,其他线程访问该方法时挂起。但是这样会造成很多无谓的等待时间,所以在代码块上加上同步锁是更好的方案。
public class SynchronizedSingleton {
//一个静态的实例
private static SynchronizedSingleton synchronizedSingleton;
//私有化构造函数
private SynchronizedSingleton(){}
//给出一个公共的静态方法返回一个单一实例
public static SynchronizedSingleton getInstance(){
if (synchronizedSingleton == null) {
synchronized (SynchronizedSingleton.class) {
if (synchronizedSingleton == null) {
synchronizedSingleton = new SynchronizedSingleton();
}
}
}
return synchronizedSingleton;
}
}
值得注意的是在同步块中,我们再次判断了synchronizedSingleton是否为null,解释下为什么要这样做。
假设我们去掉同步块中的是否为null的判断,有这样一种情况,假设A线程和B线程都在同步块外面判断了synchronizedSingleton为null,结果A线程首先获得了线程锁,进入了同步块,然后A线程会创造一个实例,此时synchronizedSingleton已经被赋予了实例,A线程退出同步块,直接返回了第一个创造的实例,此时B线程获得线程锁,也进入同步块,此时A线程其实已经创造好了实例,B线程正常情况应该直接返回的,但是因为同步块里没有判断是否为null,直接就是一条创建实例的语句,所以B线程也会创造一个实例返回,此时就造成创造了多个实例的情况。
但如果再进一步深入考虑的话,这样其实仍然是有问题的。
如果我们深入到JVM中去探索上面这段代码,它就有可能(注意,只是有可能)是有问题的。
因为虚拟机在执行创建实例的这一步操作的时候,其实是分了好几步去进行的,也就是说创建一个新的对象并非是原子性操作。在有些JVM中上述做法是没有问题的,但是有些情况下是会造成莫名的错误。
首先要明白在JVM创建新的对象时,主要要经过三步。
1.分配内存
2.初始化构造器
3.将对象指向分配的内存的地址
这种顺序在上述双重加锁的方式是没有问题的,因为这种情况下JVM是完成了整个对象的构造才将内存的地址交给了对象。但是如果2和3步骤是相反的(2和3可能是相反的是因为JVM会针对字节码进行调优,而其中的一项调优便是调整指令的执行顺序),就会出现问题了。
因为这时将会先将内存地址赋给对象,针对上述的双重加锁,就是说先将分配好的内存地址指给synchronizedSingleton,然后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,会认为synchronizedSingleton对象已经实例化了,直接返回一个引用。如果在初始化构造器之前,这个线程使用了synchronizedSingleton(即实例还未构造完成就使用),就会产生莫名的错误。
所以我们在语言级别无法完全避免错误的发生,我们只有将该任务交给JVM,所以有一种比较标准的单例模式。
package com.oneinstance;
public class InnerClassSingleton {
public static Singleton getInstance(){
return Singleton.singleton;
}
private static class Singleton{
protected static Singleton singleton = new Singleton();
}
}
一个类的静态属性只会在第一次加载类时初始化,这是JVM帮我们保证的,所以我们无需担心并发访问的问题。所以在初始化进行一半的时候,别的线程是无法使用的,因为JVM会帮我们强行同步这个过程。另外由于静态变量只初始化一次,所以singleton仍然是单例的。
上面这种写法是我们使用静态的内部类作为单例,这样不太符合我们的习惯。所以我们改为以下形式。
public class Singleton {
private Singleton(){}
public static Singleton getInstance(){
return SingletonInstance.instance;
}
private static class SingletonInstance{
static Singleton instance = new Singleton();
}
}
最终的产物就是如上述的形式。上述形式保证了以下几点。
1.Singleton最多只有一个实例,在不考虑反射强行突破访问限制的情况下。
2.保证了并发访问的情况下,不会发生由于并发而产生多个实例。
3.保证了并发访问的情况下,不会由于初始化动作未完全完成而造成使用了尚未正确初始化的实例。
有一种不常用的方式:
与双重锁定一模一样,只是给静态的实例属性加上关键字volatile,标识这个属性是不需要优化的。
//一个静态的实例
private volatile static SingletonSyn synchronizedSingleton;
这样也不会出现实例化发生一半的情况,因为加入了volatile关键字,就等于禁止了JVM自动的指令重排序优化,并且强行保证线程中对变量所做的任何写入操作对其他线程都是即时可见的。这里没有篇幅去介绍volatile以及JVM中变量访问时所做的具体动作,总之volatile会强行将对该变量的所有读和取操作绑定成一个不可拆分的动作。如果读者有兴趣的话,可以自行去找一些资料看一下相关内容。
不过值得注意的是,volatile关键字是在JDK1.5以及1.5之后才被给予了意义,所以这种方式要在JDK1.5以及1.5之后才可以使用,但仍然还是不推荐这种方式,一是因为代码相对复杂,二是因为由于JDK版本的限制有时候会有诸多不便。
还有一种饿汉式
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}
最主要的缺点就是一旦我访问了Singleton的任何其他的静态域,就会造成实例的初始化,而事实是可能我们从始至终就没有使用这个实例,造成内存的浪费。
不过在有些时候,直接初始化单例的实例也无伤大雅,对项目几乎没什么影响,比如我们在应用启动时就需要加载的配置文件等,就可以采取这种方式去保证单例。
共同学习,写下你的评论
评论加载中...
作者其他优质文章