Java深入学习之单例模式

[size=x-large][b]Java设计模式自学之单例模式[/b][/size]

对于单例模式来说,最重要的就是私有构造函数,提供静态的实例化方法,所以单例模式的几个关键字:private 的构造函数,public static 提供的实例化方法,private static 的实体类成员变量,只要满足这三个基本的要素,就能实现单例模式。

[size=x-large]1、懒汉模式[/size]

懒汉模式是最基本的单例模式之一,满足最基本的单例模式条件

 public class Single1 {

private static Single1 single = null;
//私有构造函数
private Single1(){

}
//提供实例化方法
public static Single1 getInstance(){
if (null == single) {
single = new Single1();
}
return single;
}
}

以上代码就实现的懒汉单例模式,在初始化时不会创建对象实例,只要当getInstance被调用时才会创建。懒汉嘛,就是不到最后一刻不去真正做事,就是形容开始时不创建,要用的时候才创建实例。这种方式降低了初始时的内存空间,但后续的调用都需要判断的时间。

上面的懒汉模式在多线程下会出现安全问题,线程安全就是多个线程同时访问会出现安全性问题。比如一个房间,只有第一个进入的人才能留下自己的签名,其他人发现房间里有签名时,就不能再签名了。但如果两个人同时进门,两者发现房间里面没签名,都可以进入房间留下签名,就不符合房间只能存在一个签名的场景,所以这种情况下就需要给房间加锁。

[size=large]懒汉模式变种一[/size]

这种情况是直接给房间门上把锁,第一个进入的人锁住门,后面的人就无法在他签名时再次进入房间。不算有多少人同时到达,但是只有一个人能拿到锁,所以不会存在线程安全问题。

public class Single2 {

private static Single2 single = null;

private Single2(){

}

public static Single2 getInstance(){
//使用锁关键字
synchronized(Single2.class){
if (null == single) {
single = new Single2();
}
}
return single;
}
}


这种加锁方式与在getInstance方法上加锁效果是一样的。但是这种方式有很大的效率问题,就是外面的人,都需要等里面的人把门打开,才能发现房间里是不是有签名了,因为门被锁住时,大家不知道里面的人会不会真的留下签名,如果人一旦过多,就会等待(Java跟人不同,不会因为房间被锁住就退走,而是继续等待),其实大家只需要看下里面是否有签名,而看签名的时间比起锁门再解锁的时间根本不是一个层级上的,这样就造成效率过低。

[size=large]懒汉模式变种二[/size]

另外一种方式就是给房间加上一扇窗户。大家进入房间时,先通过窗户看下,里面有没有签名,有的话直接退走,没有的话就继续往门方向走。由于窗户是大家都可以看的,需要等待的只有在通过窗户发现里面没签名的人才会继续往前走,比起上一种人人都需要等待时间消耗少非常多。

public class Single3 {

private static Single3 single = null;

private Single3(){

}

public static Single3 getInstance(){
//先判断是否为空,相当于一扇窗户
if (null == single) {
synchronized(Single3.class){
if (null == single) {
single = new Single3();
}
}
}
return single;
}
}


这种方式也叫做双重锁校验机制。理论上这种方式是完全没问题的,但是由于Java虚拟机加载机制的问题,在JDK1.5之后,需要给成员变量添加一个[color=red]volatile[/color]关键字,它可以保证变量的可见性和有序性,被它修饰的变量的值,不会被本地线程存储,所有对其修饰变量的操作都是直接共享内存,保证多个线程可以同时可见。

即:[b][color=red]private volatile static Single3 single = null,[/color][/b]这样才能完整的保证这种方式是在任何情况下都是具备正确性的。

补充:Java虚拟机加载过程主要可以分为三个步骤:装载、连接和初始化

1、装载阶段:就是将java文件对应的.class文件以二进制数据加载到JVM中,加载的实例和类位于堆中,然后创建一个java.lang.Class对象来封装类信息数据结构,而类信息则被放到方法区中。以“类的全限定名+ClassLoader实例ID”来标明这些类,这里也会涉及到[color=green]“双亲委派模型”[/color]机制。

2、连接阶段:这个阶段分为三个步骤,
[list]
[*] 步骤一:验证,验证这个class文件里面的二进制数据是否符合java规范,并且符合当前JVM;
[*] 步骤二:准备,为该类的静态变量分配内存空间并赋值为默认值;
[*] 步骤三:解析,将类的常量池中的符号引用解析为直接引用,也可以在用到相应的引用时再解析。
[/list]

3、初始化:初始化类中的静态变量,并执行类中的static代码、构造函数。初始化顺序:
[list]
[*]1. 为静态变量分配内存并赋值或者执行静态代码块;
[*]2. 为非静态属性分配内存并赋值;
[*]3. 构造方法;
[*]4. 执行非静态代码块 或 静态方法(都是调用了才加载)。
[/list]
在JVM中存在一个很大的问题就是加载过程并不是时时都是有序的,内存模型中允许存在“无序写入”。比如:single = new Single3();这段代码就不是原子性操作,在JVM处理时大概可以分为三步。
* 第一步,给Single3分配内存;
* 第二步,初始化Single3的构造器;
* 第三步,将single对象执行已经分配内存空间Single3**(此时,single已经不是null,而是有空间的内存)**;

但是由于该语句并不是原子操作,所以这三步执行在JVM实际的顺序可能是1,3,2这样执行,所以,如果线程B执行到第一个if(null == single)时而线程A恰好是在1,3,2中的3时,线程B拿到的也不是一个非null的对象,而是一个没有值得内存空间,导致直接返回,但是实际上是没有数据的,从而造成了线程安全的问题。

[size=x-large]2、饿汉模式[/size]
当然,如果在懒汉模式中初始化成员变量是直接就进行赋值,是什么情况呢

public class Single4 {

private static Single4 single = new Single4();

private Single4(){

}
public static Single4 getInstance(){
return single;
}
}


在类初始化的时候,按照JVM初始化方式,[color=red]Single4 single = new Single4()[/color] 在类初始时就会被实例化,但是可能等到程序结束也不会被调用,所以这种方法称为“饿汉模式”。这种方式根据JVM本身的特性,不会存在线程安全问题,但是在初始化时就会占据内存空间。

[size=x-large]3、内部类方式[/size]
将饿汉模式和懒汉模式总体结合归纳下,两种都有一定的利弊性,而另外一种方式完美的融合两种方式的问题。

public class Single5 {

/**
* 加载类时,内部类实例化与外部类没有绑定关系,
* 所以只有在调用时才会加载,实现延迟加载
* 而且内部类初始化时就实例化了变量,并且只有一次,保证了线程安全
*/
private static class Instance{
private static Single5 single = new Single5();
}

public static Single5 getInstance(){
return Instance.single;
}

}


这种方式即实现了延迟加载,也保证了线程安全,所以是用得最多的一种方式。

[size=x-large]4、单例方法屏蔽[/size]
在使用单例模式中,可以通过其他途径创建其他实例:
第一种;通过反射构造单例对象,反射时可以使用setAccessible方法来突破private的限制,获取到新的实例,而打破单例模式;

public static Single5 refCopy() throws Exception{
//通过反射获取构造函数
Constructor con = Single5.class.getDeclaredConstructor();
con.setAccessible(true);
//获取实例
Single5 refTest = con.newInstance();
return refTest;
}



第二种;通过反序列化构造单例对象。

/** 
* 序列化克隆
* @return
* @throws Exception
*/
public static Single5 deepCopy() throws Exception{
ByteArrayOutputStream os = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(getInstance());

InputStream is = new ByteArrayInputStream(os.toByteArray());
ObjectInputStream ois = new ObjectInputStream(is);
Single5 test = (Single5) ois.readObject();
return test;
}


测试代码:

Single5 test = Single5.getInstance();
Single5 test1 = Single5.getInstance();

Single5 test3 = Single5.deepCopy();

Single5 refTest = Single5.refCopy();

System.out.println(refTest.equals(test));
System.out.println(test == test1);
System.out.println(test3.equals(test1));


测试结果分别为false,true,false;由此可见,以上两种方法都能破坏单例模式。所以,要完完全全的实现单例模式,必须需要进行一些完善,比如序列化时,添加readResolve方法,返回获取的instance对象;而反射,则需要跟多的权限处理等。
 public Object readResolve(){
return getInstance();
}

你可能感兴趣的:(设计模式)