您的位置:首页 > 编程语言 > Java开发

设计模式之一 单例设计模式

2016-05-11 10:00 351 查看
一、简介

单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。

许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

二、适用场合

需要频繁的进行创建和销毁的对象;

创建对象时耗时过多或耗费资源过多,但又经常用到的对象;

工具类对象;

频繁访问数据库或文件的对象

资源管理器,回收站,打印机资源,线程池,缓存,配置信息类,管理类,控制类,门面类,代理类通常被设计为单例类

要求:

(1)单例类只能有一个实例

(2)单例类必须自己创建自己的唯一实例

(3)单例类必须给其他对象提供这一实例

三、单例实现方式

基本思路:

1、有一个私有的实例对象,对象是唯一的,用final修饰

private final static Singleton singleton= new  Singleton();


2、构造函数私有化,防止通过构造函数生成该类的对象

private Singleton(){}


3、提供函数来获取该唯一的对象,并且不能通过构造函数生成该类对象,只能通过类访问函数,所以是static的,相应的实例对象也是static的

public static  Singleton getInstance(){
return singleton;
}


这样就得到了单例模式饿汉式的写法:

//饿汉式-1
public class Singleton {

private final static Singleton singleton= new Singleton();

private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}


饿汉式的优点是写法简单,在类装载的时候就完成实例化。避免了线程同步问题。

缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

另一种饿汉式的变体是采用静态代码块的方法

//饿汉式-2
public class Singleton {

private static Singleton singleton;

static {
singleton= new Singleton();
}

private Singleton() {}

public Singleton getInstance() {
return singleton;
}
}


这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。

如果我们希望单例类可以延迟加载,达到lazy loading,而不是类装载的时候就完成实例化,我们就可以采用懒汉式。

//懒汉式-1  多线程下不可用
public class Singleton {

private static Singleton singleton;

private Singleton(){}
public static Singleton getInstance(){

if(singleton == null){
singleton= new Singleton();
}
return singleton;
}

}
问题:只能在单线程的情况下使用,多线程的时候会生成多个实例。


如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

为了解决多线程下会产生多个实例的问题,我们可以对获取实例进行加锁控制,得到如下变体形式

//懒汉式-2
public class Singleton {

private static Singleton singleton;

private Singleton() {}

public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}


解决上面懒汉式-1的线程不安全问题,做个线程同步就可以了,于是就对getInstance()方法进行了线程同步。

但是这种方法的缺点是效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。既然用同步方法效率太低那么就改用同步代码块的方式,例如如下的方式:

//懒汉式-3
public class Singleton {

private static Singleton singleton;

private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
singleton = new Singleton();
}
}
return singleton;
}
}


但是这种方式仍然会出现懒汉式-1相同的问题,在多线程的情况下仍然会产生多个实例。因此我们可以对上面的代码继续改造,进行双重判断。

//懒汉式-4
public class Singleton {

private static Singleton singleton;

private Singleton(){}
public static Singleton getInstance(){

if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}


Double-Check概念对于多线程开发者来说不会陌生,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。

它的优点是:线程安全;延迟加载;效率较高。

还有一种静态内部类的单例实现方法如下:

//静态内部类实现单例
public class Singleton {

private Singleton(){}
private static class SingletonInstance {
private static final Singleton singleton = new Singleton();
}

public static Singleton getInstance(){
return SingletonInstance.singleton;
}

}


这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。

类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

优点:避免了线程不安全,延迟加载,效率高。

四、单例攻击

4.1 利用java反射机制进行攻击

尽管如此,以上的单例模式实现方式仍然可能受到攻击,可以通过Java反射机制和序列化进行攻击。

public class SingletonAttack {

public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException{
Class<?> classType = Singleton.class;
Constructor<?> c = classType.getDeclaredConstructor(null);
c.setAccessible(true);
Singleton s1 = Singleton.getInstance();
Singleton s2 = (Singleton) c.newInstance();
System.out.println(s1==s2);

}
//得到的结果为false


通过反射获取构造函数,然后调用setAccessible(true)就可以调用私有的构造函数,所有s1和s2是两个不同的对象。如果要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。

public class Singleton {
//注意不要忘记加static,否则不起作用
private  static boolean flag = false;
private  Singleton(){
synchronized(Singleton.class){
if(flag == false){
flag =!flag;
}
else{
throw new RuntimeException("单例模式被攻击");
}
}
}

private static class SingletonInstance {
private static final Singleton singleton = new Singleton();
}

public static Singleton getInstance(){
return SingletonInstance.singleton;
}

}


4.2 利用序列化进行单例模式攻击

//单例类实现Serializable接口
public class Singleton implements Serializable {

private static final long serialVersionUID = 1L;

private  static boolean flag = false;
private  Singleton(){
synchronized(Singleton.class){
if(flag == false){
flag =!flag;
}
else{
throw new RuntimeException("单例模式被攻击");
}
}
}

private static class SingletonInstance {
private static final Singleton singleton = new Singleton();
}

public static Singleton getInstance(){
return SingletonInstance.singleton;
}

}


此时我们利用序列化进行攻击代码如下:

public class SingletonAttack {

public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton s1 = Singleton.getInstance();

FileOutputStream fos = new FileOutputStream("d://a.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.flush();
oos.close();
fos.close();

FileInputStream fis = new FileInputStream("d://a.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton s2 = (Singleton) ois.readObject();
ois.close();
fis.close();
System.out.println(s1 == s2);
}
}
//返回结果为false


如果过该类implements Serializable,那么就会在反序列化的过程中重新创建一个对象。这个问题的解决办法就是在反序列化时,指定反序化的对象实例,可以在单例类中添加readResolve方法。

package com.cmbchina.cc.crs.wjjDemo;

import java.io.Serializable;

public class Singleton implements Serializable {

private static final long serialVersionUID = 1L;

private  static boolean flag = false;
private  Singleton(){
synchronized(Singleton.class){
if(flag == false){
flag =!flag;
}
else{
throw new RuntimeException("单例模式被攻击");
}
}
}

private static class SingletonInstance {
private static final Singleton singleton = new Singleton();
}

public static Singleton getInstance(){
return SingletonInstance.singleton;
}

private Object readResolve(){
return SingletonInstance.singleton;
}

}


添加后再执行序列化攻击的代码执行结果为true。

五、枚举实现单例

除此此外,在JDK1.5中添加了枚举可以用来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化和java反射重新创建新的对象。

public enum Singleton {
INSTANCE;
public void whateverMethod() {

}
}


采用java反射方式创建实例时,会报java.lang.NoSuchMethodException异常。

同样将Singleton类实现Serializable接口,在无需添加readResolve方法的情况下,执行序列化攻击代码返回结果仍然为true。所以总的来说,用枚举实现单例是比较安全且简单的方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  设计模式 java