单例模式

单例模式

标签 : Java与设计模式


经典的《设计模式:可复用面向对象软件的基础》一书归纳出23种设计模式, 23种设计模式又可划分为3类: 创建型模式, 结构型模式, 行为型模式.


创建型模式

社会分工越来越细, 在软件设计方面自然也是如此,对象的创建和使用分开也就成了必然趋势: 如果对象创建会消耗很多系统资源, 那么单独对对象的创建进行研究, 从而高效地创建对象就是 创建型模式 要探讨的问题. 有6个具体的创建型模式可供研究,它们分别是: 单例模式工厂模式(简单工厂/工厂方法/抽象工厂)建造者模式原型模式.


单例模式

保证一个类只有一个实例, 并提供一个访问他的全局访问点.

  • 场景:
    • Windows任务管理器;
    • 文件系统: 一个操作系统只能有一个文件系统;
    • 数据库连接池;
    • Spring: 一个Component就只有一个实例;
    • JavaWeb: 一个Servlet只有一个实例;

实现

常见的单例模式实现方式有五种: 饿汉式, 懒汉式, 双重检测锁, 静态内部类, enum枚举.

  • 实现要点:

    • 隐藏构造器
    • static Singleton实例
    • 暴露实例获取方法
      此处输入图片的描述
  • 追求目标

    • 线程安全
    • 调用效率高
    • 延迟加载

1. 饿汉式

/**
 * 饿汉式
 * 问题: 如果只是加载本类, 而没有调用getInstance方法, 会造成资源浪费
 * Created by jifang on 15/12/4.
 */
public class HungerSingleton {

    /**
     * 类初始化时理解初始化该实例
     * 类加载时, 天然的线程安全时刻
     */
    private static final HungerSingleton instance = new HungerSingleton();

    private HungerSingleton() {
    }

    /**
     * 方法没有同步(synchronized), 调用效率高
     */
    public static HungerSingleton getInstance() {
        return instance;
    }

    @Override
    public String toString() {
        return "HungerSingleton{}";
    }
}

2. 懒汉式

/**
 * 懒汉式
 * 问题: 每次调用getInstance都要同步(synchronized), 效率降低
 * Created by jifang on 15/12/4.
 */
public class LazySingleton {

    /**
     * 类加载时并没初始化, 延迟加载
     */
    private static LazySingleton instance;

    private LazySingleton() {
    }

    /**
     * 注意synchronized, 线程安全
     */
    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }

    @Override
    public String toString() {
        return "LazySingleton{}";
    }
}

3. 双重检测锁

由于同步(synchronized)只在第一次实例化Instance时才需要,也就是单例类实例创建时, 因此我们使用双重检测锁(double checked locking pattern)实现:

/**
 * 双重锁定实现
 * 问题: 适用于JDK1.5之后的版本
 * Created by jifang on 15/12/4.
 */
public class DoubleCheckSingleton {

    /**
     * 需要使用volatile
     * 保证所有的写(write)都将先行发生于读(read)
     */
    private static volatile DoubleCheckSingleton instance;

    private DoubleCheckSingleton() {
    }

    public static DoubleCheckSingleton getInstance() {
        if (instance == null) {                          //Single Checked
            synchronized (DoubleCheckSingleton.class) {
                if (instance == null) {                  // Double Checked
                    instance = new DoubleCheckSingleton();
                }
            }
        }

        return instance;
    }

    @Override
    public String toString() {
        return "DoubleCheckSingleton{}";
    }
}

注: 有文章中指出 由于JVM底层内部模型原因, 双重锁定偶尔会出问题, 不建议使用, 但自1.5开始, 该问题已经被解决, 因此可放心使用, 详见如何在Java中使用双重检查锁实现单例.


4. 静态内部类

/**
 * 静态内部类实现Singleton
 * Created by jifang on 15/12/4.
 */
public class StaticInnerSingleton {

    /**
     * 外部类没有static属性, 因此加载本类时不会立即初始化对象
     */
    private static class InnerClassInstance {
        private static final StaticInnerSingleton instance = new StaticInnerSingleton();
    }

    private StaticInnerSingleton() {
    }

    /**
     * 只有真正调用getInstance方法时, 才会加载静态内部类(延迟加载), 而且加载类是天然的线程安全的(线程安全), 没有synchronized(调用效率高)
     *
     * @return
     */
    public static StaticInnerSingleton getInstance() {
        return InnerClassInstance.instance;
    }

    @Override
    public String toString() {
        return "StaticInnerSingleton{}";
    }
}

5. 枚举

/**
 * 枚举实现单例
 * 基于JVM底层实现, Enum天然的单例以及线程安全
 * Created by jifang on 15/12/5.
 */
public enum EnumSingleton {

    /**
     * 构造方法默认为private
     */
    INSTANCE;

    /**
     * 可以添加其他操作
     * other operation
     */
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "EnumSingleton{" +
                "name='" + name + '\'' +
                '}';
    }
}

实现对比

方式 优点 缺点
饿汉式 线程安全, 调用效率高 不能延迟加载
懒汉式 线程安全, 可以延迟加载 调用效率不高
双重检测锁 线程安全, 调用效率高, 可以延迟加载 -
静态内部类 线程安全, 调用效率高, 可以延迟加载 -
枚举 线程安全, 调用效率高 不能延迟加载

破解与防御

反射破解单例

可以利用Java的反射机制破解单例模式(Enum无法破解, 由于基于JVM底层实现),下面仅破解双重检测锁, 其他类同不再赘述:

/**
 * 单例破解
 * Created by jifang on 15/12/4.
 */
public class TestCase {

    @Test
    public void testBreakDoubleCheck() {
        try {
            Class<DoubleCheckSingleton> clazz = (Class<DoubleCheckSingleton>) Class.forName("com.feiqing.singleton.DoubleCheckSingleton");
            Constructor<DoubleCheckSingleton> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);

            DoubleCheckSingleton instance1 = constructor.newInstance();
            DoubleCheckSingleton instance2 = constructor.newInstance();
            System.out.println("singleton? " + (instance1 == instance2));
            System.out.println(instance1.hashCode());
            System.out.println(instance2.hashCode());

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

}

序列化破解单例

  • 对懒汉式破解, 需要首先对LazySingleton进行改造,支持序列化:
public class LazySingleton implements Serializable{

    private static final long serialVersionUID = 8511876423469188139L;
    /**
     * 类加载时并没初始化, 延迟加载
     */
    private static LazySingleton instance;

    private LazySingleton() {
        if (instance != null){
            throw new RuntimeException();
        }
    }

    /**
     * 注意synchronized, 线程安全
     */
    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }

    @Override
    public String toString() {
        return "LazySingleton{}";
    }
}
  • 破解:
public class TestCase {

    private static final String SYSTEM_FILE = "/tmp/save.txt";

    @Test
    public void testBreakLazy() {
        LazySingleton instance1 = LazySingleton.getInstance();

        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(SYSTEM_FILE));
            oos.writeObject(instance1);

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(SYSTEM_FILE));
            LazySingleton instance2 = (LazySingleton) ois.readObject();

            System.out.println("singleton? " + (instance1 == instance2));
            System.out.println(instance1.hashCode());
            System.out.println(instance2.hashCode());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

}

序列化防御

  • LazySingleton添加readResolve()方法:
    /**
     * 反序列化时, 如果定义了readResolve方法, 则直接返回此方法制定的对象.
     *
     * @return
     */
    private Object readResolve() {
        return instance;
    }

详细可参考: 深入理解Java对象序列化.


性能测试

/**
 * 单例性能测试
 * Created by jifang on 15/12/4.
 */
public class TestCase {

    private static final String SYSTEM_FILE = "/tmp/save.txt";
    private static final int THREAD_COUNT = 10;
    private static final int CIRCLE_COUNT = 100000;

    @Test
    public void testSingletonPerformance() throws IOException, InterruptedException {
        final CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
        FileWriter writer = new FileWriter(new File(SYSTEM_FILE), true);

        long start = System.currentTimeMillis();
        for (int i = 0; i < THREAD_COUNT; ++i) {
            new Thread(
                    new Runnable() {
                        @Override
                        public void run() {
                            for (int i = 0; i < CIRCLE_COUNT; ++i) {
                                Object instance = HungerSingleton.getInstance();
                            }
                            latch.countDown();
                        }
                    }
            ).start();
        }
        latch.await();
        long end = System.currentTimeMillis();

        writer.append("HungerSingleton 共耗时: " + (end - start) + " 毫秒\n");
        writer.close();
    }
}
  • 结果
* 单例实现 耗时
1 HungerSingleton 30 毫秒
2 LazySingleton 48 毫秒
3 DoubleCheckSingleton 25 毫秒
4 StaticInnerSingleton 16 毫秒
5 EnumSingleton 6 毫秒

Enum毫无疑问的成为了实现单例的王者, <Effective Java>中也推荐使用, 因此Enum成为Java中实现单例的最好方式, 但是Enum也有其自身的限制, 因此在使用时还需要做一番权衡.


小结

由于单例模式只生成一个实例, 减少了系统性能开销, 因此 当一个对象的产生需要比较多的资源时(如读取配置/产生其他依赖对象), 则可以通过只产生一个单例对象, 然后永久驻留内存的方式来提高系统整体性能.


posted @ 2015-12-09 14:26  挨踢人啊  阅读(169)  评论(0编辑  收藏  举报