【JAVAEE】单例模式的介绍及实现(懒汉模式和饿汉模式)

目录

1.单例模式

1.1概念

1.2单例模式的实现方式

怎么去设计一个单例

饿汉模式

懒汉模式

懒汉模式-多线程版 

解决懒汉模式多线程不安全问题-synchronized

解决懒汉模式多线程不安全问题-volatile


1.单例模式

1.1概念

单例是一种设计模式

啥是设计模式 ?
设计模式好比象棋中的 " 棋谱 ". 红方当头炮 , 黑方马来跳 . 针对红方的一些走法 , 黑方应招的时候有一些固定的套路 . 按照套路来走局势就不会吃亏 .
软件开发中也有很多常见的 " 问题场景 ". 针对这些问题场景 , 大佬们总结出了一些固定的套路 . 按照这个套路来实现代码 , 也不会吃亏 .

单例在全局范围内只有一个实例对象。

单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建多个实例。

例如:JDBC中的DateSourse实例就只需要一个。

1.2单例模式的实现方式

怎么去设计一个单例

1.口头约定

对外提供一个方法,要求大家使用这个对象的时候,通过这个方法来获取。(不太靠谱,一般不采用)

2.使用变成语言本身的特性来处理

首先分析一下在Java中哪些对象是全局唯一的:

①类对象  .class

②用static修饰的变量(static修饰的变量是类的成员变量,所有实例对象,访问的都是同一个成员变量)

通过类对象和static配合可以实现单例的目的。

示例(实现单例模式):

用static修饰变量,变量就是自己,并且赋初始值。再提供一个对外获取实例对象的方法。

    private static Singleton instance=new Singleton();

    public Singleton getInstance() {
        return instance;
    }

验证单例是否正确

public static void main(String[] args) {
        //验证单例是否正确
        Singleton singleton01=new Singleton();
        Singleton singleton02=new Singleton();
        Singleton singleton03=new Singleton();
        //分别打印
        System.out.println(singleton01.getInstance());
        System.out.println(singleton02.getInstance());
        System.out.println(singleton03.getInstance());
    }

结果:

 既然是单例,那么通过new的方法去获取对象是有歧义的,不能让外部去new这个对象。

将构造方法私有化,通过静态方法调用,获取单例:

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

    //构造方法私有化
    private Singleton(){};

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

用类名去调用:

        Singleton singleton01=Singleton.getInstance();
        Singleton singleton02=Singleton.getInstance();
        Singleton singleton03=Singleton.getInstance();
        System.out.println(singleton01);
        System.out.println(singleton02);
        System.out.println(singleton03);

结果依旧正确:

 

饿汉模式

类一加载就完成初始化的方式称为饿汉模式

特点:书写简单,不容易出错。

上面那个例子就是饿汉模式:

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

    //构造方法私有化
    private Singleton(){};

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

懒汉模式

彼岸程序启动的时候浪费过多的系统资源,当程序使用这个对象时再对他进行初始化。即类加载的时候不创建实例,第一次使用的时候才创建实例的方式称为懒汉模式

示例:

public class SingletonLazy {
    //定义一个类成员变量
    private static  SingletonLazy instance=null;

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

运行一下,结果如下:

public static void main(String[] args) {
        SingletonLazy singleton01=SingletonLazy.getInstance();
        SingletonLazy singleton02=SingletonLazy.getInstance();
        SingletonLazy singleton03=SingletonLazy.getInstance();
        System.out.println(singleton01);
        System.out.println(singleton02);
        System.out.println(singleton03);
    }

懒汉模式-多线程版 

懒汉模式的实现其实是线程不安全的。

例如(在多线程环境下获取单例对象):

public static void main(String[] args) {
        //创建多个线程,并获取单例对象
        for (int i = 0; i < 10; i++) {
            Thread thread=new Thread(()->{
                //获取单例对象,并打印
                SingletonLazy instance=SingletonLazy.getInstance();
                System.out.println(instance);
            });
            //启动线程
            thread.start();
        }
    }

结果:

发现获取到了不同的对象,并不符合我们的预期,也就是说出现了线程不安全的现象。

那具体是什么原因导致的线程不安全呢?

t1先LOAD,然后t2执行完所有指令后,t1再继续执行。如上图所示,进行了两次初始化。所以,再多线程环境下,多个线程又可能创建多个实例。

解决懒汉模式多线程不安全问题-synchronized

①可以给getInstance方法加锁。

②将synchronized的范围指定为整个初始化过程,与在方法中加锁是等价的

synchronized (SingletonLazy.class) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }

 得到的结果目前看来是符合预期的。

但是有一个非常严重的问题:

每一次调用getInstance()方法的时候,都需要进行锁竞争,再进行判断,而锁竞争是非常耗费系统资源的。其实,synchronized代码块再整个程序运行过程中,只需要执行一次就够了。

用户态:Java层面,在JVM中执行的代码

内核态:执行的是CPU指令

也就是说加了synchronized参与锁竞争之后就从应用层面进入到了系统层面。

为了避免过度锁竞争,这里可以加入一层判断

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

双重检查,避免了过度耗费系统资源。

执行一下,结果正确:

 public static void main(String[] args) {
        // 创建多个线程,并获取单例对象
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                // 获取单例对象,并打印
                SingletonDCL instance = SingletonDCL.getInstance();
                System.out.println(instance);
            });
            // 启动线程
            thread.start();
        }

    }

解决懒汉模式多线程不安全问题-volatile

synchronized关键字解决了原子性,内存可见性问题。

volatile关键字解决有序性问题。

初始化过程,并不是一条指令。在整个初始化过程中,经历如下阶段:

1.在内存中开辟一片空间

2.初始化对象的属性(数据)

3.把内存中的地址,赋给instance变量

程序在正在执行的过程中就是按1,2,3这个顺序执行的。1与3是强相关的执行过程,2是一个单独的过程,所以编译或者CPU就有可能进行指令重排序,使程序执行过程变1,3,2.。如果出现这种顺序,那么其它线程就又肯拿到一个创建了一半的对象,导致了线程不安全。

这个时候就需要用volatile修饰变量,禁止指令重排序。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值