设计模式之单例模式

简介: 设计模式之单例模式

一、介绍

单例模式,属于创建型设计模式,单指一个,例指实例,就是说在一个程序的运行过程中,应用了单例模式的类始终只有一个实例,且不允许出现多个实例,并在整个程序中提供一个获取该实例的方法。

应用:

  • spring中的bean默认就是单例模式,类的实例化由spring完成,我们只需要从框架中获取即可,而不是直接去new一个实例。
  • web应用中的Servlet
  • 我们在应用开发时,线程池也应当设计成单例模式。

何时使用:如果一个类的实例在整个生命周期中是无状态的,则可以使用单例模式使整个应用程序中只有一个实例。

优点:在内存里只有一个实例,减少了内存的开销,同时避免了频繁的创建实例与销毁实例。

注意:

  • 单例类只能有一个实例
  • 必须在单例类内部完成该类实例的创建
  • 单例类应提供获取该实例的方法
  • 不允许通过构造方法(即new的方式)来获取实例。

二、实现步骤

实现单例模式的方式有很多,总结起来为三个步骤:

① 将构造方法私有化

② 在类内部定义一个唯一的实例化对象,且使用private statis修饰。

③ 定义一个静态方法访问该实例对象。

三、饿汉式

饿汉式是最简单的单例模式实现,

基于上面三个步骤,饿汉式的实现如下:

public class Person {
   
   

    //  在类内部定义一个唯一的实例化对象,且使用private statis修饰
    private static Person person = new Person();

    /**
     * 将构造方法私有化
     */
    private Person() {
   
   
    }

    public static Person getInstance() {
   
   
        return person;
    }
}

在程序启动时对该类进行实例化并赋值给person属性,因此又被称为立即加载。由于其构造方法被私有化,调用方只能通过静态方法getInstance()获取该实例。

优点:

  • 实现简单
  • 线程安全

缺点:

  • Person类被加载时,静态变量person被初始化并分配其所需的内存空间,直至程序运行结束该内存空间才会被释放,即使我们从始至终都没有使用该实例,所以造成了一定的内存空间的浪费

四、简单懒汉式

针对饿汉式单例实现的缺点,我们对其进行改造:该类被加载时先不要对其person属性进行初始化,当我们需要使用该实例时,才对person属性进行初始化。就是说把person属性的初始化延迟到调用getInstance()方法时,因此该实现方式也称为延迟加载

下面我们通过代码对饿汉式进行改造

public class Person {
   
   

    private static Person person;

    private Person() {
   
   
    }

    public static Person getInstance() {
   
   
        // 将实例化延迟进行
        if (person == null) {
   
   
            person = new Person();
        }
        return person;
    }
}

优点

  • 实现简单
  • 一定程度上节省内存空间。如果在程序运行过程中从不调用getInstance()方法获取实例,则永远不会对Person类进行实例化。

缺点

  • 线程不安全。当多个线程同时调用getInstance()方法获取实例时,由于此时person==null成立,那么这些线程则无一例外都会对Person类进行实例化。

1. 线程不安全

下面我们通过在idea上模拟多线程环境,对该实现进行测试。

  • 在判断person==null时添加多线程模式的断点,如下图所示

    懒汉式添加多线程模式的断点.jpg

  • 新建三个线程,在run()方法中调用Person.getInstance()获取实例。

    public static void main(String[] args) {
         
         
    
        Thread thread1 = new Thread(() -> {
         
         
            Person person = Person.getInstance();
            // 输出该对象的内存地址
            System.out.println(System.identityHashCode(person));
        });
    
        Thread thread2 = new Thread(() -> {
         
         
            Person person = Person.getInstance();
            System.out.println(System.identityHashCode(person));
        });
    
        Thread thread3 = new Thread(() -> {
         
         
            Person person = Person.getInstance();
            System.out.println(System.identityHashCode(person));
        });
    
        thread1.start();
        thread2.start();
        thread3.start();
    }
    
  • 使用Debug模式启动程序

    此时我们在左下角可以看到三个线程都进入断点,并且三个线程中person==null均为true,则意味着三个线程都会进入person = new Person()这一行代码,那么也就表示三个线程都会对Person类进行一次实例化并赋值给person属性。

    多个线程进入断点.jpg

我们每个线程都会输出获取的实例对象的内存地址,通过System.identityHashCode()方法获取。下图为三个线程获取的实例对象的内存地址。

线程不安全时实例对象的内存地址.jpg

五、线程安全懒汉式

针对于上面线程不安全的懒汉式实现方案,为了保证线程安全,最简单粗暴的方式就是加锁,例如使用synchronized关键字。

于是我们对其进行改造,代码如下

public class Person {
   
   

    private static Person person;

    private Person() {
   
   
    }

    // 添加synchronized关键字,保证线程安全
    public static synchronized Person getInstance() {
   
   
        if (person == null) {
   
   
            person = new Person();
        }
        return person;
    }
}

此时,无论多少个线程同时调用getInstance()方法,在synchronized关键字的加持下,只允许有一个拿到锁的线程进入方法内部。当第一个拿到锁的线程对person属性实例化完成后,后续拿到锁的线程都会直接将person返回,而不再对其进行初始化。但是,虽然实现了线程安全,此实现方案仍有缺点,即便person属性已经被第一个线程完成初始化,后续的线程却依然因拿不到锁而漫长地等待,这对性能无疑是一种损耗,典型的时间换空间

优点

  • 线程安全

缺点

  • 性能大打折扣

六、双重检查锁的线程安全懒汉式

该方案通过双重检测(Double Check) + 锁的机制,既满足了线程安全的需求,同时又能避免性能的损耗。

public class Person {
   
   

    // volatile关键字使得person对象在多线程环境下彼此可见
    private static volatile Person person;

    private Person() {
   
   
    }

    public static Person getInstance() {
   
   
        // 第一次检查person是否被实例化出来,如果没有进入if块
        if (person == null) {
   
   
            synchronized (Person.class) {
   
   
                // 第一个线程拿到锁,实例化对象前第二次检查person是否已经被实例化出来,如果没有,才最终实例出对象
                if (person == null) {
   
   
                    person = new Person();
                }
            }
        }
        return person;
    }
}

由于java内存模型的设计,在此方案中应对person属性添加volatile关键字才能实现真正的延迟加载。

优点

  • 延迟加载
  • 线程安全

缺点

  • 影响性能。因为该方案的线程安全是通过synchronized关键字实现的,该关键字本身对程序就是一种消耗。

七、静态内部类

有没有一种既能满足延迟加载,又能保证线程安全的实现方案呢?答案是肯定的。

我们可以利用java的语法特性来满足要求,我们都知道,一个类的静态内部类是延迟加载的,当我们仅使用外部类的成员变量或成员方法而不涉及其静态内部类时,静态内部类是不会被加载的。

public class Person {
   
   

    private Person() {
   
   
    }

    public static Person getInstance() {
   
   

        return LazyPerson.person;
    }

    private static class LazyPerson {
   
   
        private static final Person person = new Person();
    }
}

这种方式既避免了饿汉式单例的内存浪费问题,又摆脱了synchronized关键字的性能问题,同时也不存在线程安全问题。

八、总结

  • 无论通过哪种方式实现单例,都需要将构造方法私有化,避免外部通过new的方式创建多个实例。
  • 单例的类内部应定义一个获取单例对象的静态方法
  • 单例模式的实现有多种,立即加载延时加载线程安全线程不安全等。




纸上得来终觉浅,绝知此事要躬行。

————————我是万万岁,我们下期再见————————

相关文章
|
3月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
35 2
|
1月前
|
设计模式 存储 前端开发
前端必须掌握的设计模式——单例模式
单例模式是一种简单的创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。适用于窗口对象、登录弹窗等场景,优点包括易于维护、访问和低消耗,但也有安全隐患、可能形成巨石对象及扩展性差等缺点。文中展示了JavaScript和TypeScript的实现方法。
|
1月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
28 2
|
2月前
|
设计模式 Java 数据库连接
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
47 4
|
2月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
2月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
2月前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入理解与应用
【10月更文挑战第22天】 在软件开发中,设计模式是解决特定问题的通用解决方案。本文将通过通俗易懂的语言和实例,深入探讨PHP中单例模式的概念、实现方法及其在实际开发中的应用,帮助读者更好地理解和运用这一重要的设计模式。
29 1
|
2月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
31 0
|
3月前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入解析与实践
在PHP开发中,设计模式是提高代码可维护性、扩展性和复用性的关键技术之一。本文将通过探讨单例模式,一种最常用的设计模式,来揭示其在PHP中的应用及优势。单例模式确保一个类仅有一个实例,并提供一个全局访问点。通过实际案例,我们将展示如何在PHP项目中有效实现单例模式,以及如何利用这一模式优化资源配置和管理。无论是PHP初学者还是经验丰富的开发者,都能从本文中获得有价值的见解和技巧,进而提升自己的编程实践。
|
3月前
|
设计模式 安全 Java
C# 一分钟浅谈:设计模式之单例模式
【10月更文挑战第9天】单例模式是软件开发中最常用的设计模式之一,旨在确保一个类只有一个实例,并提供一个全局访问点。本文介绍了单例模式的基本概念、实现方式(包括饿汉式、懒汉式和使用 `Lazy<T>` 的方法)、常见问题(如多线程和序列化问题)及其解决方案,并通过代码示例详细说明了这些内容。希望本文能帮助你在实际开发中更好地应用单例模式,提高代码质量和可维护性。
98 1