Java设计模式(二):单例模式

简介: **单例设计模式**(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式

1.定义

单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

2.使用场景

单例模式应用的场景一般发现在以下条件下:

1)资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。

2)控制资源的情况下,方便资源之间的互相通信。如线程池等。

从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。比如,配置信息类、数据库连接池、日志对象等等。在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。如果资源对象存在多个可能造成资源冲突、数据不一致等问题。

如下面的唯一递增 ID 号码生成器,如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器类设计为单例

import java.util.concurrent.atomic.AtomicLong;
public class IdGenerator {
   
  // AtomicLong是一个Java并发库中提供的一个原子变量类型,
  // 它将一些线程不安全需要加锁的复合操作封装为了线程安全的原子操作,
  // 比如下面会用到的incrementAndGet().
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator();
  private IdGenerator() {
   }
  public static IdGenerator getInstance() {
   
    return instance;
  }
  public long getId() {
    
    return id.incrementAndGet();
  }
}

// IdGenerator使用举例
long id = IdGenerator.getInstance().getId();

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨群:Shepherd_126

3.单例模式的实现方式

要实现一个单例,我们需要关注一下几点:

1)构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;

2)考虑对象创建时的线程安全问题;

3)考虑是否支持延迟加载;

4)考虑 getInstance() 性能是否高(是否加锁)

针对以上几个注意点,实现单例方式大概有以下几种:

饿汉式

饿汉式的实现方式比较简单。在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载(在真正用到 IdGenerator 的时候,再创建实例),从名字中我们也可以看出这一点,不关三七二十一先创建单例对象再说。具体的代码实现如下所示:

public class IdGenerator {
    
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator();

  private IdGenerator() {
   }

  public static IdGenerator getInstance() {
   
    return instance;
  }

  public long getId() {
    
    return id.incrementAndGet();
  }
}

因为不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化,不过对此看法仁者见仁智者见智罢了,因为凡事都是有两面性的,这种饿汉式加载是典型的空间换时间,虽然我们在项目启动时加载实例化单例对象花费了空间和时间,但是相对于用的时候再加载实例化的好处就是用的时候不用花费时间去创建,极大提高了接口响应速度。

懒汉式

通过上面知道饿汉式不支持延迟加载,所以懒汉式相对于饿汉式的优势是支持延迟加载,具体实现如下:

public class IdGenerator {
    
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;

  private IdGenerator() {
   }

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

  public long getId() {
    
    return id.incrementAndGet();
  }
}

不过懒汉式的缺点也很明显,我们给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度很低。量化一下的话,并发度是 1,也就相当于串行操作了。而这个函数是在单例使用期间,一直会被调用。如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。

双重检测式

饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。那我们再来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式。

在这种实现方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了。所以,这种实现方式解决了懒汉式并发度低的问题。具体的代码实现如下所示:

public class IdGenerator {
    
  private AtomicLong id = new AtomicLong(0);
  private volatile static IdGenerator instance;

  private IdGenerator() {
   }

  public static IdGenerator getInstance() {
   
    if (instance == null) {
   
      synchronized(IdGenerator.class) {
    // 此处为类级别的锁
        if (instance == null) {
   
          instance = new IdGenerator();
        }
      }
    }
    return instance;
  }

  public long getId() {
    
    return id.incrementAndGet();
  }
}

静态内部类

再来看一种比双重检测更加简单的实现方法,那就是利用 Java 的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。具体是怎么做到的呢?我们先来看它的代码实现

public class IdGenerator {
    
  private AtomicLong id = new AtomicLong(0);

  private IdGenerator() {
   }

  //内部类
  private static class SingletonHolder{
   
    private static final IdGenerator instance = new IdGenerator();
  }

  public static IdGenerator getInstance() {
   
    return SingletonHolder.instance;
  }

  public long getId() {
    
    return id.incrementAndGet();
  }
}

SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载

枚举实现

基于枚举类型的单例实现是一种最简单的实现方式。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。具体的代码如下所示:

public enum IdGenerator {
   
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);

  public long getId() {
    
    return id.incrementAndGet();
  }
}

调用方法:

public class Main {
   

    public static void main(String[] args) {
   
        IdGenerator.INSTANCE.getId();
    }

}

4.单例模式存在的问题

大部分情况下,我们在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID 生成器类。单例模式书写简洁、使用方便,在代码中,我们不需要创建对象,直接通过类似 IdGenerator.getInstance().getId() 这样的方法来调用就可以了。但是,这种使用方法有点类似硬编码(hard code),会带来诸多问题。

1)不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
2)由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
3)单例类的职责过重,在一定程度上违背了“单一职责原则”。
4)滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

使用注意事项:
1)使用时不能用反射模式创建单例,否则会实例化一个新的对象
2)使用懒单例模式时注意线程安全问题
3)饿单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承。

目录
相关文章
|
2天前
|
设计模式 存储 Java
【十】设计模式~~~结构型模式~~~享元模式(Java)
文章详细介绍了享元模式(Flyweight Pattern),这是一种对象结构型模式,通过共享技术实现大量细粒度对象的重用,区分内部状态和外部状态来减少内存中对象的数量,提高系统性能。通过围棋棋子的设计案例,展示了享元模式的动机、定义、结构、优点、缺点以及适用场景,并探讨了单纯享元模式和复合享元模式以及与其他模式的联用。
【十】设计模式~~~结构型模式~~~享元模式(Java)
|
2天前
|
设计模式 存储 Java
【九】设计模式~~~结构型模式~~~外观模式(Java)
文章详细介绍了外观模式(Facade Pattern),这是一种对象结构型模式,通过引入一个外观类来简化客户端与多个子系统之间的交互,降低系统的耦合度,并提供一个统一的高层接口来使用子系统。通过文件加密模块的实例,展示了外观模式的动机、定义、结构、优点、缺点以及适用场景,并讨论了如何通过引入抽象外观类来提高系统的可扩展性。
【九】设计模式~~~结构型模式~~~外观模式(Java)
|
2天前
|
设计模式 Java
【八】设计模式~~~结构型模式~~~装饰模式(Java)
文章详细介绍了装饰模式(Decorator Pattern),这是一种对象结构型模式,用于在不使用继承的情况下动态地给对象添加额外的职责。装饰模式通过关联机制,使用装饰器类来包装原有对象,并在运行时通过组合的方式扩展对象的行为。文章通过图形界面构件库的设计案例,展示了装饰模式的动机、定义、结构、优点、缺点以及适用场景,并提供了Java代码实现和应用示例。装饰模式提高了系统的灵活性和可扩展性,适用于需要动态、透明地扩展对象功能的情况。
【八】设计模式~~~结构型模式~~~装饰模式(Java)
|
2天前
|
设计模式 XML 存储
【七】设计模式~~~结构型模式~~~桥接模式(Java)
文章详细介绍了桥接模式(Bridge Pattern),这是一种对象结构型模式,用于将抽象部分与实现部分分离,使它们可以独立地变化。通过实际的软件开发案例,如跨平台视频播放器的设计,文章阐述了桥接模式的动机、定义、结构、优点、缺点以及适用场景,并提供了完整的代码实现和测试结果。桥接模式适用于存在两个独立变化维度的系统,可以提高系统的可扩展性和灵活性。
【七】设计模式~~~结构型模式~~~桥接模式(Java)
|
2天前
|
设计模式 存储 负载均衡
【五】设计模式~~~创建型模式~~~单例模式(Java)
文章详细介绍了单例模式(Singleton Pattern),这是一种确保一个类只有一个实例,并提供全局访问点的设计模式。文中通过Windows任务管理器的例子阐述了单例模式的动机,解释了如何通过私有构造函数、静态私有成员变量和公有静态方法实现单例模式。接着,通过负载均衡器的案例展示了单例模式的应用,并讨论了单例模式的优点、缺点以及适用场景。最后,文章还探讨了饿汉式和懒汉式单例的实现方式及其比较。
【五】设计模式~~~创建型模式~~~单例模式(Java)
|
2天前
|
设计模式 算法 安全
Java编程中的设计模式:提升代码的可维护性和扩展性
【8月更文挑战第19天】在软件开发的世界里,设计模式是解决常见问题的一种优雅方式。本文将深入探讨Java编程语言中常用的几种设计模式,并解释如何通过这些模式来提高代码的可维护性和扩展性。文章不涉及具体的代码实现,而是侧重于理论和实践相结合的方式,为读者提供一种思考和改善现有项目的新视角。
|
2天前
|
设计模式 XML 存储
【六】设计模式~~~结构型模式~~~适配器模式(Java)
文章详细介绍了适配器模式(Adapter Pattern),这是一种结构型设计模式,用于将一个类的接口转换成客户期望的另一个接口,使原本不兼容的接口能够一起工作,提高了类的复用性和系统的灵活性。通过对象适配器和类适配器两种实现方式,展示了适配器模式的代码应用,并讨论了其优点、缺点以及适用场景。
|
2天前
|
设计模式 Java
常用设计模式介绍~~~ Java实现 【概念+案例+代码】
文章提供了一份常用设计模式的全面介绍,包括创建型模式、结构型模式和行为型模式。每种设计模式都有详细的概念讲解、案例说明、代码实例以及运行截图。作者通过这些模式的介绍,旨在帮助读者更好地理解源码、编写更优雅的代码,并进行系统重构。同时,文章还提供了GitHub上的源码地址,方便读者直接访问和学习。
常用设计模式介绍~~~ Java实现 【概念+案例+代码】
|
2天前
|
设计模式 算法 Java
【十六】设计模式~~~行为型模式~~~策略模式(Java)
文章详细介绍了策略模式(Strategy Pattern),这是一种对象行为型模式,用于定义一系列算法,将每个算法封装起来,并使它们可以相互替换。策略模式让算法独立于使用它的客户而变化,提高了系统的灵活性和可扩展性。通过电影院售票系统中不同类型用户的打折策略案例,展示了策略模式的动机、定义、结构、优点、缺点以及适用场景,并提供了Java代码实现和测试结果。
【十六】设计模式~~~行为型模式~~~策略模式(Java)
|
2天前
|
设计模式 网络协议 Java
【十五】设计模式~~~行为型模式~~~状态模式(Java)
文章详细介绍了状态模式(State Pattern),这是一种对象行为型模式,用于处理对象在其内部状态改变时的行为变化。文中通过案例分析,如银行账户状态管理和屏幕放大镜工具,展示了状态模式的应用场景和设计方法。文章阐述了状态模式的动机、定义、结构、优点、缺点以及适用情况,并提供了Java代码实现和测试结果。状态模式通过将对象的状态和行为封装在独立的状态类中,提高了系统的可扩展性和可维护性。
【十五】设计模式~~~行为型模式~~~状态模式(Java)