深入研究单例模式

单例模式号称是最简单的设计模式,但是通过对单例模式一些学习,发现里面知识点挺多的,涉及的一些内容并不是初学者常见的知识。

本文目录结构如下:

  • 单例模式都有哪些写法
  • 单例模式需要注意的地方
  • 单例模式使用场景
  • 单例模式和静态方法的区别

单例模式写法:

1-饿汉式单例

/**
 * 饿汉式单例,非懒加载
 * 
 * 以空间换时间
 * 
 * 线程安全
 */
public class EagerSingleton {

    // 注意:类被加载的时候初始化实例
    private static EagerSingleton instance = new EagerSingleton();

    // 注意:private类型的构造函数,保证其他类对象不能直接new一个该对象的实例
    private EagerSingleton() {
    }

    public static EagerSingleton getInstance() {
        return instance;
    }

}

2-懒汉式单例

/**
 * 懒汉式单例,实现了懒加载
 * 
 * 线程安全
 */
public class LazySingleton {

    private static LazySingleton instance = null;

    // 注意:private类型的构造函数,保证其他类对象不能直接new一个该对象的实例
    private LazySingleton() {
    }

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

3-双重检查

public class DoubleCheckSingleton {

    // 注意:这里必须加volatile,否则其他线程可能因为线程缓存,再次判断到实例为null
    private volatile static DoubleCheckSingleton instance = null;

    // 注意:private类型的构造函数,保证其他类对象不能直接new一个该对象的实例
    private DoubleCheckSingleton() {
    }

    /**
     * 优点: <br/>
     * 1、先检查实例是否存在,如果存在,直接返回,不用线程同步<br/>
     * 2、如果实例不存在,再进入同步代码块,同时再判断一下是否已经存在,如果不存在,则确实不存在,可以在当前线程中初始化<br/>
     * 3、这里instance必须结合volatile关键字使用,否则其他线程可能因为线程缓存,再次判断到实例为null
     */
    public static DoubleCheckSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

4-静态内部类实现的单例

/**
 * 线程安全
 * 
 * 懒加载
 */
public class HolderSingleton {

    private HolderSingleton() {
    }

    // 内部类前面加static,表示类级内部类
    // 类级内部类特点:使用时才会被加载
    public static class InstanceHolder {
        // 类/静态变量的初始化是由JVM保证线程安全的
        private static HolderSingleton instance = new HolderSingleton();
    }

    /**
     * 当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;
     * 而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例;
     * 由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
     * 这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。
     */
    public static HolderSingleton getInstance() {
        return InstanceHolder.instance;
    }
}

5-枚举实现的单例

/**
 * 1、枚举本身构造方法是private,也就是枚举本身就是单例的
 * 2、枚举是线程安全的
 * 3、枚举实现了序列化,其他几种方式序列化都有问题
 */
public enum EnumSingleton {
    instance;
}

以上几种写法,最佳写法是第五种,利用枚举实现。

下面有一种不太常见,在各种教材和资料中不太会出现的写法:单例注册表

6-单例注册表实现的单例

package spring;

import java.util.HashMap;

public class SingletonWW {
    private static HashMap registry = new HashMap();   
    public SingletonWW(){}    
    public synchronized static SingletonWW getInstance(String name){    
        if(name != null) {
            if(registry.get(name) == null) {
                try {
                    registry.put(name, Class.forName(name).newInstance());
                } catch(Exception ex) {
                    ex.printStackTrace();
                }  
            } else {
                return (SingletonWW) registry.get(name);
            }
        }
        return null;   
    }
}

单例模式需要注意的地方:

写单例模式,有几个点需要注意:

  1. 构造方法必须是private类型,保证其他类对象不能直接new一个该对象的实例。
  2. 要考虑到线程安全问题,高并发的问题。
  3. 考虑懒加载的问题。

单例模式使用场景:

单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:

  1. 需要频繁实例化然后销毁的对象。
  2. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  3. 有状态的工具类对象。
  4. 频繁访问数据库或文件的对象。

以下都是单例模式的经典使用场景:

  1. 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
  2. 控制资源的情况下,方便资源之间的互相通信。如线程池等。

应用场景举例:

  1. 外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件 。
  2. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
  3. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
  4. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  5. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  6. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
  7. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
  8. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
  9. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。

单例模式和“静态类”的区别

“静态类”指“所有方法皆为静态方法的类”

作为一个小白,个人感觉这俩确实太像了,有些场景应该都可以。网上有人列举出了一些区别,不过没有什么说服力,总结一下吧:

  1. 静态方法是面向过程的,而非面向对象的编程思想 。单例是对象(实例),而静态类是变量和方法的集合 。
  2. 单例可以被继承,这是一个很大的好处,这便于用户overwrite其中的某方法,当然,继承单例的场景较少见;而静态类一般不被继承。关于单例的继承细节,这里暂不讨论,有几种办法,有兴趣的同学可以自行阅读JDK的Calendar类。
  3. 如果希望在类加载的时候做复杂的操作,那么在静态类中,需要引入static块来初始化数据,如果期间抛出了异常,就可能发生一个“ClassDefNotFoundError”的诡异错误,这对问题定位是不利的。

参考列表:

https://raychase.iteye.com/blog/1471015

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

  • © 2019 知研片语
  • 京ICP备16042882号