Java设计模式之单例模式

1、使用场景

所谓单例模式,就是只可能存在唯一一个类的实例,不能再多了。单例模式可以说是Java设计模式中最简单但也是最常用的一种了。在Android开发中也同样如此,当App需要一个全局的、跟Application同生命周期的服务或者需要统一管理调度某种资源等情况,我们通常会编写一个单例实现的类,达到统一资源服务入口的目的,同时也可以减少不必要的资源开销,提升App性能。Android源码中,也有很多单例模式的使用。因此,学习好单例模式非常重要。

2、实现方式

单例模式有很多种实现方式,基本思想是内部提供一个static方法返回类的一个唯一实例,同时只设计一个private类型构造函数,屏蔽外部创建类实例的入口,以此保证类的实例最多只有一个。下面简单总结分析下。

  • 饿汉模式
public class SingletonTest {
    private SingletonTest (){}
    private final static SingletonTest instance = new SingletonTest();
    public static SingletonTest instance(){
        return instance;
    }
}

利用Java语言的特性,static类型变量在类加载时就会被初始化,从而保证了有且只有一个类实例。但是,这种写法可能会造成资源浪费,因为即使程序中没有使用到该类方法,也会创建该类实例;并且,如果该类初始化加载操作过多,也会延长程序的启动,影响用户体验。

  • 懒汉模式
public class SingletonTest {
    private SingletonTest (){}
    private static SingletonTest instance;
    public static SingletonTest instance(){
        if (instance == null) {
            instance = new SingletonTest();
        }
        return instance;
    }
}

这种写法解决了上面的问题,在第一次使用该类时,才会去创建该类实例。但是,却引入了另一个问题,在多线程环境下,并不能保证只有一个类实例。于是很自然有了下面这种写法。

  • 线程安全模式
public class SingletonTest {
    private SingletonTest (){}
    private static SingletonTest instance;
    public static synchronized SingletonTest instance(){
        if (instance == null) {
            instance = new SingletonTest();
        }
        return instance;
    }
}

synchronized关键字很好的保证了多线程环境下也能只有一个类实例,但是在高并发情况下效率却是很低的,每次获取类实例都得锁住整个类,严重影响其他线程的操作。我们希望只有在实例没有创建时才去加锁保证只创建一次,其他情况下,应该直接返回类实例即可,没有必要再加锁,提升性能。

  • Double-Checked Locking
public class SingletonTest {
    private SingletonTest (){}
    private static SingletonTest instance;
    public static SingletonTest instance(){
        if (instance == null) {
            synchronized (SingletonTest.class){
                if(instance == null) {
                    instance = new SingletonTest();
                }
            }
        }
        return instance;
    }
}

Double-Checked Locking写法可以说基本做到了上面的要求。但是,这还不够完美。instance = new SingletonTest()这条语句并不是原子性的,大体包括三个操作:
(1)为new操作分配内存空间
(2)执行构造函数,创建SingletonTest类实例
(3)将instance引用指向分配的内存地址
JVM或者Android 的Dalvik VM并不保证上述(2)、(3)两步操作的顺序,也就是说有可能instance不为null了,但是指向的内存对象还未完全初始化成功,这时其他线程直接取走instance使用就有可能出现问题。在Java5之后,可以使用volatile关键字,保证instance每次都从主内存读取,从而一定程度上保证单例实现的正确性。

  • 静态内部类实现
public class SingletonTest {
    private SingletonTest (){}
    public static SingletonTest instance(){
        return InnerClass.instance;
    }    
    private static class InnerClass{
        private final static SingletonTest instance = new SingletonTest();
    }
}

静态内部类中的instance实例并不会在SingletonTest类加载时初始化,而是只有在InnerClass第一次使用时才去创建,类似于懒汉模式延迟了加载,但是也保证了多线程安全,是一种比较优秀的实现方式。

  • 枚举实现
public enum SingletonTest {
    INSTANCE;
}

简单几行代码便解决了问题,并且不会出现多线程安全问题。经典的《Effective Java》一书中推荐这种实现方式。

3、进阶

上述单例的各种实现,真的就能保证只存在唯一实例了吗?我们考虑以下两种情况。

  • Java反射实例化

作为一名Java程序员,Java自带的黑科技——反射,应该派上用场了。使用如下代码,对上面几种单例实现(除了枚举实现)逐一进行测试。

public static void main(String[] args) throws Exception {
    Constructor c = SingletonTest.class.getDeclaredConstructor();
    c.setAccessible(true);
    SingletonTest test = (SingletonTest) c.newInstance();
    System.out.println(instance() == test);
}

不出所料,输出结果均为false,表明我们创建了唯一实例之外类的另一个实例。

  • Java序列化反序列化

Java序列化是将object转换成字节序列,反序列化是将字节序列恢复成内存对象。通常反序列化会重新创建类实例,即使是单例类。

public class SingletonTest implements Serializable {
    private SingletonTest (){}
    public static SingletonTest instance(){
        return InnerClass.instance;
    }
    private static class InnerClass{
        private static SingletonTest instance = new SingletonTest();
    }

    public static void main(String[] args) throws Exception {
        // 序列化
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("file.txt"));
        outputStream.writeObject(instance());

        // 反序列化
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("file.txt"));
        SingletonTest test = (SingletonTest) inputStream.readObject();
        System.out.println(test == instance());
    }
}

输出结果仍然为false,同反射一样,反序列化也重新创建了类实例。但是,不同于对于反射攻击的无计可施,我们可以使用readResolve方法指定反序列化返回的对象,从而保证单例的唯一性。例如:

private Object readResolve() throws ObjectStreamException {
    return instance();
}

需要注意的是上面的讨论都避开了枚举实现方式,为什么呢?借用《Effective Java》里的一段关于枚举实现的解释。

无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值