设计模式-单例模式

引言

单例模式,这个名字大家绝对是耳熟能详。作为对象的创建模式,单例模式实质上是为了确保某一个类它只有一个实例,并且自行实例化向整个系统提供这个实例。


单例模式的要点

单例模式的要点有三个,这也是我们在设计单例模式时需要注意的几点:
  • 单例类在整个系统的运行过程中只能有一个实例;
  • 单例类必须要自行来创建实例(换句话说,在Java中就是不对外暴露构造方法,不能由其他的对象来做实例化,除了自己);
  • 单例类必须自行向这个系统提供这个实例。

单例模式的实现


饿汉式单例类

饿汉式单例类是实现起来最为简单的单例类,如下类图所示:

代码如下:
public class Singleton {
    
    private static final Singleton instance = new Singleton();
    
    private Singleton() {
        
    }
    
    public static Singleton getInstance() {
        return instance;
    }
}
可以看出,在在这个类被加载时,静态变量instance会被初始化,这时候类的私有构造方法会被调用,单例类的唯一实例被创建。
需要注意的是:由于构造方法是私有的,单例类不能是不能被继承的。

懒汉式单例类

先看看类图:

代码如下:
public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {

    }

    public synchronized static LazySingleton getInstance() {
        if (null == instance) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
从代码可以看出,跟饿汉式不一样的是不是在类被加载的时候就实例化,而是在单例类第一次被引用的时候再实例化。上述代码还稍微做了一写措施来保证线程安全。

到这里已经给出了两种单例实现的模式,但是细心的读者可能都会发现,这两种方式对并发的处理并不是那么的好,单例模式最需要注意的就是线程安全的问题,因为全程可能有n个对象都在修改单例类对象。
之前有了解过单例模式或者看过博客的读者可能很容易就想到double check,双重检查,接下来我就懒汉模式从线程不安全到线程安全举例加以说明,同时说说为什么我觉得双重检查是错误的,做不到线程安全。

线程安全的单例

首先考虑单线程版本的:
public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {

    }

    public static LazySingleton getInstance() {
        if (null == instance) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
很明显,在多线程的环境下,这样子写你的系统肯定game over了。
我给具体分析下为什么这样写是错误的:
假设现在有A和B两个线程几乎同时到底if(null == instance),假设A比B早那么一点点,那么:
  1. A首先进入if(null == instance)代码块内部,并且开始执行new LazySingleton()语句,此时,instance的值仍然为null,直到赋值语句执行完毕;
  2. 线程B不会在if(null == instance)语句外等待,此时还未赋值给instance,if语句成立,它会马上进入if代码块内部,B也开始执行instance = new LazySingleton()语句,创建出第二个实例;
  3. A的instance = new LazySingleton()执行完毕后,这时候instance不为null了,第三个线程不会再进入到if代码块内部;
  4. B也创建了一个实例,instance的变量值被覆盖,但是A引用的之前的instance不会被改变。这时候A和B都各自拥有一个独立的instance对象,这是不对的。
将上述代码做个小优化,让它线程安全:
public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {

    }

    public synchronized static LazySingleton getInstance() {
        if (null == instance) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
加了synchronized关键字之后,这个静态工厂方法都是同步的,当线程A和B同时调用此方法时:
  1. 早到一点点的线程A率先进入此方法,B在方法外部等待;
  2. A执行instance = new LazySingleton(),创建出实例;
  3. 方法锁释放,线程B进入此方法,此时instance不再为null,if代码块不会再次被执行。线程B取到的instance变量所含有的引用与A是同一个。
看到这里思维比较活跃的可能会觉得其实只需要在instance变量第一次被赋值的时候做好同步就行了,直接在方法上加锁是不是开销太大了啊?需要细化锁的粒度。然后 双重检查就被提出了, 但是,在这里我有必要提醒一句:双重检查在Java编译器里无法实现,所以它是不对的!!!接下来我就来分析分析为什么双重检查做不到线程安全。
先看看双重检查代码:
public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {

    }

    public static LazySingleton getInstance() {
        if (null == instance) { //第一次检查,位置1
            //多个线程同时到达,位置2
            synchronized(LazySingleton.class) {
                //这里只能有一个线程,位置3
                if (null == instance) {//第二次检查,位置4
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

同样还是假设线程A和B几乎同事调用静态工厂方法:
  1. 因为A和B是一批调用者,因此当它们进入此静态方法工厂时,instance变量为null,线程A和B几乎同时到达位置1;
  2. 假设A先到达位置2,进入synchronized(LazySingleton.class)到达位置3,这是,由于synchronized(LazySingleton.class)的同步限制,B无法到达位置3,只能在位置2等候;
  3. A执行instance = new LazySingleton()语句,实例化instance,此时B还在位置2处等候;
  4. A退出synchronized(LazySingleton.class),返回instance,退出静态工厂方法;
  5. B进入synchronized(LazySingleton.class)块,到达位置3,并且到达位置4,这时instance已不为空,B退出synchronized(LazySingleton.class),返回A创建的instance,退出工厂方法。此时A和B得到的是同一个instance对象。
表面上看,这个方法多么完美的解决了线程安全问题,并且节省了好多开销,然而,双重检查在Java编译器中根本就不能成立,为什么呢?

双重检查成例对Java语言编译器不成立

在Java编译器中,LazySingleton类的初始化和instance变量赋值的顺序是不可预料的,如果一个线程在没有同步化的条件下读取instance的引用,并且调用这个对象的方法的话,可能会发现对象的初始化过程尚未完成,从而造成崩溃。

在一般情况下,饿汉模式+懒汉模式基本上足以解决在实际设计工作中遇到的问题,建议读者不要过分在双重检查上面花费太多时间。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值