设计模式(创建型)---单例模式

本文详细介绍了Java中的两种单例模式实现方式:懒汉(延迟加载)和饿汉(提前创建)。针对懒汉模式,讨论了其线程安全问题,并通过双重校验锁解决了这一问题。同时,对比了饿汉模式的优缺点。总结了不同场景下选择单例模式实现的考量因素。
摘要由CSDN通过智能技术生成

分类

单例模式分为懒汉与饿汉
懒汉:懒加载,延迟创建对象
饿汉:提前创建对象
实现步骤
1.私有化构造方法
2.对外提供一个获取单例的方法

单例模式(懒汉)

第一种方式

/**
 * 单例模式-懒汉
 * 实现步骤1.私有化构造函数  2.对外提供一个获取实例的方法
 */
public class SingletonLazy {

    private static SingletonLazy instance;

    /**
     * 私有化构造方法
     */
    private SingletonLazy(){

    }

    /**
     * 多线程不安全
     * @return
     */
    public static SingletonLazy getInstance(){
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }
}

多线程调用时不安全,不能保证单例
想要保证单例,最简单的方法就是对getInstance方法加锁

/**
 * 单例模式-懒汉
 * 实现步骤1.私有化构造函数  2.对外提供一个获取实例的方法
 */
public class SingletonLazy {

    private static SingletonLazy instance;

    /**
     * 私有化构造方法
     */
    private SingletonLazy(){

    }

    /**
     * 加锁,保证单例
     * @return
     */
    public static synchronized SingletonLazy getInstance(){
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }
}

这样可以保证线程安全与单例,但是使用synchronized 会有较大的性能开销,为保证性能,我们可以尝试减小锁的粒度,不对整个方法加锁,只对创建对象的代码块加锁

/**
 * 单例模式-懒汉
 * 实现步骤1.私有化构造函数  2.对外提供一个获取实例的方法
 */
public class SingletonLazy {

    private static SingletonLazy instance;

    /**
     * 私有化构造方法
     */
    private SingletonLazy() {

    }

    /**
     * 双重校验锁
     *
     * @return
     */
    public static SingletonLazy getInstance() {
        //第一重校验
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                //第二重校验
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

}

这样仍然是不安全的,因为创建对象的instance = new SingletonLazy()并不是一个原子操作,该操作共分三步
1.在内存中为对象分配空间
2.在分配好的空间中创建对象
3.将该对象的引用(地址)的值赋值给instance
对于这三个步骤的执行顺序,JVM是可能会进行指令重排的,指令重排是JVM对语句执行的优化,只要语句之间没有依赖,JVM就有权对语句进行优化。所以执行的顺序可能不是123,可能是132,那么假设有如下的情况出现:
有两个线程,线程1与线程2调用getInstance方法,这里假设创建对象时按132的步骤执行,当线程1执行到3时(将对象引用赋值给instance),此时instance就不是null了,此时线程2执行到第一个if判断,instance不是null,线程2会执行最后的return instance,那么线程2拿到的instance是一个未创建的对象,这肯定会导致程序出问题的,所以在这里需要禁止指令重排,可以使用volatile修饰instance以禁止指令重排。
在这里插入图片描述
那么当我们用synchronized修饰getInstance方法时,同样是执行了instance = new SingletonLazy()的,为什么不用考虑指令重排的问题呢,首先双重校验锁的那种写法有问题的原因是当创建对象的操作被JVM指令重排后执行到一半有另一个线程拿到了未创建完成的对象,而用synchronized修饰getInstance方法在一个线程释放锁之前另一个线程是不能执行该方法的,所以先拿到锁的线程可以执行完整个对象创建的代码,等另一个线程拿到锁的时候获取到的就是一个创建完成的instance对象,所以不会出现问题。
双重校验锁的安全写法

/**
 * 单例模式-懒汉
 * 实现步骤1.私有化构造函数  2.对外提供一个获取实例的方法
 */
public class SingletonLazy {

    //volatile修饰instance,禁止指令重排
    private static volatile SingletonLazy instance;

    /**
     * 私有化构造方法
     */
    private SingletonLazy() {

    }

    /**
     * 双重校验锁
     *
     * @return
     */
    public static SingletonLazy getInstance() {
        //第一重校验
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                //第二重校验
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

}

单例模式(饿汉)

饿汉即提前创建好对象


/**
 * 单例模式-饿汉
 */
public class SingletonHungry {

    //类加载时就创建对象
    private static SingletonHungry instance = new SingletonHungry();

    /**
     * 私有化构造方法
     */
    private SingletonHungry(){

    }

    public static SingletonHungry getInstance(){
        return instance;
    }
}

优点:简单,没有多线程同步问题
缺点:不管是否调用,instance都会被创建,占用一段内存
如果对象不大,并且创建不复杂,直接使用饿汉
其它情况使用懒汉

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值