1.单例模式
什么是单例模式?
所谓单例,即单个实例。通过编码技巧约定某个类只能有唯一一个实例对象,并且提前在类里面创建好一个实例对象,把构造方法私有化,再对外提供获取这个实例对象的方法,(方法名通常是用getInstance这个名称)。
根据创建时机不同,分为两种:
1.类加载的时候创建,也称为饿汉模式。
public class Singleton {//私有构造方法 禁止外界创建实例对象private Singleton() {};//唯一实例对象private static Singleton instance = new Singleton();//为外界提供获取唯一实例的方法public static Singleton getInstance() {return instance;}}
2.在第一次使用的时候创建,也称为懒汉模式 但这种有线程安全问题。
public class SingletonLazy {//私有构造方法private SingletonLazy() {}//实例对象private static SingletonLazy instance = null;//首次调用该方法时才真正创建出实例public static SingletonLazy getInstance() {if(instance == null) {instance = new SingletonLazy();}return instance;}}
总结:
高效性:饿汉模式是在类加载的时候就会创建实例,不管后面用不用得到,都会创建出来。而懒汉模式是只有你真正用了,才会创建出实例,如果不用则不创建,这样也就比较灵活,也就省下了创建实例这一开销。
比如有个非常大的文档(10G)需要打开,有两种方式打开:
- 先把所有的内容都加载到内存中,然后再显示内容。即饿汉。
- 先只加载一部分数据到内存,立即显示内容。随着用户翻页,再动态加载其他内容。即懒汉。
为什么懒汉模式会有线程安全问题?
先来说说线程安全问题产生的原因。
- 如果多个线程同时修改同一个变量,就有可能出现线程安全问题。
- 如果多个线程同时读取同一个变量,是不会出现线程安全问题的。
饿汉模式中是直接创建实例并返回实例,而懒汉模式是通过判断进行了修改,既读又修改。这种判断再修改就可能会导致线程不安全问题(因为可能会new多次,创建多个实例的话就不是单例模式了)。假设有两个线程 t1,t2,假如t1进行判断instance为null,准备new时,这时候可能会出现t1还没new呢,t2就开始判断instance是否为null。那此时instance肯定为null。这样的话,实例就会被创建多次。显然这就违背了单例模式的要求:单个实例。
如何使懒汉模式线程安全?
进行加锁。
加锁也得注意咋加,要看加的合不合适,不是说加了就好了。
比如这种加锁:
public class SingletonLazy {//私有构造方法private SingletonLazy() {}//实例对象private static SingletonLazy instance = null;//首次调用该方法时才真正创建出实例public static SingletonLazy getInstance() {if(instance == null) {synchronized (SingletonLazy.class) {instance = new SingletonLazy();}}return instance;}}
缺点:没有使 if判断 和 new操作 成为一个整体,虽然在实例对象的时候加锁了,但是线程在if判断的时候,没有加锁,还是会出现误判。假设有两个线程t1,t2,由于if判断并没有加锁,两个线程是可以同时判断的,如果t2线程刚好在t1线程判断instance为nullt1线程进入new之前或还没new完时t2进行if判断,也是会创建多个实例对象的, 这就导致虽然new的时候加了锁线程是顺序执行的,但new外面的逻辑线程还是随机调度的。于是给整个if上锁。
public class SingletonLazy {//私有构造方法private SingletonLazy() {}//实例对象private static SingletonLazy instance = null;//首次调用该方法时才真正创建出实例public static SingletonLazy getInstance() {synchronized (SingletonLazy.class) {//if和new成为一个整体if(instance == null) {instance = new SingletonLazy();}}return instance;}}
这样加锁弥补了上一个代码的缺点,但是还有一个问题,加锁这种操作就是把调度的随机性改为顺序执行了,那效率,性能必然会大打折扣,况且我们把加锁放在最外面的话,只要用到实例都要加锁,而创建实例对象只有在首次时会发生线程不安全,其实加锁一次就行,用不着回回都进行加锁。这个代码线程虽然是安全了,但是同时效率也降低了,那么有没有一种既能使线程安全又能使效率比较快的代码逻辑呢?当然有,一种方法是在加锁外面再加一层if判断。即两层if。代码如下:
public class SingletonLazy {//私有构造方法private SingletonLazy() {}//实例对象private static SingletonLazy instance = null;//首次调用该方法时才真正创建出实例public static SingletonLazy getInstance() {if(instance == null) {synchronized (SingletonLazy.class) {if(instance == null) {instance = new SingletonLazy();}}}return instance;}}
外面这层if就是用来判断对象是否创建好,如果创建好了,就不用进入外层if加锁,直接执行return。代码效率一下提高。如果没创建好,才会进入并加锁。而不是想上面代码频繁加锁。里面的if是判断是否需要new对象。
但是上面代码还是会有一个问题,就是指令重排序问题。
指令重排序问题是什么?
说到底和内存可见性一样,都是编译器为了增加效率,而对原有代码的执行顺序做出调整。调整的前提是保持逻辑不变。
举个例子:假如我们去超市买东西,需要买菜,买衣服,买首饰,买玩具。此时若按照衣服,玩具,首饰,菜这种顺序效率是最高的。
在单例模式中,new操作,时可能会触发指令重排序问题的,new操作可分为三步:
- 申请内存空间。
- 在内空间上构造对象(构造方法)。
- 给对象引用。
其中1的顺序不变,2和3的顺序是可以换的。执行1->2->3顺序使我们希望的,但若是执行1->3->2顺序,执行到3时,对象虽然不是null了,但是此时的对象还没有初始化,贸然使用是非法的。若有两个线程,一个线程才执行到1->3,另一个线程去使用还未new完的这个对象,就会引发异常。
但虽然这样说,我们不是加了锁吗,那不应该是其他没加锁的线程阻塞等待加锁的这个线程执行完new的三步后,释放锁后,其他线程才能继续执行吗,为什么我加锁的线程还没new完,甚至是还没释放锁呢,其他线程就已经去使用对象了?原因是:另一个线程压根就没进入外层if。一个线程加锁,没拿到锁的等待,不是锁一个线程拿到锁了,不管其他线程在干嘛都得停下来等待,而是执行到有synchronized语句时,才等待。既然这个线程都没进入外层if,肯定碰不到synchronized语句,就会直接return,实例就被拿去使用了。
上述问题的核心是解决指令重排序问题,解决办法就是给实例对象加volatile修饰。
public class SingletonLazy {//私有构造方法private SingletonLazy() {}//实例对象private static volatile SingletonLazy instance = null;//首次调用该方法时才真正创建出实例public static SingletonLazy getInstance() {if(instance == null) {synchronized (SingletonLazy.class) {if(instance == null) {instance = new SingletonLazy();}}}return instance;}}
应用场景举例:
- 外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件 。
- Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
- windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
- 网站的计数器,一般也是采用单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
- Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
- 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
- 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
- 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
- HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例。