网上有那么多的资料,我们为什么还要费时费力的去写博客呢?我想应该是要写出自己本身对一个问题的看法,否则真的没必要写,
直接转载别人的就可以了。以前总是有点应付的感觉,对自己的这样的行为真的很惭愧。话不多说,直入正题。
单例模式也是被用的次数最多的模式之一,接下来我们说下单例模式最简单的一种写法:
class Singleton1{
private static Singleton1 singleton1=new Singleton1();
private Singleton1(){
}
public static Singleton1 getInstance(){
return singleton1;
}
}
这并不是单例模式的标准形式,可以根据自己需要来写。从上面不难看出:
1:一个私有的方法,防止对象再次被实例化。
2:一个静态的方法返回一个被实例化的对象,用来执行一些方法。注意这个方法是静态的哦,否则就需要实例化去执行这个方法,而
单例模式是不能再次实例化的。
为什么叫单例模式呢?
这大概是因为在程序运行的过程中,保证只有一个实例存在。
上述形式虽然完成了我们的需要,但从另一个角度上来说,如果我们访问了这个类的任何一个其他的静态域,那么会出现什么后果呢?
这就会导致所有的静态的变量被加载,这可能导致我们也许从来就没有使用过这个变量,白白浪费资源。
既然可能白白的浪费资源,那么就对它稍作调整。
class Singleton1{
private static Singleton1 singleton1=null;
private Singleton1(){
}
public static Singleton1 getInstance(){
if(singleton1==null){
singleton1=new Singleton1();
}
return singleton1;
}
}
也许你觉得这已经很完美了,但是,对于考虑并发的情况下,这种情况还真的实用么?
首先我们需要说的是java实例化一个对象的步骤:
1:分配内存空间
2:初始化构造器
3:将内存地址指向分配的内存空间
这是一个jvm初始化对象的过程,2,3的部分的顺序是可能会改变的,毕竟设计jvm的时候会考虑到优化,而优化算法是会对这个过程的
指令进行理论上的改变,以达到期望的效率。
这就让我们想到上面的问题?
假如有100个人进行登录操作,调用配置文件读取,也就是对单例的访问,我们能够保证只有一个实例被创建了么?
下面就简单说一下:
for(int i=0;i<100;i++){
new Runnable() {
public void run() {
Singleton1.getInstance();
}
};
}
现在我们第一个线程来了,检测到singleton1为null,进行创建类创建实例,然而在创建实例未完成之前,第二个线程来了,然而他依然检测到singleton1为null值
在这种情况下,我们不能够保证只有一个实例被创建出来,那么我们的单例就出错了。
既然这样写不是线程安全的,那么我们将要面对这种情况怎么办?
首先,大家想到的是进行加锁,进行同步代码块,毕竟锁机制本来的目的是为了同步代码而来的。
然后我们就有了另一种写法:
class Singleton1{
private static Singleton1 singleton1=null;
private Singleton1(){
}
public static synchronized Singleton1 getInstance(){
if(singleton1==null){
singleton1=new Singleton1();
}
return singleton1;
}
}
这种方法的弊端显而易见,我们要是有有1000个线程,不能老是等一个线程全部判断完吧,这样我们的效率会低到一个无法接受的地步吧
所以这种写法我们不能接受,我们就有了另一种写法:
public static synchronized Singleton1 getInstance(){
if(singleton1==null){
synchronized(Singleton1.class){
if(singleton1==null){
singleton1=new Singleton1();
}
}
}
return singleton1;
}
第一次判断是否为null值是可以理解的,但是为什么还要判断null值呢?
这就要看我们的过程呢?我们的线程1,2,3,线程1,2都执行到了第一次判断null之后,接着线程1去实例化对象,实例化完了,线程2进去了,然而线程2不知道
我们已经实例化完了,他会一股脑再进行另一次实例化,而线程3反而可以直接判断不为null,直接跳过加锁的同步化代码,这样我们即提升了效率又避免了错误,这样很好
我也是这么认为为的,但是我们前面说了jvm的实例化对象的过程,在这种情况下,我们真的能保证不会有问题么?
如果jvm的实例化对象和1,2,3的步骤顺序一致,那么这样是对的,但是我们不能保证是这个顺序,也许顺序变成了1,3,2,如果顺序是1,3,2的情况,那么会导致
什么样的状况呢?
这可能导致我们提前将栈中的地址指向了堆中的对象,就会导致等待的线程2误认为线程1已经实例化对象完成,然而并没有进行初始化构造的过程,直接导致了我们的线程2返回的是没有初始化构造的对象。这时我们会问,难道就没有理论上没有问题的单例么?这也太操蛋了吧。
当然有了,如果没有,我们的单例模式还有意义?
下面就说下我们的最终模式:
public class Singleton {
private Singleton(){}
public static Singleton getInstance(){
return SingletonInstance.singleton;
}
private static class SingletonInstance{
protected static Singleton singleton = new Singleton();
}
}
利用内部类的构造,我们能规避上述的问题,既能解决了效率问题,又能同步代码问题,那么难道我们用加锁机制不能完成最终操作么?
这个当然是能的,我们能考虑到的问题,java的开发者当然能考虑到,在jdk1.5之后就有了volatile,这个功能也相当于取消了jvm的实例化时指令优化取消,只要我们在对象上加上这个关键字,那么我们的实例化操作的1,2,3就会按照这个顺序被强迫执行完,不过在jdk1.5以后才有用。
这篇文章想了很久,也参考了很多资料,对我的许多理解帮助甚大,希望对正在学习的人有帮助。当然有什么错误也请和我说,技术的路上达者为师。