[04]从零开始的JAVAEE-单例设计模式

目录

什么是设计模式

单例模式

一个例子

饿汉模式

懒汉模式

 两种模式的线程安全讨论

解决办法


什么是设计模式

在编程语言发展的这些年中,前人踩过了无数的坑,也总结了无数宝贵的经验,试过了无数的错。

而在这么多年的发展中,就有一些大佬们,针对一些常见案例,设计出来了一些代码编写的经典套路,按照这些套路来写代码,不说代码写的多好,但起码下限摆在那里,也不会差到哪去。我们把这些套路就称为设计模式

设计模式有很多种,但在我们的校招阶段,重点只讨论两种:单例模式和工厂模式

单例模式

单例模式是一种经典的设计模式。

单例,顾名思义,就是单个实例,单个对象。在一个程序中,使得一个类只能创建出一个对象,就是单例设计模式,例如一只猫类,根据这只猫只能实例化一只猫,在这个程序中,这只猫就是独一无二的存在。

一个例子

今天你要和你的女朋友(是否存在?存疑)一起吃午饭,这是一顿丰富的午饭,三菜一汤,并且一人炫了两碗大米饭

午饭过后,你们看着桌上的残羹剩饭,开始讨论今天谁来洗碗

假设是你来洗碗,你选择吃完午饭之后立马就把碗给洗了。

那如果是你的女朋友来洗碗,她选择吃完午饭之后把碗放一边,等吃晚饭的时候在洗这两个碗。

到了晚饭时间,由于中午以及洗过碗,做饭时就不需要在洗碗了,但是吃完晚饭后,你又要收拾

如果是你的女朋友来洗碗,她会先把中午的碗给洗了然后做饭,吃完晚饭后,她会把碗放在一边,等待下次吃饭再洗

一天下来之后,你会发现,如果是你来洗碗,你两顿饭洗了两次碗,如果是你的女朋友来洗碗,她两顿饭下来只洗了一次碗

此时我们就把你的行为称为:饿汉模式

把你的女朋友的行为称为:懒汉模式

在计算机中,懒并不是一个贬义词。相反,懒是一个褒义词,正是“懒”这个属性,推动了人类的科技进步。

再以计算机来举一个例子:打开一个硬盘中的文件读取其中内容并显示

饿汉模式:把所有文件都丢到内存中,并显示

懒汉模式:只把文件读一小部分,把当前屏幕填充上,如果用户翻页,在读取其他文件

饿汉模式

下面我们来针对懒汉模式设计代码

class Singleton{
    private static Singleton Instance = new Singleton();//static属性的变量 说明instance这个变量属于Singleton这个类的属性
    //由于在JVM中每个类的类对象只能存在一份,所以被static修饰的变量自然也是唯一一份了

    public static Singleton getInstance(){
        return Instance;
    }

    private Singleton(){
        ;//禁用构造方法
    }
}

public class threaddemo11 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
    }
}

这是一份经典的饿汉模式代码,在Singleton这个类中,我们将其的实例放在了自己的成员变量中,并将他用static修饰,我们知道,static修饰的变量是类方法,类方法不属于任何类的对象,且在所有对象中共享,换而言之,使用static修饰的变量,有且只有一份。

然后我们给定一个静态方法,用于外部获取这个类的实例,最后我们把构造方法使用private修饰,使得外部无法实例化这个对象

这样我们就得到了一个,有且只有一个实例化对象,且无法在外部创建实例化的类。这就是单例模式中的饿汉模式。

这份代码之所以被称为饿汉模式,是因为:在这个类一加载的时候就会立马创建这个唯一实例,无论在后面的代码中是否用到这个实例。也就是说,即使后面没有用到这个实例,这个实例依旧被创建出来了,这样无疑的浪费效率的,所以为了提高效率,我们可以使用懒汉模式来设计代码

懒汉模式

class Singletonlazy{
    private static Singletonlazy instance = null;

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

}

public class threaddemo11 {
    public static void main(String[] args) {
        Singletonlazy s1 = Singletonlazy.getInstance();
        Singletonlazy s2 = Singletonlazy.getInstance();
        System.out.println(s1 == s2);//输出结果为true
    }
}

这是一段经典的单例模式懒汉模式实现代码,在类加载时不会先创建这个类的实例化对象,而是在主函数中第一次调用时创建,在日后调用时,由于第一次已经创建过实例化对象,在调用getinstance方法获取的也是第一次创建的对象。所以s1 == s2为true。

核心思想:非必要,不创建

两种模式的线程安全讨论

在多线程的前提下,cpu会对两个线程的代码执行顺序进行随机调度,而这里的懒汉模式,在多线程的情况下,就有可能出现线程不安全的情况,例如

 假设t1,t2两个线程同时获取实例,两个线程同时执行getInstance方法,此时cpu对这两个线程的调度如图所示

t1判断instance为空,t2判断instance为空,t1new对象并返回,此时instance已经不为空,但判断代码在t2已经执行完毕,t2再次new对象并返回。

这样就new出了两个对象,也就没法保证单例模式的单例了

而饿汉模式,由于在类加载的时候就直接创建对象了,所以它是线程安全的

解决办法

懒汉模式线程不安全的问题归根结底在于修改和判断这两个操作不是原子的

为了解决懒汉模式线程不安全的问题,我们使用加锁使得两个操作变成一个原子的行为

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

修改后的代码是这样的,但是这样修改虽然解决了问题。效率并不高,这是因为:加锁实际上是一个非常低效的行为,因为如果针对这里的判定和创建对象进行加锁,如果多个线程同时调用这个方法,就一定会触发锁竞争,锁竞争会导致线程的阻塞,线程的阻塞就会导致效率的低下。

为了再度解决效率不高的问题,我们秉持着:非必要不加锁的原则,对其再次进行优化:

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

我们加入了一个判定:如果对象以及创建完毕,那么这个方法本身就是线程安全的,无需在加锁判定,如果对象没有创建,在进行加锁。

注意:这里加入了两个if(instance == null)判定,并非错误

在CPU的随机调度下,可能会出现上面的执行情况,t1,t2同时执行getInstance方法,t1判断是否要进行加锁,t2判断是否要进行加锁,结果全部为true,随后t1进行实例化,然后t2再次进行实例化,如果只加入一次if判断,那么还是会产生线程不安全的情况,如果加入两次判断,那么t1实例化完毕后instance引用就不为空了,t2在创建前又判断了一次,这样就不会创建两个对象了。

现在我们以及解决了线程不安全和效率低下的问题,但其实这个代码还要一个很严重的问题:指令重排序:

在instance = new Singletonlazy();这段代码执行的时候,new方法实际上在cpu的指令会分为三部

  • 1.创建内存
  • 2.调用构造方法
  • 3.将内存地址赋值给引用

 现在假设系统是这样的执行顺序

 这样,即使两个线程确实只创建了一个实例,但一个线程拿到的实例却是一个空壳,也是非常致命的。

class Singletonlazy{
    volatile private static Singletonlazy instance = null;//修改了这里

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

}

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

这里将 instance加上volatile修饰,禁止了指令重排序这样的操作,还保证了内存可见性,至此一个完美的单例懒汉模式代码设计完毕。

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不卷啊

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

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

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

打赏作者

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

抵扣说明:

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

余额充值