作者主页paper jie_博客

本文作者大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦。

本文于《JavaEE》专栏,本专栏针对大学生编程小白精心打造的。笔者用重金(时间和精力)打造,将MySQL基础知识一网打尽,希望可以帮到读者们哦。

其他专栏:《MySQL》《C语言》《javaSE》《数据结构》等

内容分享:本期将会分享设计模式中的单例模式

目录

什么是设计模式

什么是单例模式

单例模式的实现方式

饿汉模式

具体代码

代码分解 

懒汉模式

懒汉模式 – 单线程

具体代码

代码分析

懒汉模式 – 多线程

问题一: 原子性

改进

问题二: 加锁带来的开销

改进

问题三: 指令重排序

改进:


什么设计模式

设计模式是咱们程序圈子中的一些大佬写出来的,为了规范我们代码,让我们代码不至于写的见不得人,可以使用设计模式等于给了你一个下限,只要你按照这个模式来写,再怎么样也不至于水到哪去. 就可以把它理解为好比棋谱一般.给了我们一些套路,在遇到什么情况,该什么应对. 我们软件开发就是有许多常见问题场景,针对这些问题场景,大佬们就总结出来一些固定的套路给我们这些小萌新使用.

什么是单例模式

单例模式就是能保证某个类只能创建一个实例,而不能创建多个实例.这一点在很多场景中都需要使用.因为往往这类实例需要内存空间和开销都比较大,我们就需要限制创建实例个数.

单例模式的实现方式

单例模式有许多种实现方式,不过其中最常见就是饿汉模式和懒汉模式.

饿汉模式

饿汉模式,顾名思义,对于创建这个实例非常迫切,在类一加载就创建好了.

具体代码

class Singleton {
    private static Singleton instance = new Singleton();
    public Singleton getInstance() {
        return instance;
    }
    private Singleton() {

    }
}

代码分解 

1. 这里instance静态属性,类属性. 是随着类的加载而创建.因为一个类只有一个类对象,类属性也只有一份,所以这里只能创建一次实例.

2. 在类外需要获取这个实例需要使用getinstance方法获取,而不是自己new一个.

3. 这里在类外再new一个对象发现编译报错,这是因为这里将它的构造方法设置私有的,在类外是访问不到.这就保证了只能创建一份实例.

懒汉模式

这里懒汉模式有两种,一种单线程,一种多线程.

懒汉模式 – 单线程

具体代码
class Singleton2 {
    private static Singleton2 instance = null;
    public Singleton2 getInstance() {
        if(instance == null) {
            return new Singleton2();
        }
        return instance;
    }
    
    private Singleton2() {
        
    }
}
代码分析

这里不会在类加载时候直接创建实例,而是将创建实例放到方法中. 当第一个调用getinstance方法时就会创建出实例.后面再使用这个方法时因为instance不是null后就不会再创建了.

这样的代码也可以保证只创建一次实例. 创建实例的实例就看代码中什么时候使用这个方法.这样创建实例就不是很急迫,只有需要的时候才会创建,所以叫懒汉. 且这样有一个好处就是如果用不到这个实例它就不会创建,遮掩就省了一笔开销.要知道一般单例模式中的实例都需要占很大的内存空间的,加载的时间需要很久,这样子加载的时间省了,空间也省了.

懒汉模式 – 多线程

上述的单线程中,是没有多线程安全问题的,但是当在多线程协调工作时就会线程安全.像饿汉模式也是线程安全的,因为它只涉及到读操作,但是懒汉模式涉及到读和写操作.这样就会有线安全问题.

问题一: 原子

我们假设两个线程,t1和t2.它们都需要使用getinstance这样方法. 这里可能就会有一种情况: t1先执行,到t1执行到if()语句判断后它就被调度走了,这时t2调度过来执行.等t2执行完到new一个实例返回后,t1再执行.但是t1执行时候还是认为instance是null,于是它就又创建了一个实例.

这里就是因为这些操作没有具有原子性(当然这里是伪原子,只是针对t2这个线程来说)

改进

我们这里就需要用到我们的加锁操作了.将相关代码加锁!

class Singleton1 {
    private static Singleton1 instance = null;
    public Singleton1 getInstance() {
        synchronized (Singleton1.class) {
            if (instance == null) {
                return new Singleton1();
            }
            return instance;
        }
        
    }
    private Singleton1() {
    }
}
问题二: 加锁带来的开销

这里看似处理问题,但是还有其他的毛病.这里我们发现加锁操作其实只有第一次创建对象(也就是操作)是需要的,后面的操作都不需要,但是我们还是需要进行锁竞争,这里就是导致开销比较大.

改进

这里我们在外头再加上一层if判断即可. 这里的第一个if就是判断需不需要加锁,第二个if就是在加锁操作判断没有创建实例.

class Singleton1 {
    private static Singleton1 instance = null;
    public Singleton1 getInstance() {
        if(instance == null) {
            synchronized (Singleton1.class) {
                if(instance == null) {
                    return new Singleton1();
                }
            }
        }
        return instance;
    }
    private Singleton1() {
    }
}
问题三: 指令排序

对于new这个代码,有三个操作: 1. 申请一块内存. 2. 在这块内存调用构造方法初始化这个实例. 3. 将这个内存地址赋值到instance中.  这里一般来说我们就是按顺序执行.但是JVM中可能就会给你优化,也就是指令排序. 可能就会出现 1 3 2这种顺序. 这里就会发生问题: 

这里也假设有t1和t2, 当t1一直执行到new操作的3后,这时就被CPU调度走了,让t2进来执行代码.当t2执行到第一个if判断时,发现instance不为null,他就直接返回了.但是这里的instance是一块没有初始化空间地址,这就会导致t2接下来逻辑发生问题.

改进:

这里我们就需要用到我们的volatile.它有两个作用:

1. 保证内存可见性. 它可以强制让JVM不进行代码优化,保证每次都到内存中读取变量.

2. 禁止指令排序. 针对volatile修饰的这个变量相关指令,JVM是不可优化排序的.

class Singleton1 {
    private volatile static Singleton1 instance = null;
    public Singleton1 getInstance() {
        if(instance == null) {
            synchronized (Singleton1.class) {
                if(instance == null) {
                    return new Singleton1();
                }
            }
        }
        return instance;
    }
    private Singleton1() {
    }
}

原文地址:https://blog.csdn.net/paperjie/article/details/134738670

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_39648.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注