震惊!!!原来这就是单例模式!!!

一:什么是设计模式

单例模式是一种典型的设计模式.
设计模式就是为解决编程中的一些典型问题,提供的一些解决方案.
遇到这个情景,遇到这个问题,代码应该怎么写,效率更高,能够更好的解决这个问题.

二:单例模式

2.1:什么是单例

在进程中的某个类,有且只有一个对象(不能new出来对个对象),这样的对象,就称为"单例".
但如何保证一个类只有一个实例???
此时就需要通过一些编程上的技巧,使编译器能够自动发现代码中是否有多个实例,并且在尝试创建多个实例的时候,直接编译出错.
从代码上保证单例,这种代码就称为单例模式

2.2:饿汉模式:

class Singleton{
    /**
     * static 修饰成员变量instance,说明这个成员变量是静态的,是属于类对象的,**只有一份**
     * **在类加载的时候就被初始化了,这个创建过程就是线程安全的**
     *
     */
    private static Singleton instance=new Singleton();

    /**
     * 通过getInstance(),获取到类对象并返回
     * 后续需要使用这个类的实例,就可以通过getInstance()来获取已经new 好的这个,而不是重新new 
     * @return
     */
    public static Singleton getInstance(){
        return instance;
    }

    /**
     * private说明构造方法是私有的,在类外不能创建类对象,从而保证了类对象的唯一
     * 类之外的代码,尝试new 的时候,就会调用构造方法,由于构造方法是私有的,无法调用,就会编译出错
     */
    private Singleton(){

    }
}
public class Demo2 {
    public static void main(String[] args) {
        Singleton s1=Singleton.getInstance();
        Singleton s2=Singleton.getInstance();
        /**
         * 由于类对象是唯一的,所以这里输出true
         */
        System.out.println(s1==s2);

    }
}

class Singleton {
    private static Singleton instance = new Singleton();
    public  static Singleton getInstance(){
        return instance;
    }
    private Singleton (){
        
    }
}
public class Demo1 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2=new Singleton();
        
    }
}

在这里插入图片描述

2.3:懒汉模式:

懒汉模式:不是程序启动的时候创建实例,而是在第一次使用的时候才去创建(如果不使用,就不用创建,就会把创建实例的代价节省下来了).

class SingletonLazy{
    public static SingletonLazy instance=null;
    public static SingletonLazy getInstance(){
        if(instance==null){
            instance=new SingletonLazy();
        }
        return instance;
    }
    private SingletonLazy(){

    }
}
public class Demo4 {
    public static void main(String[] args) {
        SingletonLazy s1=SingletonLazy.getInstance();
        SingletonLazy s2=SingletonLazy.getInstance();
        System.out.println(s1==s2);

    }
}

如果代码中存在多个单例类,使用饿汉模式,就会使这些实例都在程序启动的时候扎堆创建,就可能把程序启动的时间拖慢.
如果是懒汉模式,啥时候首次调用,调用时机是分散的.

2.4:单例模式应用场景

代码中的有些对象,本身就不应该有多个实例的,从业务的角度应该就是单个实例.
比如:服务器要从硬盘上加载100G的数据到内存中,肯定要写一个类,封装上述加载操作,并且写一些获取/处理数据的业务逻辑.
这样的类就应该 是单例的,一个实例,就管理100G的内存数据,创建多个实例:就是N*100G的内存数据,机器吃不消,而且也没必要.
再比如:服务器很可能涉及到一些"配置项",代码中也需要有专门的类,管理配置,需要加载配置数据到内存中的供其他代码使用.
这样的类也应该是单例的,如果是多个实例,就存储了多份数据,如果一样就罢了,如果不一样,以哪个为准呢?

三:线程安全吗???

在这里只考虑多个线程同时调用getInstance()方法是否会产生线程安全问题.

3.1:饿汉模式??

饿汉模式是在Java进程启动的时候创建实例,而后面在线程中调用getInstance(),实例已经创建好了,不会进行创建,也就意味着饿汉模式的getInstance(),只做了一件事,那就是读取静态变量,而读取操作又是线程安全的.

3.2:懒汉模式??

懒汉模式不仅有读操作,还有修改操作,并且这两个操作还不是原子的,因此就会造成线程安全问题.
比如:
在这里插入图片描述

上图情况就会造成创建了两个实例,违背了 单例模式,就是线程不安全的.

3.2.1 加锁

通过加锁的方式,来保证懒汉模式下,getInstance()是线程安全的.

class SingletonLazy{
    private static SingletonLazy instance = null;
    Object locker =new Object();
    public SingletonLazy getInstance() {
        synchronized (locker) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
            return instance;
        }
    }
    private SingletonLazy(){
        
    }
}
public class Demo2 {
    public static void main(String[] args) {

    }
}

t1执行加锁之后,t2就会阻塞,直到t1释放锁(new 完了)t2才能拿到锁,才能进行条件判定,t2的条件就会认为instance非null,就不会创建实例了.

3.2.2 双重if判断

上述代码认为,只要调用getInstance()就需要先加锁,但懒汉模式下,只有第一次调用getInstance()的时候,才会涉及到线程安全问题,一旦把线程创建好了,后续再调用,就只是读取操作了,线程就是安全的了,就没必要加锁了.
所以我们就需要判断是否要加锁,第一次创建对象要加锁,然后都是读取操作了,就没必要加锁了,
第一次创建对象是什么时候??
答案是显然的,当instance为null的时候,当调用getInstance()就是要创建对象,此时要加锁.


/**
 *
 * 懒汉模式
 */
class SingletonLazy{
    private static SingletonLazy instance = null;
    public  static  SingletonLazy getInstance() {
        Object locker = new Object();
        if (instance == null) {//判断是否要加锁

            synchronized (locker) {
                //synchronized 会阻塞在线程,阻塞过程中,其他线程就可能修改instance的值
                if (instance == null)  {     //判断是否要创建对象,
                    instance = new SingletonLazy();
                }

            }
        }
        return instance;
    }
    private SingletonLazy(){

    }
}
public class Demo2 {
    public static void main(String[] args) {
        SingletonLazy s1=SingletonLazy.getInstance();
        SingletonLazy s2=SingletonLazy.getInstance();
        System.out.println(s1==s2);

    }
}

3.2.3:内存可见性问题

t1线程修改了instance的引用,t2线程可能读取不到(概率比较小),加上volatile关键字就可以解决.

3.3.4:指令重排序问题

我们写的代码,最终编译成了机器能够识别的二进制指令,正常来说,CPU,应该是按照顺序,一条一条往下执行的,但编译器比较智能,会根据情况,调整执行的顺序,调整顺序的主要目的就是为了提高效率(前提保证逻辑是等价的).
举一个生活中的例子:
家人让我去超市买菜:西红柿,鸡蛋,黄瓜,茄子.
但我们可能不按这个顺序买,会根据实际情况(摊位的顺序)调整:先买黄瓜,西红柿,茄子,鸡蛋,
在这里插入图片描述

把执行顺序调整之后,最终的效果没变,但效率提高了不少.
指令重排序的前提,一定是重排序之后,逻辑和之前等价.
单线程下,编译器进行指令重排序的操作,一般是没有问题的,编译器可以准确的识别出哪些操作可以重排序,而不会影响到逻辑
但在多线程下,编译器的判定就不会那么准确了,就可能会出现重排序之后,逻辑发生改变了,进而引起bug.

 instance = new SingletonLazy();

                    /**
                     * 这一行代码,可以分为三个(指令)步骤:
                     * 1:申请内存空间
                     * 2:调用构造方法(对内存空间进行初始化)
                     * 3:把此时内存空间的地址,赋值给instance引用
                     */

在指令重排序优化策略下,上述的执行顺序可能是 1 3 2 也可能是 1 2 3 (1一定是先执行的)
而如果是 1 3 2 在多线程下,可能会引起bug.
在这里插入图片描述
在这里插入图片描述
要解决上述问题,就需要引入volatile,volatile不仅仅能够解决内存可见性问题,也能禁止编译器针对这个变量读写操作的指令重排序问题.
加上volatile 之后,此时, t2 线程读到的数据,一定是t1已经构造完毕的完整对象了(一定是 1 2 3 都执行完毕的对象了).

/**
 *
 * 懒汉模式
 */
class SingletonLazy{
    private static  volatile SingletonLazy instance = null;
    public  static  SingletonLazy getInstance() {
        Object locker = new Object();
        if (instance == null) {//判断是否要加锁

            synchronized (locker) {
                //synchronized 会阻塞在线程,阻塞过程中,其他线程就可能修改instance的值
                if (instance == null)  {     //判断是否要创建对象,
                    instance = new SingletonLazy();

                    /**
                     * 这一行代码,可以分为三个步骤:
                     * 1:申请内存空间
                     * 2:调用构造方法
                     * 3:把此时内存空间的地址,赋值给instance引用
                     */
                }

            }
        }
        return instance;
    }
    private SingletonLazy(){

    }
}
public class Demo2 {
    public static void main(String[] args) {
        SingletonLazy s1=SingletonLazy.getInstance();
        SingletonLazy s2=SingletonLazy.getInstance();
        System.out.println(s1==s2);

    }
}

正确的写法:
1:先写最初的版本(不考虑线程安全的问题)
2:加上锁
3:加上双重if
4:最后加上volatile

  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十一.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值