设计模式——单例模式

单例模式算是最基础的一种设计模式,但是在工作中应用很广泛。单例模式的实现方式有很多种,大体上呈现出推进演变的趋势,大家常用的分类方式是将单例模式分为饿汉式和懒汉式,本文也将沿着单例模式演变的脉络介绍单例模式的实现方式。

首先学习一个东西需要了解这个技术是干什么的,解决了哪些问题。在开发中很多类的对象是不需要new多个对象,比如配置文件的对象存储了许多配置参数,并且通常这些配置参数是不变的,那么如果将该对象设计成唯一的就将复杂的配置简单化了,单例模式就是保证该类的对象只有一个的方法。

饿汉式

单例模式中饿汉模式。 饿汉模式中的类实例在类被加载时就被初始化出来了,因此在应用初始化的时会占用不必要的内存。同时,由于该实例在类被夹在的时候就创建出来了,所以它是线程安全的,因为类初始化是由ClassLoader完成的,而ClassloaderloadClass方法在加载类的时候使用了synchronized关键字实现线程同步。

饿汉模式的代码示例如下图所示:

public class Singleton_01 {
    private static Singleton_01 instance = new Singleton_01();
    private Singleton_01(){}
    public static Singleton_01 getInstance(){
        return instance;
    }
    public static void main(String[] args) {
    }
}

在实际使用中,这种实现方式是被推荐的,因为简单!!!而且是线程安全!!!的,但是它也存在一些问题。比如这个类不管在什么情况下都是实例化一个Singleton_01对象,那么假如这个类不被使用,那实例化这个对象的内存就被浪费了。那当然不行了!强迫症不能忍!我们试着对这个实现方式进行优化,于是就有了懒汉式实现单例方式。

懒汉式

既然我们不想在类加载时候就实例化类对象,那我们就把它改一下,如下实现:

public class Singleton {
    private static Singleton INSTANCE = null;
    public static Singleton getInstance(){
        if (INSTANCE == null){
            new Singleton();
        }
        return INSTANCE;
    }
}

乍一看好像没啥问题,但是我们考虑一下多线程环境下,假设线程一执行了到了INSTANCE==null这条语句判断为空但是还没有执行到new Singleton()这条语句,这时候线程二刚好也执行到了INSTANCE==null这条语句,那么它也会判断为空,这样就实例化了两个对象。因此这种实现方式是线程不安全的,好的那么我们都知道synchronized是可以锁住对象保证线程安全的,我们将上面的代码改成如下所示:

public class Singleton {
    private static Singleton INSTANCE = null;
    public static synchronized Singleton getInstance(){
        if (INSTANCE == null){
            new Singleton();
        }
        return INSTANCE;
    }
}

这样的实现方式当然是线程安全的,但是直接锁住一个方法,是否会对性能造成影响呢?答案是肯定的,在多线程环境下每个线程想要获取Singleton对象都需要去获得这把锁,而这个方法中也许会有一些业务代码是不需要上锁的,这样粗暴的上锁方式是不合适的并且代码效率也不高,那我们当然不能忍!于是就有了下面的解决方案,细化锁,我们只对实例化操作上锁:

public class Singleton {
    private static Singleton INSTANCE = null;
    public static Singleton getInstance(){
        //业务代码
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                INSTANCE = new Singleton();
            }
        }
        return INSTANCE;
    }
}

但是不幸的是,和上面一样,这种实现方式也是线程不安全的。因为在一个线程获取锁时,另一个线程可能也进入了这个判断,在等待前一个线程释放锁,这样也可能发生实例化对象不唯一的错误。可能这时候你会有疑惑,为什么不在if条件判断外面上锁,这个问题很好想明白,如果这样做的话任然是每次获取这个INSTANCE都需要去获取锁。为了解决上面这种实现方式的问题,双重检查的懒汉式单例实现就出来了,代码如下所示:

public class Singleton {
    private static volatile Singleton INSTANCE = null;
​
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

这样即使另外一个线程等待锁的释放由于内部还有一重检查,也不会创建第二个实例对象,保证了线程安全。

需要注意的是,INSTANCEvolatile关键字不能少。原因是这样禁止了实例化Singleton对象时发生指令重排序,在JVM中实例化对象并不是一个原子操作,而是分为三步:开辟空间成员变量赋值内存空间指向该对象的引用。在java代码编译过程中为了提高效率,会对执行顺序进行优化,也就是指令重排序,我们想象一下这样的场景:先开辟空间再将内存空间地址指向该对象的引用,最后为成员变量赋值,那么这个时候另一个线程执行到if(INSTANCE == null)这时该线程会判断当前INSTANCE对象不为空,将直接返回INSTANCE,但是这时候该对象并未进行赋值操作,这会使得我们得到了一个错误的数据,同样是线程不安全的。因此volatile关键字是需要加上的。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值