Java设计模式之单例模式

来源:互联网 发布:整数划分问题c语言 编辑:程序博客网 时间:2024/05/22 08:24

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》里的一段关于枚举实现的解释。

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

0 0