单例模式是最简单最常见的设计模式之一。单例模式的学习教程一般都会提到饿汉式、懒汉式,其中体现了单例模式最要关心的两个问题:对象创建的次数以及何时被创建。 本文会着重总结如上两类实现方式。
介绍
单例模式,即该类的对象只允许一个实例存在,提供全局的访问。既然只允许进行一次实例化,那么比较容易想到的实现方式就是将构造函数设为私有,然后类内创建一个对象,并对外提供一个接口用于使用者的获取。但由于多线程以及资源、效率等方面的考虑,衍生出多种实现方式。
应用场景
乍一听可能觉得单例模式局限性太大,用处有限,其实在实际的应用场景有很多:
工具类对象,如应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。如log4j日志系统。又比如windows的任务管理器,回收站。
Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
需要频繁创建销毁的对象,或创建对象时耗时过多或耗费资源过多,但又经常用到的对象。数据库连接池的设计一般就是采用单例模式,因为数据库连接是一种数据库资源,数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的。
多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
spring中的bean默认也是单例的。
……(写的有些乱,不过应该还有很多)
实现方式
所谓的饿汉式、懒汉式,我觉得应该是按照是否实现了延迟加载(lazy load)来区分的。最好的情况是即实现了lazy load,保证了线程安全,同时又能在性能(如运行时间)上有一定的保证。
1.饿汉式(不保证线程安全)
|
这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。但如果从始至终从未使用过这个实例,则会造成内存的浪费。
2.懒汉式
版本一:基本版本(不保证线程安全)
|
实现了Lazy Load,即第一次使用时才进行实例化,但仅能在单线程程序中使用,多线程无法保证单例。如果在创建实例对象前,两个线程都通过了if的判断,则会new出两个对象。
版本二:synchronized同步方法(保证线程安全,不推荐用)
|
实现了Lazy Load,且能保证线程安全。但是这里有个很大(至少耗时比例上很大)的性能问题。除了第一次调用时是执行了Singleton的构造函数之外,以后的每一次调用都是直接返回instance对象。返回对象这个操作耗时是很小的,绝大部分的耗时都用在synchronized修饰符的同步准备上,因此从性能上说很不划算。
(synchronized不仅实现了实现线程间的互斥,还能保证内存的可见性:由Synchronized的内存可见性说起)
版本三:synchronized同步块(不保证线程安全,无法使用,用于比较)
|
由于上一个版本同步效率太低,所以摒弃同步方法,改为在同步代码块内完成实例化。但是这种同步并不能保证线程安全。与版本一的情形一致,假如一个线程进入了if判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例对象。
版本四:双检锁(保证线程安全,推荐)
|
双检锁(DCL,double check lock)即双重if检查,同时在使用synchronized代码块的同时,还要将INSTANCE的定义设为volatile类型。
volatile(java5及以后版本)可以保证多线程下变量的可见性:
读volatile:每当子线程某一语句要用到volatile变量时,都会从主线程重新拷贝一份,这样就保证子线程的会跟主线程的一致。
写volatile: 每当子线程某一语句要写volatile变量时,都会在读完后同步到主线程去,这样就保证主线程的变量及时更新。
对于双检锁的具体论述,可以参考单例模式中用volatile和synchronized来满足双重检查锁机制
版本五:内部静态类(保证线程安全,推荐)
|
内部静态类SingletonInner并不会随着Singleton类的装载而装载,要在有SingletonInner的引用了以后才会装载到内存的。所以在第一次调用getInstance()之前,SingletonInner是没有被装载进来的,只有在第一次调用了getInstance()之后,里面调用了内部静态类的静态方法,产生了对SingletonInner的引用,内部静态类才会真正装载。从而产生实例对象,这也就达到了Lazy load的目的。
而类的静态属性只会在第一次加载类的时候初始化,而且在类装载时,别的线程是无法进入的。所以在这里,JVM帮助我们保证了线程的安全性。
其他
至于枚举实现、反射、序列化等相关问题,将在下一部分进行总结。
参考(看了很多,主要是以下两个):