设计模式之单例模式

浅析java中的单例设计模式

单例设计模式在JavaEE中应用十分广泛,例如spring中的javaBean,由于经常碰到这种设计模式(通常会与工厂模式一起来用),,因此对其进行进一步的了解是有十分必要的,这里是我个人在学习过程中的一些总结。

首先单例模式又可以细分为好几种:懒汉式单例模式、饿汉式单例模式、静态内部类单例模式、枚举单例模式、双重锁检测单例模式,这里我就只重点总结了前三种单例模式,对于后两种没有过多的研究,不过基本上前两种设计模式已经足够大家在平时学习和工作中使用了,同时也会顺带说所如何用反射和反序列化来破解单例模式(不包含枚举单例模式)以及如何避免这种破解。

懒汉式单例模式,首先代码如下:

/**
 * 懒汉式单例模式,具有延时加载的优势
 * @author LiuJinkun
 *
 */
public class SingleTon {
	
	//私有的构造器
	private SingleTon(){}
	
	private static SingleTon instance=null;
	
	public static synchronized SingleTon getInstance(){
		if(instance==null){
			instance=new SingleTon();
		}
		return instance;
	}

}

懒汉式单例模式具有如下特点:

①首先必须将其构造器私有化(private),这样才能在其他类中不能通过new来创建对象;

②需要有一个私有的静态的成员变量(private和static)instance,并让其等于null(顾名思义,懒加载就是懒得加载,只有用的时候才加载,因此只有让其等于null了,才能称之为懒加载);

③需要提供一个公共的静态方法,并返回本类的实例,同时关键点是需要有static修饰,因为该类不能用new来创建实例,因此需要用static来修饰,调用时采用SingleTon.getInstance()才能返回单例,还需要有synchronized来保证线程安全,否则的话讲不能保证单例(关于为什么使用synchronized,读者可以自行去看看线程有关的知识,后续我也会在博客里写一些关于线程的知识总结)。

这样就实现懒汉式单例模式,正所谓懒汉式正是因为懒,所以具有延时加载的优势,在不着急使用该对象时,可以使用懒汉式的单例模式。


接下来再来看看饿汉式单例模式,首先代码如下:

package com.tiantang.singleton;

/**
 * 饿汉式单例模式,不具备延时加载
 * @author LiuJinkun
 *
 */
public class SingleTon2 {
	//私有的静态成员变量,然后直接赋值
	private static SingleTon2 instance=new SingleTon2();
	//私有的构造器
	private SingleTon2(){}
	//对外开放的静态方法
	public static SingleTon2 getInstance(){
		return instance;
	}

}

饿汉式单例模式具有如下特点:

①:私有的构造器,与懒汉式一样;

②:私有的静态成员变量,在定义该成员变量时,直接给其赋上值,因此成为是饿汉式(可以理解成比较饥饿,上来就要吃)

③:一个公共的静态方法,使其对外返回本类的实例对象

饿汉式单例模式不具有延时加载的优点。

通过对比懒汉式单例模式和饿汉式单例模式,相信大家很容易理解这两种单例模式。不过看到这里,有的可能会有疑问:前面提到了懒汉式中加了synchronized关键词,因此懒汉式是线程安全的,那饿汉式中为什么没加synchronized?它还是线程安全的吗?在这里可以肯定的告诉大家,饿汉式中不加synchronized也是线程安全的。请看这行代码:

private static SingleTon2 instance=new SingleTon2();
在JVM虚拟机加载类SingleTon2时,会加载该行代码,而在加载该行代码时是天然的线程安全的(天然的是指虚拟机在加载类时会保证线程安全)。

静态内部类单例模式,首先代码如下:

package com.tiantang.singleton;

/**
 * 静态内部类实现单例模式,线程安全,同时具有延时加载(好于懒汉式)
 * 
 * 实现原理:外部类没有定义一个static变量,因此不会出现在加载外部类时就初始化实例
 * 而在静态内部类里面定义了一个static和final修饰的外部类类型的常量,一经加载,
 * 就不会被改变。而在外部类的getInstance()的方法里面,使用到了静态内部类,因此
 * 此时才会开始加载和初始化静态内部类,从而初始化instance,因此实现了延时加载
 * 
 * @author LiuJinkun
 *
 */
public class SingleTon3 {
	
	private static class InnerSingleTon{
		private static final SingleTon3 instance=new SingleTon3();
	}
	
	private SingleTon3(){}
	
	public static SingleTon3 getInstance(){
		return InnerSingleTon.instance;
	}

}

静态内部类单例模式的特点:

①:与懒汉式和饿汉式不同,没有定义一个私有的静态的成员变量,而是定义了一个静态内部类,在静态内部类里定义了一个常量;

②:构造器私有化;

③:对外开放的静态方法,用来返回本类的实例对象,显示原理是:类加载器在加载SingleTon3类时不会加载内部类InnerSingleTon(记住:类只有在使用时才会被加载),,而在方法getInstance()里面才用到了内部类,因此此时才会加载内部类,从而达到了延时加载的效果,同时也是线程安全的。


双重锁机制单例模式:

也能实现单例模式,但有时会出问题,具体的我也不太懂,读者如果感兴趣,可以自己去翻阅一些书籍去了解一下。

枚举单例模式:

枚举实现单例模式不具备延时加载,但实现起来很简单,但笔者不知道具体的实现原理(笔者对枚举的理解不太深刻),因此就不在这里细说了,但枚举实现的单例模式是不能被反射和反序列化破解的,其他的集中单例模式能被破解(当然也有相应的解决方案)。

如果在使用过程中需要延时加载时,静态内部类单例模式效率比懒汉式效率高;不需要延时加载时,枚举式比饿汉式效率高。读者可以根据具体的情况而选择,通常懒汉式和饿汉式基本上就满足平时的需求了(笔者目前基本上就碰到过这两种,可能是笔者的经历太少)。

如何利用反射和反序列化来破解单例模式(不包括枚举式单例模式)

先用反射来破解单例模式,测试代码如下(以懒汉式单例模式为例,其他几种单例模式与这一样):
懒汉式单例模式如下:
package com.tiantang.singleton;

/**
 * 懒汉式单例模式,具有延时加载的优势
 * @author LiuJinkun
 *
 */
public class SingleTon {
	
	//私有的构造器
	private SingleTon(){}
	
	private static SingleTon instance=null;
	
	public static synchronized SingleTon getInstance(){
		if(instance==null){
			instance=new SingleTon();
		}
		return instance;
	}

}
测试的代码如下:
package com.tiantang.singleton;

import java.lang.reflect.Constructor;

public class Test {
	
	public static void main(String[] args) throws Exception {
		SingleTon s1=SingleTon.getInstance();
		SingleTon s2=SingleTon.getInstance();
		System.out.println(s1==s2);//输出结果为true
		
		//测试反射破解单例模式
		Class clazz=Class.forName("com.tiantang.singleton.SingleTon");
		Constructor c=(Constructor) clazz.getDeclaredConstructor(null);//得到无参构造器对象
		c.setAccessible(true);//由于构造器是私有的,因此需要手动将其访问权限设置为true
		SingleTon s3=(SingleTon)c.newInstance(null);
		System.out.println(s3==s2);//输出结果为false
		
	}

}
运行代码,打印结果发现,与我们预期的单例模式结果不一致了。也就是我们可以利用反射将这种单例模式破解掉了,那么如何防止反射破解呢?
现在修改单例模式代码如下:
package com.tiantang.singleton;

/**
 * 测试懒汉式反射破解
 * @author LiuJinkun
 *
 */
public class SingleTon{
	
	private static SingleTon instance=null;
	
	private SingleTon(){
		if(instance!=null){
			throw new RuntimeException();//只新增加了这一行代码,如果instance不为空了,再想用反射得到构造器创造对象时就抛出异常
		}
	}
	
	public static SingleTon getInstance(){
		if(instance==null){
			instance=new SingleTon();
		}
		return instance;
	}

}

通过比较原来的代码会发现,只是修改了构造器中的代码,当想利用反射获取构造器,利用构造器创建对象时,如果instance已经初始化了,就会抛出异常,从而防止了反射破解单例模式。

现在再来看反序列化来破解单例模式:

单例模式的代码:
package com.tiantang.singleton;
import java.io.Serializable;
/**
 * 测试懒汉式反序列化破解
 * @author LiuJinkun
 *
 */
public class SingleTon implements Serializable{
	
	private static SingleTon instance=null;
	
	private SingleTon(){
		if(instance!=null){
			throw new RuntimeException();
		}
	}
	
	public static SingleTon getInstance(){
		if(instance==null){
			instance=new SingleTon();
		}
		return instance;
	}

}

破解的测试代码:
package com.tiantang.singleton;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;

public class Client {
	
	public static void main(String[] args) throws Exception {
		SingleTon s1=SingleTon.getInstance();
		SingleTon s2=SingleTon.getInstance();
		System.out.println(s1==s2);//结果为true

		
		//测试反序列化破解单例模式
		OutputStream os=new FileOutputStream("d:a.txt");
		ObjectOutputStream oos=new ObjectOutputStream(os);
		oos.writeObject(s1);//将对象s1写到磁盘中
		oos.close();
		os.close();
		InputStream in=new FileInputStream("d:a.txt");
		ObjectInputStream ois=new ObjectInputStream(in);
		SingleTon s3=(SingleTon) ois.readObject();//将对象从磁盘中读取出来
		System.out.println(s3==s1);//结果为false
		ois.close();
		in.close();
		
	}

}

从测试的结果来看,发现从磁盘中读取的对象与原对象不相同了,说明单例模式又被破解了,那么如何防止反序列化破解单例模式呢?

现修改SingleTon类的代码,修改后的代码如下:

package com.tiantang.singleton;

import java.io.Serializable;

/**
 * 测试懒汉式的反序列化破解
 * @author LiuJinkun
 *
 */
public class SingleTon implements Serializable {
	
	private static SingleTon instance=null;
	
	private SingleTon(){
		if(instance!=null){
			throw new RuntimeException();
		}
	}
	
	public static SingleTon getInstance(){
		if(instance==null){
			instance=new SingleTon();
		}
		return instance;
	}
	
	/**
	 * 对象在被反序列化时,该方法会被执行
	 * @return
	 */
	private Object readResolve(){
		return instance;
	}

}

与修改之前的代码相比较,读者会发现只在SingleTon类里面增加了一个方法readResolve()方法,这是因为在进行反序列化时,虚拟机会自动执行该方法,因此我们只需要在该方法里面返回原本的instance实例即可,从而防止了反序列化的破解,同时在构造器里面增加的代码也防止了反射的破解。
以上内容就是笔者在学习过程中关于单例设计模式的总结,希望对读者们有所帮助,欢迎读者们提出宝贵的意见以及指出其中的不足,同时也是为了今后的回顾留下一点笔记。



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