程序员棋谱之一——单例模式

单例模式呢是一种设计模式;什么是设计模式呢?这就相当于一个下棋中的一个族谱,我们学习设计模式可以提高我们写代码的下限,但如果想提高上限就得靠自己了。目前呢主流的设计模式有26种,我们今天聊聊单例模式。

单例模式的运用场景是只实例一个对象,而什么时候会涉及到只实例一个对象呢?当一个对象的数据过于庞大的时候我们就会只实例一个对象,如我们常用的搜索引擎,一个搜索引擎里边的数据是非常多的,实例一个对象消耗的内存是非常庞大的,所有我们在使用这个类时只能实例一个对象,不然可能导致服务器崩溃。

单例模式有两种设计方案,一个是饿汉方式一个是懒汉模式。

饿汉模式

复制代码
class Singleton{
    private static Singleton instance = new Singleton();
    public static Singleton getInstance(){
        return instance;
    }
    private Singleton() {
        System.out.println("构建成功!!!");
    }
}

这里讲饿汉模式之前我们先聊聊static修饰的成员变量,我们之前学过static了但过了许久可能忘了;static修饰的成员变量呢在代码运行是就会自动加载,也就是说呢即使我们不使用这个类我们,只要我们运行了代码,成员变量就会自动的加载到内存中,而这里的成员变量是直接new Singleton,所有她是在我们运行代码的瞬间就会直接创建对象,这也是为啥这个代码会被称为饿汉的意思,他在代码运行的瞬间就创建了,说明他已经非常饥饿了。

这个到吗中阻止这个类创建第二个对象的关键要点是构造方法使用了private修饰,使得她不可在其他类中创建。

懒汉模式

懒汉模式呢是我们这章节学习的重点,在计算机中我们并不想要饿汉模式那种刚运行就创建对象的方法,我们一般习惯于在我们需要的时候才创建对象,这是为什么呢?我们都知道程序运行时会在内存或CPU中消耗资源的,饿汉模式呢会从程序开始到结束都在内存中占用空间,这使得很多资源浪费掉,而懒汉模式呢只有在用到的时候才创建对象,这使得内存或CPU的资源能够高效的利用起来,

懒汉模式代码如下:

复制代码
class Singleton{
    private static Singleton instance = null;
    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
    private Singleton(){
        
    }
}

懒汉模式与饿汉模式的不同之处在于头把istance设计为了null这使得程序运行起来也不会占用资源,而后如果需要用到这个类时直接引用getInstance方法来使用,当第一次使用时会创建对象,第n次使用时会引用同一个对象而实现单例模式。

这是我们有一个问题,这两个模式线程安全吗?

线程安全

对于饿汉模式中当程序跑起来了只涉及到一次修改操作即开头的Singleton的对象创建,即使多个线程使用这个饿汉模式的类也不会影响到线程安全问题。

对于懒汉模式呢?他是涉及到线程安全问题的,我们知道线程安全问题主要考虑到的是这个线程的执行是否为原子性,是否需要修改,而懒汉模式中的getInstance()不仅不是原子的(三个指令,判断是否为空,新创对象或赋值,返回instance)而且还是可修改的(新创对象的引用),故而线程是不安全的。

我们来演示一遍:

在这里中我们设计了两个线程一个假设为t1,另一个假设为t2;当t1线程执行到判断instance是否为空的时候,由于线程的转换是分时复用的,可能t2线程也执行判断instance是否为空,这是两个线程都会进入if语句,然后t1线程执行了new操作并返回instance,此时线程t2也执行了以上操作,这是这个代码就会执行了两次创建对象,就不符合了单例模式。

那如何修改呢?

加锁是一个解决线程安全问题的常规手段,如何加锁呢?

复制代码
class Singleton{
    private static Singleton instance = null;
   private static Object object = new Object();
    public static Singleton getInstance(){
        synchronized (object){
            if(instance == null){
                instance = new Singleton();
            }
        }

        return instance;
    }
    private Singleton(){

    }
}

我们上述推导发现这个线程安全无非是两个线程因为争判断instance是否为null而进入if语句中,我们可以直接把if语句给锁上即可完成线程安全问题。

可这是又有了新的问题,这个线程安全问题主要是两个线程第一次使用这个类引起的,如果后续我们在引用这个对象还得因为这个锁的问题造成堵塞等到浪费太多时间了,这对于程序来说是致命的,哪有解决方案吗?

线程优化

复制代码
class Singleton{
    private static Singleton instance = null;
   private static Object object = new Object();
    public static Singleton getInstance(){
        if(instance == null) {
            synchronized (object) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    private Singleton(){

    }

我们可以通过在加锁前在判断一次instance是否为空来解决因为锁的问题而造成的线程等待。

指令重排序

我们学到这里应该能清楚的知道jvm会帮我们优化代码,使得我们的代码逻辑不变的情况下加快运行速度,我们举个例子简单分析咋优化的:

当我妈叫我去买菜,他给我的菜单上是西红柿,鸡蛋,茄子,黄瓜;但按着这个顺序来买菜明显发现效率太低了,我就会耍点小心机:调整买菜的顺序,按照茄子,鸡蛋, 黄瓜,西红柿,的顺序来买,这样会加快了买菜的效率,但要买的菜都买回来了,而懒汉模式和这有什么关联呢?

我们知道新建对象会涉及到三个指令操作:1)申请内存空间,2)在空间上构造对象,3)在空间的首地址,赋值引用变量。正常来说一般程序运行都是按照这三步骤的顺序执行的,但Jvm可能会为了提高效率错开这几个顺序,可能按1 3 2来执行,当然在单线程模式可能并不影响逻辑,毕竟最终还是创建了一个新的对象,但如果多线程的话影响就大了!!!

我们还是假设有两个线程,t1线程和t2线程。

当我们先执行的是t1线程,执行到这里,JVM按照上述的1 3 2执行,此时会先把一个野指针(Java中没有指针,但明白意思即可)赋值给instance,此时instance就不为空了,又因为线程的执行是分时复用的,故而可能这是直接换成了t2来执行,这里instance已经不是空了就会造成t2线程直接跳过了锁,直接来到了return,这是t2线程拿到的就是一个野指针了!!!完全不符合预期结果,造成了线程安全问题!!!!

咋解决呢?我们之前学习过volatile,这个关键字能够解决成员变量中的内存可见性问题,在这里也非常适用,volatile同样也能解决指令重排序问题。使用懒汉模式的最终代码如下:

复制代码
class Singleton{
    private static volatile Singleton instance = null;
   private static Object object = new Object();
    public static Singleton getInstance(){
        if(instance == null) {
            synchronized (object) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    private Singleton(){

    }
}
相关推荐
zfj3212 小时前
java synchronized关键字用法和底层原理
java·开发语言·轻量级锁·重量级锁·偏向锁·线程同步
沐雨风栉2 小时前
用 Kavita+cpolar 把数字书房装进口袋
服务器·开发语言·数据库·后端·golang
Henry Zhu1233 小时前
Qt Model/View架构详解(二):内置视图与模型
开发语言·qt
chao1898443 小时前
在Qt中实现任意N阶贝塞尔曲线的绘制与动态调节
开发语言·qt
真正的醒悟3 小时前
什么是标准等保架构
开发语言·php
郑州光合科技余经理3 小时前
同城020系统架构实战:中台化设计与部署
java·大数据·开发语言·后端·系统架构·uni-app·php
LcVong3 小时前
Android 25(API 25)+ JDK 17 环境搭建
android·java·开发语言
苏宸啊3 小时前
C++string(一)
开发语言·c++
老鱼说AI3 小时前
深入理解计算机系统1.5:抽象的重要性:操作系统与虚拟机
c语言·开发语言·汇编