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

一、简介

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

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

二、适用场合

  • 需要频繁的进行创建和销毁的对象;
  • 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
  • 工具类对象;
  • 频繁访问数据库或文件的对象

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

要求:

(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。所以总的来说,用枚举实现单例是比较安全且简单的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值