1、介绍
在开发过程中,发现有时整个系统只需要拥有一个全局对象,这就可以应用单例模式。例如:在应用中,应该只有一个ImageLoader实例,这个ImageLoader中含有线程池、缓存系统、网络请求等,构造多个实例资源消耗多。
2、使用场景
Ensure a class has only one instance,and provide a global of access to it.
意思是: 确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式的主要作用是确保一个类只有一个实例存在。单例模式可以用在建立目录、数据录连接等单线程操作的场合,用于对系统资源的控制。
3、UML类图
UML类图如下所示:
角色介绍:
- Client: 高层客户端
- Singleton:单例类
关键点:
- 构造函数不对外开放,一般修饰符为private
- 通过一个静态方法或者枚举返回单例类对象
- 确保单例类的对象有且只有一个,尤其是在多线程环境下
- 确保到单例类对象在反序列化时不会重新构建对象
因为单例类的构造函数私有化,从而客户端代码不能通过new 的形式手动构造对象。单例类会暴露一个公有静态方法,外界通过这个静态方法获取单例类对象,在获取单例类对象的过程中需要确保线程安全。
4、示例
1、饿汉模式
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public static synchronized Singleton getInstance() { return instance; } }Singleton类加载时,就进行了对象实例化。
2、懒汉模式
public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
懒汉模式实在第一次引用类时,才进行对象实例化。getInstance代码中添加了synchronized,是在多线程情况下保证单例对象唯一的一种手段。但是,细心的人肯定会发现,若是instance已经初始化,每次调用getInstance 都会进行同步,这样会消耗资源。
3、双重检查锁定(DCL)
public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
双重检查锁定是优化方案,既能保证线程安全,又能在单例对象初始化后调用getInstance不进行同步锁。getInstance方法中对instance进行了两次判断:第一层判断主要是为了避免不必要的同步,第二层判断是为了在null的情况下创建实例。
4、静态内部类
DCL在《Java并发编程实践》谈到某些情况下失效问题,并指出DCl这种“优化”是丑陋的,不赞成使用,推荐以下代码替代:
public class Singleton { private Singleton() {} public static Singleton getInstance() { return SingletonHolder.intance; } private static class SingletonHolder { private static final Singleton intance = new Singleton(); } }
当第一次加载Singleton类时并不会初始化instance,只有在第一次调用getInstance才会导致instance初始化,这时才会导致虚拟机加载SingletonHolder类,这种方式不仅能够确保线程安全,也能保证单例对象唯一性,同时也延迟了单例的初始化,推荐使用该方法。
5、枚举单例
public enum Singleton { INSTANCE; public void doSomething() { System.out.print("do something"); } }
写法简单是枚举单例最大的优势,枚举在java中与普通类是一样的,不仅有字段,还有自己的方法,最重要的是默认枚举实例的创建是线程安全的,并且在任何情况下都是一个单例。
6、使用容器实现单例模式
import java.util.HashMap; import java.util.Map; public class Singleton { private static Map<String, Object> objMap = new HashMap<>(); private Singleton () {} public static void registerService(String key, Object instance) { if (!objMap.containsKey(key)) { objMap.put(key, instance); } } public static Object getInstance(String key) { return objMap.get(key); } }在程序初始化,将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低使用成本,也隐藏具体实现,降低了耦合度。
5、总结
优点:
- 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建和销毁时性能又无法进行优化,单例模式的优势非常明显。
- 由于单例模式只生成一个实例,减少了系统的性能开销,当一个对象的产生需要较多的资源时,如:读取配置,产生其他依赖对象时,可以通过在应用启动时直接产生一个单例对象,用永久驻留内存方式解决。
- 单例模式可以避免对资源的多重占用,例如一个写文件操作,由于只有一个实例存在内存中,避免同一个资源文件的同时写操作。
- 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如:可以设计一个单例类负责所有数据表的映射处理。
缺点:
- 单例模式一般没有接口,扩展性差。
- 单例模式对测试不利。在并行开发环境中,若是采用单例模式的类没有完成,是不能进行测试的;单例模式的类通常不会实现接口,妨碍了使用mock方式虚拟一个对象。
- 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要用单例模式取决于环境,单例模式把“要单例”和业务逻辑融合在一起。
- 在Android中,单例对象若是持有Context,那么容易引发内存泄露,此时需要注意传递给单例对象的Context最好是Application Context。