自学内容网 自学内容网

【JavaEE】单例模式

目录

前言

1.单例模式

2.如何实现单例模式

2.1饿汉模式

2.2懒汉模式

3.单例模式中的线程安全问题

3.1加锁

3.2解决懒汉模式中频繁加锁问题

3.3解决new中指令重排序的线程安全问题 


前言

在聊单例模式之前,我们先来了解一下什么是设计模式

设计模式是在软件工程中用来解决常见问题的一种标准方法或模板,它们代表了软件开发者在面对特定类型的问题时,经过验证的、可复用的解决方案。设计模式并不是完成具体任务的代码,而是一种描述问题和解决方案的方式,帮助开发者理解如何设计灵活、可维护和可扩展的系统。

设计模式通常分为三大类:

  1. 创建型模式(Creational Patterns):关注于对象的创建机制,确保系统在合适的地方创建合适的对象。
  2. 结构型模式(Structural Patterns):关注的是如何组合类或对象构成更大的结构。
  3. 行为型模式(Behavioral Patterns):关注的是对象之间的职责分配和通信机制。

单例模式是在面试中常见的设计模式之一,属于创建型模式。

1.单例模式

单例模式能够保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例,并通过提供一个全局访问点来访问这个实例。单例模式可以避免重复创建,减轻系统性能压力。

常见的单例模式有:1、饿汉模式;2、懒汉模式。

2.如何实现单例模式

2.1饿汉模式

饿汉模式是最简单的单例模式实现,实例在类加载时就被创建。

为什么构造方法要设置为private权限?

为了防止其他人不小new了这个对象。每次调用都需要通过getInstance方法

/**
 * 单例模式的实现类。
 * 保证一个类只有一个实例,并提供一个全局访问点。
 */
class Singleton{
    // 静态实例变量,确保在类加载时就初始化,从而实现单例。
    private static Singleton singleton=new Singleton();

    /**
     * 公共的静态方法,用于获取Singleton类的唯一实例。
     *
     * @return Singleton类的唯一实例。
     */
    public static Singleton getInstance(){
        return singleton;
    }

    // 将构造函数私有化,防止外部通过new方式创建实例,确保单例的唯一性。
    //为了让外面的不可以创建实例,单例模式,构造方法要私有化
    private Singleton(){}
}

public class Singles {
    public static void main(String[] args) {
        // 通过getInstance方法获取Singleton的实例
        Singleton s1=Singleton.getInstance();
        // 再次通过getInstance方法获取Singleton的实例
        Singleton s2=Singleton.getInstance();
        // 检查两个实例是否相同,预期输出为true
        System.out.println(s1==s2);//true
    }
}

运行之后,我们期望的结果是两个对象的实例是一样的。 

 

上述的饿汉模式在创建实例时,是在类加载时就创建好了,但如果想要让其晚一点创建呢?

那么我们就得使用懒汉模式。

2.2懒汉模式

类加载的时候不创建实例,第一次使用的时候才创建实例。饿汉模式是在通过在类里创建了一个静态的实例,静态的成员变量在类加载的时候就会被创建,这样就会导致不管是否有用到这个实例,这个实例都会被创建,这样就会造成资源浪费。而懒汉模式则是通过在类中定义一个静态成员变量,并初始化为null。当调用getInstance时,如果静态成员变量为空,那么就会创建一个实例;反之,则直接返回这个实例。

class SingleLazy{
    private static SingleLazy singleLazy=null;

    public static SingleLazy getInstance(){
        if(singleLazy==null){
            singleLazy=new SingleLazy();
        }
        return singleLazy;
    }
    
    private SingleLazy(){}
}

但如果是在多线程中,懒汉模式按照上面写真的没事吗,其实不然,若真的按照上面去实例对象,会出现线程安全问题。

3.单例模式中的线程安全问题

在上一篇中,我们已经知道了线程在操作系统中是抢占式执行、随机调度的。

对于饿汉模式,在创建对象时不会出现线程安全问题,但对于懒汉模式,会有线程安全问题。

懒汉模式的getInstance方法中,想要创建实例,是需要先通过判断再进行修改的。这种是典型的线程不安全代码,在判断和修改之间,线程可能会进行切换。

3.1加锁

 对于上述这种线程安全问题,那么我们就可以进行加锁。那这把锁加在哪里?我们知道线程安全问题是出现在if和new操作上,这两操作应该打包为一个整体。

class SingleLazy{
    private static SingleLazy singleLazy=null;

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

    private SingleLazy(){}
}

虽然通过加锁,我们解决了在判断和实例之间线程安全问题,但同时,我们还需要考虑一下,如果我们已经创建了实例,那么后续调用getInstance方法还需要进行加锁操作吗?不需要,我们在此加锁,是为了将if和new合为一个整体,防止在线程在if和new之间来回切换。

当我们实例完之后,if语句是进不去的。后续操作都是读操作。若每个线程调用getInstance方法都需要加锁,就会产生阻塞,影响性能。

3.2解决懒汉模式中频繁加锁问题

 针对上述问题,我们需要在加锁之前,判断一下当前的singlelazy对象是否为null,若为null,则进行加锁操作,并进行实例;反之,则直接返回singlelazy。

/**
 * 单例模式的懒汉式实现,确保在多线程环境下安全地创建单例对象。
 * 这种实现方式称为"双重检查锁定",既延迟了单例的初始化,又保证了线程安全性。
 */
class SingleLazy {
    // 静态实例变量,初始为null,用于存储单例对象
    private static SingleLazy singleLazy = null;

    /**
     * 静态方法,用于获取单例对象。
     * 如果实例尚未创建,则通过双重检查锁定来确保线程安全地创建单例。
     * 
     * @return 单例对象的实例
     */
    public static SingleLazy getInstance() {
        // 检查实例是否已经存在,如果不存在则进行实例化
        if (singleLazy == null) {
            // 使用synchronized关键字确保线程安全
            synchronized (SingleLazy.class) {
                // 再次检查实例是否存在,避免多线程环境下重复实例化
                if (singleLazy == null) {
                    // 实例化单例对象
                    singleLazy = new SingleLazy();
                }
            }
        }
        // 返回单例对象
        return singleLazy;
    }

    // 将构造函数设为私有,防止外部直接实例化对象
    private SingleLazy() {}
}

可能有人会疑惑,这里为什么要判断两次singlelazy是否为空?

第一次if:判断是否需要加锁

第二次if:判断是否要创建实例。 

懒汉模式的代码到这,问题都解决了吗?

还没有,在new对象时,可能会造成重排序问题。重排序问题也是造成线程安全问题的因素之一。

在new一个对象时,其实可以分为三个步骤:

  1. 向内存申请空间
  2. 执行构造方法并初始化
  3. 将内存空间的地址赋值给引用变量

 如果发生指令重排序,本来线程的执行顺序为1、2、3,但却被重排序为1、3、2,那么在线程并发执行时,可能就会出现问题。

从图中可以看到,线程t1判断完singlelazy为空之后,进入if分支创建实例时,此时若执行完两条指令,但此时线程被切换到t2,线程t2中singlelazy在判断完不为空,直接返回singlelazy对象,但由于此时的singlelazy并没有被初始化,若被使用,则会出现问题。

3.3解决new中指令重排序的线程安全问题 

对于上述问题,这种情况就是因为编译器为了提高性能,对指令的顺序进行了优化。

示例:

假如我们现在有个清单:

  1. 买西瓜
  2. 买酱油
  3. 买哈密瓜
  4. 买葡萄

若我们按着清单的顺序来买,即是:

但是一般水果都是在店里都有,所以可以进行优化:

 

但是我们在某些情况下我们不想要进行优化,那么我们就可以使用volatile关键字来防止指令重排序,保证内存的可见性。

/**
 * 单例模式的懒汉式实现,确保在多线程环境下安全地创建单例对象。
 * 这种实现方式称为"双重检查锁定",既延迟了单例的初始化,又保证了线程安全性。
 */
class SingleLazy {
    // 使用volatile修饰符确保多线程环境下的可见性,避免出现指令重排序的问题
    // 静态实例变量,初始为null,用于存储单例对象
     private static volatile SingleLazy singleLazy = null;

    /**
     * 静态方法,用于获取单例对象。
     * 如果实例尚未创建,则通过双重检查锁定来确保线程安全地创建单例。
     * 
     * @return 单例对象的实例
     */
    public static SingleLazy getInstance() {
        // 双重检查锁定的第一重检查,如果实例已经存在,则直接返回,避免不必要的同步锁定
        // 检查实例是否已经存在,如果不存在则进行实例化
        if (singleLazy == null) {
            // 使用synchronized关键字确保线程安全,避免多个线程同时进入创建实例的代码块
            // 使用synchronized关键字确保线程安全
            synchronized (SingleLazy.class) {
                // 双重检查锁定的第二重检查,再次确认实例是否已经被其他线程创建,避免重复创建实例
                // 再次检查实例是否存在,避免多线程环境下重复实例化
                if (singleLazy == null) {
                    // 实例化单例对象
                    singleLazy = new SingleLazy();
                }
            }
        }
        // 返回单例对象
        return singleLazy;
    }

    // 将构造函数设为私有,防止外部直接通过new关键字创建实例,确保单例的唯一性
    // 将构造函数设为私有,防止外部直接实例化对象
    private SingleLazy() {}
}

 所以我们在使用懒汉模式时,需要考虑三个因素:

  1. 由if和new引起的线程安全问题
  2. 频繁加锁产生的阻塞
  3. 指令重排序引起的线程安全问题

单例模式的讲解就先到这了~

若有不足,欢迎指正~ 


原文地址:https://blog.csdn.net/zhyhgx/article/details/140687510

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!