什么是单例
单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。
我们知道,在面向对象的思想中,通过类的构造函数可以创建对象,只要内存足够,可以创建任意个对象。所以,要想限制某一个类只有一个单例对象,就需要在他的构造函数上下功夫。
实现对象单例模式的思路是:
1、一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);
2、当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;
3、同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
最简单的单例代码:
以上Java代码,就实现了一个简单的单例模式。我们通过将构造方法定义为私有,然后提供一个getInstance方法,该方法中来判断是否已经存在该类的实例,如果存在直接返回。如果不存在则创建一个再返回。
线程安全的单例
如果有两个线程同时执行到if(instance==null)这行代码,这是判断都会通过,然后各自会执行instance = new Singleton();并各自返回一个instance,这时候就产生了多个实例,就没有保证单例!
上面这种单例的实现方式我们通常称之为懒汉模式,所谓懒汉,指的是只有在需要对象的时候才会生成(getInstance方法被调用的时候才会生成)。上面的这种懒汉模式并不是线程安全的,所以并不建议在日常开发中使用。基于这种模式,我们可以实现一个线程安全的单例的,如下:
通过在getInstance方法上增加synchronized,通过锁来解决并发问题。这种实现方式就不会发生有多个对象被创建的问题了。
双重校验锁
上面这种线程安全的懒汉写法能够在多线程中很好的工作,但是,遗憾的是,这种做法效率很低,因为只有第一次初始化的时候才需要进行并发控制,大多数情况下是不需要同步的。
我们其实可以把上述代码做一些优化的,因为懒汉模式中使用synchronized定义一个同步方法,我们知道,synchronized还可以用来定义同步代码块,而同步代码块的粒度要比同步方法小一些,从而效率就会高一些。如以下代码:
上面这种形式,只有在singleton == null的情况下再进行加锁创建对象,如果singleton!=null的话,就直接返回就行了,并没有进行并发控制。大大的提升了效率。
从上面的代码中可以看到,其实整个过程中进行了两次singleton == null的判断,所以这种方法被称之为”双重校验锁”。
还有值得注意的是,双重校验锁的实现方式中,静态成员变量singleton必须通过volatile来修饰,保证其初始化不被重排,否则可能被引用到一个未初始化完成的对象。
饿汉模式
前面提到的懒汉模式,其实是一种lazy-loading思想的实践,这种实现有一个比较大的好处,就是只有真正用到的时候才创建,如果没被使用到,就一直不会被创建,这就避免了不必要的开销。
但是这种做法,其实也有一个小缺点,就是第一次使用的时候,需要进行初始化操作,可能会有比较高的耗时。如果是已知某一个对象一定会使用到的话,其实可以采用一种饿汉的实现方式。
所谓饿汉,就是事先准备好,需要的时候直接给你就行了。这就是日常中比较常见的”先买票后上车”,走正常的手续。
如以下代码,饿汉模式:
饿汉模式中的静态变量是随着类加载时被完成初始化的。饿汉变种中的静态代码块也会随着类的加载一块执行。
以上两个饿汉方法,其实都是通过定义静态的成员变量,以保证instance可以在类初始化的时候被实例化。
因为类的初始化是由ClassLoader完成的,这其实是利用了ClassLoader的线程安全机制。ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。也正是因为这样, 除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)
除了以上两种饿汉方式,还有一种实现方式也是借助了calss的初始化来实现的,那就是通过静态内部类来实现的单例:
前面提到的饿汉模式,只要Singleton类被装载了,那么instance就会被实例化。
而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。
使用静态内部类,借助了classloader来实现了线程安全,这与饿汉模式有着异曲同工之妙,但是他有兼顾了懒汉模式的lazy-loading功能,相比较之下,有很大优势。
单例的破坏
前文介绍过,我们实现的单例,把构造方法设置为私有方法来避免外部调用是很重要的一个前提。但是,私有的构造方法外部真的就完全不能调用了么/p>
其实不是的,我们是可以通过反射来调用类中的私有方法的,构造方法也不例外,所以,我们可以通过反射来破坏单例。
除了这种情况,还有一种比较容易被忽视的情况,那就是其实对象的序列化和反序列化也会破坏单例。
如使用ObjectInputStream进行反序列化时,在ObjectInputStream的readObject生成对象的过程中,其实会通过反射的方式调用无参构造方法新建一个对象。
所以,在对单例对象进行序列化以及反序列化的时候,一定要考虑到这种单例可能被破坏的情况。
可以通过在Singleton类中定义readResolve的方式,解决该问题:
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!