单例模式(Singleton Pattern):保证一个类仅有一个实例,并提供一个访问它的全局访问点。
设计模式之单例模式
1 什么是单例模式
1.1 模式理解
保证一个类仅有一个实例,并提供一个访问它的全局访问点
单例模式三要点:
1 单例类只能有一个实例
这是最基本的,真正做到整个系统中唯一并不容易,通常还要考虑反射破坏、序列化/反序列化、对象垃圾回收等问题。
2 单例类必须自己创建自己的唯一实例
通常给实例构造函数protected或private权限。
3 单例类必须给所有其他对象提供这一实例
通常定义静态方法getInstance()返回
1.2 特点
优点:
- 提供了对唯一实例的受控访问,避免对资源的多重占用。
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
- 缩小名空间,避免全局变量污染空间,但比类操作更灵活。
缺点:
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了”单一职责原则”。
因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
1.3 应用
单例模式是一种对象创建型模式,用来编写一个类,在整个应用系统中只能有该类的一个实例对象。
常见应用场景:
线程池、缓存、日志、配置文件、打印机/显卡等硬件设备的驱动程序对象等等。
2 单例模式实现示例
2.1 饿汉式
简单可用
Lazy 初始化:否;
多线程安全:是;
这种方式比较常用,它基于JVM的类加载器机制避免了多线程的同步问题,对象在类装载时就实例化,所以称为饿汉式。
优点:没有加锁,执行效率会提高。
缺点:没有Lazy初始化,可能有时候不需要使用,浪费内存。
1 | public class Singleton { |
2.2 懒汉式
线程不安全,不可用
Lazy 初始化:是;
多线程安全:否;
描述: 能够在getInstance()时再创建对象,所以称为懒汉式。这种实现最大的问题就是不支持多线程。因为没有加锁同步。
1 | public class Singleton { |
2.3 同步方法的懒汉式
同步方法效率低,不推荐
Lazy 初始化:是
多线程安全:是
描述: 除第一次使用,后面getInstance()不需要同步;每次同步,效率很低。
1 | public class Singleton { |
2.4 双重校验锁(可用)
Lazy 初始化:是;
多线程安全:是;
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。 实例变量需要加volatile 关键字保证易变可见性,JDK1.5起才可用。
1 | public class Singleton { |
2.5 静态内部类(推荐)
Lazy 初始化:是;
多线程安全:是;
描述:同样利用了JVM类加载机制来保证初始化实例对象时只有一个线程,静态内部类SingletonHolder 类只有第一次调用 getInstance 方法时,才会装载从而实例化对象。
1 | public class Singleton { |
2.6 枚举
《Effective Java》推荐,不常见
Lazy 初始化:否;
多线程安全:是;
描述: 从Java1.5开始支持enum特性;无偿提供序列化机制,绝对防止多次实例化,即使在面对复杂的序列化或者反射攻击的时候。
不过,用这种方式写不免让人感觉生疏,这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法
1 | public enum Singleton { |
3 小结
以上6种单例实现方式,不是线程安全的不能用,至于是否需要延时加载,看情况而定。
一般情况下,使用最基本、最简单的第一种饿汉式就行了(JDK中有不少使用该种方式),需要延时加载的使用静态内部类方式,需要高安全性的可以使用第6种枚举方式。
4 其他关注点
4.1 单例模式VS静态类
把类中所有属性/方法定义成静态也可以实现”单例”。
那为什么需要用”NEW”单例模式,在而不把类中所有属性/方法定义成静态的?
静态类不用实例化就可以使用,虽然使用比较方便,但失去了面向对象的一些优点,适用于一些过程简单且固定、不需要扩展变化、不需要维护任何状态的类方法,如java.lang.Math,里面每种计算方法基本都是固定不变的。
单例模式保证一个类对象实例的唯一性,有面向对象的特性,虽然扩展不容易,但还是可以被继承(protected权限的构造方法)、重写方法等。
4.2 Java反射攻击破坏单例
上面6种Java单例模式实现方式除枚举方式外,其他的给实例构造函数protected或private权限,依然可以通过相关反射方法,改变其权限,创建多个实例,如下:
1 | public class Test { |
可以给构造函数加上判断,限制创建多个实例,如下:
1 | private Singleton() { |
4.3 反序列化攻击破坏单例
很多语言、框架都支持对象的序列化,对象序列化后再进行存储或传输,以获得更好的效率,之后再反序列化得到同样的对象信息。
同样,前面6种Java单例模式实现方式除枚举方式外,其他方式用一样的序列化数据,可以多次反序列出多个不同的实例对象。
对于Java语言提供的序列化/反序列化机制,需要单例类实现java.io.Serializable接口;而在在反序列化时会调用实例的readResolve()方法,只要加入该方法,并在方法中指定返回单例对象,就不会再新建一个对象,如下:
1 | private Object readResolve() { |
4.4 单例中对象被垃圾回收
对于JDK1.2后的JVM HotSpot来说,判断对象可以回收需要经过可达性分析,由于单例对象被其类中的静态变量引用,所以JVM认为对象是可达的,不会被回收。
另外,对于JVM方法区回收,由堆中存在单例对象,所以单例类也不会被卸载,其静态变量引用也不会失效。
4.5 多JVM/ClassLoader系统使用单例
不同ClassLoader加载同一个类,对类本身的对象(Singleton.class)来说是不一样的,所以可以创建出不同的单例对象,对不同JVM的情况更是如此,这些在JavaEE开发中还是比较常见。
所以,在多JVM/ClassLoader的系统使用单例类,需要注意单例对象的状态,最好使用无状态的单例类
4.6 IOC框架实现的单例
Spring的一个核心功能控制反转(Inversion of Contro,IOC),或称依赖注入(dependency injection ,DI):
高层模块通过接口编程,然后通过配置Spring的XML文件或注解来注入具体的实现类(Bean)。 这样的好处的很容易扩展,想要更换其他实现类时,只需要修改配置就可以了。
其功能是通过IOC容器来实现,其默认生成的Bean是单例的:
在整个应用中(一般只用一个IOC容器),只创建Bean的一个实例,多次注入同一具体类时都是注入同一个实例。
IOC容器来实现过程简述如下:
当需要注入Bean时,IOC容器首先解析配置找到具体类,然后判断其作用域(@Scope注解);
如果是默认的单例@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON),则查找容器中之前有没有为其创建了Bean实例;
如果有则直接注入该Bean实例,如果没有生成一个放到容器中保存(ConcurrentHashMap – map.put(bean_id, bean)),再注入。注:其中解析配置查找具体类、生成Bean实例和注入过程都是通过Java反射机制实现的
从上面可以了解到,Spring实现的单例和我们所说的单例设计模式不是一个概念:
前者是IOC容器通过Java反射机制实现,后者只是一种编程方法(套路)。 但总的来说,它们都可以实现“单例”
5 总结
单例模式:
1)、单例模式可以在一些应用场景带来很好的效果,但不能滥用,因为单例模式并不是一种很好的模式。
2)、单例模式有多种实现方式,没有特殊要求的,用最基本、最简单的饿汉式,需要延时加载的使用静态内部类方式,需要高安全性的可以使用枚举方式;
3)、对其他关注点应有所了解,有时间可以深入探究,扩展知识面。