单例 (Singleton) 设计模式
参考:单例模式
单例设计模式,就是采取一定的方法保证某个类只能存在一个对象实例。
考虑到一个对象的创建和销毁需要较多的资源,可以采用单例模式只生成一个实例,从而减少系统的性能开销。
实现方式
要让类只能产生一个对象,我们首先必须将类的构造器的访问权限设置为private,这样,就不能用new操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象。因为在类的外部无法通过构造器来创建对象,只能调用该类的某个静态方法以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的。
1.饿汉式-线程安全
代码实现
public class test {
public static void main(String[] args) {
//测试
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2); // true
}
}
//饿汉式(静态变量)
class Singleton {
//构造器私有化,外部不能new对象
private Singleton() {
}
//内部创建类的对象,此对象也必须声明为static的
private static Singleton instance = new Singleton();
//提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
return instance;
}
}
复制代码
构造器私有使得外界无法通过构造器实例化Singleton类,要取得实例只能通过getInstance()方法。
优缺点
优点:类装载时就完成实例化,无需考虑线程同步,本身就是线程安全。
缺点:1.如果从始至终从未使用过这个实例,则会造成内存的浪费 2.对象加载时间过长
2.懒汉式-线程不安全
class Singleton {
private Singleton() { //私有化类的构造器
}
private static Singleton instance=null;
//声明当前类对象,没有初始化
//此对象也必须声明为static的
//声明一个static、public的返回当前类对象的方法
//当使用到该方法时,才去创建instance,即懒汉式
public static Singleton getInstance() {
if (instance == null) { //如果instance为null,说明还未创建对象,那么就进行实例化
instance = new Singleton(); // 创建实例
}
return instance; //不是null,直接return
}
}
复制代码
优缺点
- 懒汉式:起到了懒加载的效果,延迟对象的创建。在需要对象的时候才进行实例化操作。
- 在多线程下,线程A进入了if (singleton == null)判断语句,还未来得及往下执行,线程B也进入了这个判断语句,并判定singleton == null,此时线程B创建了一个实例对象;线程A接着执行创建实例的操作,也创建了实例对象,这就导致了出现创建多个实例对象的情况。因此只能在单线程下使用,在多线程环境下由于没有同步措施不可使用这种方式 。
3.懒汉式-线程安全、同步方法
class Singleton {
private static Singleton instance;
private Singleton() {
}
//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题,即懒汉式
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
复制代码
优缺点
- 使用synchronized关键字来对getInstance()方法做同步处理,解决了多线程并发的线程安全问题。
- 带来了效率问题:每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而我们的目的是只创建一个实例,即其实这个方法只执行一次创建实例就够了,也正是这个地方才需要同步;后面的想获得该类实例, singleton非空就会直接return就行了,而不用每次都在同步代码块中进行非空验证。
- 如果getInstance()被多个线程调用,synchronized将导致巨大的性能开销。
4.懒汉式-线程安全、同步代码块
为了解决上面出现的问题只对创建实例处进行同步:
class Singleton{
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
singleton = new Singleton(); //创建实例
}
}
return singleton;
}
}
复制代码
存在的问题: 不能起到线程同步的作用。多个线程同时执行到条件判断语句时,会创建多个实例。
原因: 问题在于当一个线程创建一个实例之后,singleton就不再为空了,但是后续的线程并没有做第二次非空检查
5.双重检查-线程安全
为了解决上面出现的问题,就是进行二次非空检查,这样就可以保证线程安全了。
为什么要进行第二次判空?
假如现在没有第二次验校,线程A执行到第一次验校那里,它判断到single ==null。此时它的资源被线程B抢占了,B执行程序,进入同步代码块创建对象,然后释放同步锁,此时线程A又拿到了资源也拿到了同步锁,然后执行同步代码块,因为之前线程A它判断到single ==null,因此它会直接创建新的对象。所以就违反了我们设计的最终目的。
这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null), 直接return实例化对象,也避免的反复进行方法同步.
public class DoubleCheckedLocking {
private static Instance instance;
public static Instance getInstance(){
if(instance ==null) { //第一次检查
synchronized (DoubleCheckedLocking.class) { //加锁
if (instance == null) //第二次检查
instance = new Instance(); //创建实例
}
}
return instance;
}
}
复制代码
如果第一次检查instance不为null,那就不需要执行下面的加锁和创建实例的操作。因此,可以大幅降低synchronized带来的性能开销。
这样似乎很完美,但这是一个错误的优化!在线程执行到第4行,代码读取到instance不为null时,instance引用的对象可能还没有完成初始化。
存在的问题
创建实例,这一行代码可以分解为如下的3行伪代码。
memory=allocate(); //1:内存分配:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:返回对象引用:设置instance指向刚分配的内存地址
复制代码
JVM可能会对指令进行重排序。2和3重排序之后的执行时序如下。
memory=allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址
//注意,此时对象还没有被初始化!
ctorInstance(memory); //2:初始化对象
复制代码
如果发生重排序,对应到创建实例就是singleton已经不是null,而是指向了堆上的一个对象,但是该对象却还没有完成初始化动作。当线程B发现singleton不是null而直接使用这个对象时,就会出现这个对象可能还没有被A线程初始化的问题。
在知晓了问题发生的根源之后,我们可以通过不允许2和3重排序来实现线程安全的延迟初始化。
使用volatile禁止重排序
把instance声明为volatile型,就可以实现线程安全的延迟初始化
class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); //instance为volatile,现在没问题了
}
}
}
return instance;
}
}
复制代码
当声明对象的引用为volatile后,之前的3行伪代码中的2和3之间的重排序,在多线程环境中将会被禁止。上面的示例代码江安如下的时序执行。
这个方案是通过禁止上图2和3之间的重排序,来保证线程安全的延迟初始化。
解决方案: JDK1.5之后,可以使用volatile关键字修饰变量来解决无序写入产生的问题,因为volatile关键字的一个重要作用是禁止指令重排序,即保证不会出现内存分配、返回对象引用、初始化这样的顺序,从而使得双重检测真正发挥作用
6.静态内部类
基于类初始化的解决方案
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
基于这个特性可以实现另一种线程安全的延迟初始化方案。
public class InstanceFactory{
private static class InstanceHolder{
public static Instance instance = new Instance();
}
public static Instance getInstance(){
return InstanceHolder.instance; //这里将导致InstanceHolder类被初始化
}
}
复制代码
假设两个线程并发执行getInstance()方法,下面是执行示意图。
两个线程并发执行的示意图
这个方案的实质是:允许之前的3行伪代码中的2和3重排序,但不允许非构造线程(这里指线程B)“看到”这个重排序。
总结
通过对比基于volatile的双重检查锁定的方案和基于类初始化的方案,我们会发现基于类初始化的方案的实现代码更简洁。但基于volatile的双重检查锁定的方案有一个额外的优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。
字段延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段的开销。在大多数时候,正常的初始化要优于延迟初始化。如果确实需要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于volatile的延迟初始化方案;如果确实需要对静态字段使用线程安全的延迟初始化,请使用基于类初始化的方案。
不用手写,了解其它的实现方式
使用静态内部类为什么能够实现延迟加载?
// 静态内部类完成, 推荐使用
class Singleton {
private static volatile Singleton instance;
//构造器私有化private Singleton() {}
//写一个静态内部类,该类中有一个静态属性 Singleton
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
//提供一个静态的公有方法,直接返回 SingletonInstance.INSTANCE
public static synchronized Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
复制代码
优缺点:
- 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
- 静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载 SingletonInstance 类,从而完成 Singleton 的实例化。
- 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
- 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
- 结论:推荐使用.
7.枚举
enum Singleton {
INSTANCE;
//属性
public void sayOK() {
System.out.println("ok~");
}
}
复制代码
这借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。