前言
谈到单例模式,在这之前呢,首先来聊一下什么是设计模式,仁者见仁智者见智,设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。设计模式并不是一项具体的技术,而是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性,以及对类的关联关系和组合关系运用的最佳实践。
设计模式在实际的开发中,不仅能够增加的代码的可重用性,而且还可以让代码更容易被他人理解、保证代码可靠性。
设计模式是软件工程的基石,一个优秀的工程师对设计模式应该是融会贯通的,但是对设计模式能够融会贯通的工程师并不一定是优秀的。
一:什么是单例模式
单例模式(Singleton Pattern)作为23种设计模式之一,它属于创建型模式,用于返回该对象的实例。单例模式只跟本身的一个类有关系,该类负责创建自己的对象,同时确保只有唯一的一个对象被创建。可以直接访问,不需要实例化该类的对象。
单例模式与设计模式的六大原则中的单一职责原则有所冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。而单例模式恰好与之相反。
实际生活中有许多类似单例的实例,任务管理器,文件管理器、打印机等等
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
二:Java中的单例模式
在Java中,实现单例模式有两个必经之路和六种实现实现方式,每种实现方式体现它在实际软件工程中的应用范畴。
两个必经之路
1.将类的所有构造函数的可见性设置为private,避免该类的实例被外部代码创建;
2.提供一个公有的静态方法,一般方法名为getInstance,来返回该对象的唯一的对象实例。
六种实现方式:
1.懒汉式一
这种实现方式不支持多线程。严格意义上它并不算单例模式。这种方式懒加载很明显,不能够在多线程下使用。
public class HungrySingleton {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
}
2.懒汉式二
这种方式使用懒加载的方式实现,避免了内存浪费,能够在多线程中使用,但是,因为使用了同步,所以效率较低。
public class Singleton {
private static Singleton instance; private Singleton (){
}
public static synchronized Singleton getInstance() {
if (instance == null
)
{
instance = new Singleton();
}
return instance;
}
}
3、饿汉式
这种方式比较常用,但容易产生垃圾对象。通过预加载的方式进行初始化,较为浪费内存。
public class Singleton
{
private static Singleton instance = new Singleton();
private Singleton (){
}
public static Singleton getInstance()
{
return instance;
} }
4、DCL
实现DCL单例模式的时候,instance需要用volatile修饰。因为instance = new DCLSingleton()并不是一个原子操作,从字节码上来看,一共涉及4个操作(new,dup,invokespecial,putstatic),其中invokespecial是调用构造函数进行初始化,putstatic是讲new出来的引用值赋值给instance,这两个操作没有相互依赖关系。如果不加volatile修饰,在多线程并发访问的时候,可能导致重排序,先执行putstatic,后执行invokespecial,在这两个操作之间,如果另外一个线程执行getInstance,此时instance == null是不成立的,直接返回了instance,这样就获取到一个没有初始化的单例对象,后面可能会导致意外的结果。
public class DCLSingleton {
private static volatile DCLSingleton instance;
private DCLSingleton() {
}
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
if (instance == null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
}
5.静态内部类
这种方式能达到双检锁方式一样的功效,使用懒加载,没有使用synchronized方法,并且代码相对简洁。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。
public class InnerClassSingleton {
private InnerClassSingleton() {
}
public static InnerClassSingleton getInstance() {
return InnerClassSingletonHold.INSTANCE;
}
private static class InnerClassSingletonHold {
private final static InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
}
6.枚举
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
public enum Singleton {
INSTANCE; public void whateverMethod()
{
}
}
三:单例模式之线程安全
实际开发过程中,涉及了更多的多线程、高并发场景,那改如何保证单例的线程安全?
1..类初始化
饿汉和内部类就是使用类初始化过程的线程安全,来保证单例的线程安全。
2.同步锁
懒汉使用synchronized同步方法来保证线程安全,而双重检查(Double Check Lock)则使用synchronized同步块加volatile来实现线程安全。
四:什么是懒加载
懒加载就是在对象必须用到的时候再去初始化,和延时加载是同一概念,主要是避免内存的浪费,与懒加载相反的策略是预加载,预加载是指在服务的启动的时候,就把后面可能会用到的数据都提前都初始化了,通过牺牲空间换时间来增加用户的体验。
一般情况下,对内存敏感的系统,我们采用懒加载策略,用时间换空间;而对时间敏感的系统,我们采用预加载策略,用空间换时间。
五:总结
一般情况下,不建议使用懒汉方式,建议使用饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第静态内部类的方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用DCL方式。