设计模式-单例模式

目录

单例模式

概要

[单例模式(Singleton Pattern)](#单例模式(Singleton Pattern))

意图

主要解决

何时使用

如何解决

关键代码

应用实例

优点

缺点

使用场景

注意事项

结构

问题描述

输入描述

输出描述

输入示例

输出示例

提示信息

构思思路

初步实现

HashMap的选择

LinkedHashMap的选择

LinkedHashMap和HashMap的异同

单例模式中的线程安全性

getInstance的线程安全性

使用synchronized关键字

使用静态初始化

两种方法的比较

总结

完整代码


在学习设计模式的过程中,单例模式是一个非常重要且常用的模式。今天我们通过一个实际的例子来深入理解单例模式及其应用。

单例模式

单例模式 | 菜鸟教程 (runoob.com)

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

概要

单例模式(Singleton Pattern)
意图

确保一个类只有一个实例,并提供一个全局访问点来访问该实例。

主要解决

频繁创建和销毁全局使用的类实例的问题。

何时使用

当需要控制实例数目,节省系统资源时。

如何解决

检查系统是否已经存在该单例,如果存在则返回该实例;如果不存在则创建一个新实例。

关键代码

构造函数是私有的。

应用实例
  • 一个班级只有一个班主任。
  • Windows 在多进程多线程环境下操作文件时,避免多个进程或线程同时操作一个文件,需要通过唯一实例进行处理。
  • 设备管理器设计为单例模式,例如电脑有两台打印机,避免同时打印同一个文件。
优点
  • 内存中只有一个实例,减少内存开销,尤其是频繁创建和销毁实例时(如管理学院首页页面缓存)。
  • 避免资源的多重占用(如写文件操作)。
缺点
  • 没有接口,不能继承。
  • 与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心实例化方式。
使用场景
  • 生成唯一序列号。
  • WEB 中的计数器,避免每次刷新都在数据库中增加计数,先缓存起来。
  • 创建消耗资源过多的对象,如 I/O 与数据库连接等。
注意事项
  • 线程安全getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成实例被多次创建。
  • 延迟初始化 :实例在第一次调用 getInstance() 方法时创建。
  • 序列化和反序列化 :重写 readResolve 方法以确保反序列化时不会创建新的实例。
  • 反射攻击:在构造函数中添加防护代码,防止通过反射创建新实例。
  • 类加载器问题:注意复杂类加载环境可能导致的多个实例问题。
结构

单例模式包含以下几个主要角色:

  • 单例类:包含单例实例的类,通常将构造函数声明为私有。
  • 静态成员变量:用于存储单例实例的静态成员变量。
  • 获取实例方法:静态方法,用于获取单例实例。
  • 私有构造函数:防止外部直接实例化单例类。
  • 线程安全处理:确保在多线程环境下单例实例的创建是安全的。

问题描述

小明去了一家大型商场,拿到了一个购物车,并开始购物。请你设计一个购物车管理器,记录商品添加到购物车的信息(商品名称和购买数量),并在购买结束后打印出商品清单。(在整个购物过程中,小明只有一个购物车实例存在)。

【设计模式专题之单例模式】1.小明的购物车 (kamacoder.com)

输入描述

输入包含若干行,每行包含两部分信息,分别是商品名称和购买数量。商品名称和购买数量之间用空格隔开。

输出描述

输出包含小明购物车中的所有商品及其购买数量。每行输出一种商品的信息,格式为 "商品名称 购买数量"。

输入示例
Apple 3
Banana 2
Orange 5
输出示例
Apple 3
Banana 2
Orange 5
提示信息

本道题目请使用单例设计模式:

使用私有静态变量来保存购物车实例。

使用私有构造函数防止外部直接实例化。

构思思路

拿到这个问题,我们首先想到的是如何确保购物车管理器在整个购物过程中只有一个实例。这就是单例模式的经典应用场景。单例模式确保一个类只有一个实例,并提供一个全局访问点。

初步实现

在最初的实现中,我们考虑使用两个ArrayList来分别存储商品名称和购买数量。这种方法虽然简单直接,但有几个明显的缺点:

  1. 商品名称和数量是分开的:这使得每次访问商品及其数量都需要同时操作两个列表,增加了代码的复杂性。

  2. 无法避免重复商品:如果用户多次添加同一种商品,我们需要额外的逻辑来合并数量。

鉴于上述问题,我们考虑使用更合适的数据结构来简化实现。

HashMap的选择

为了更好地管理商品信息,我们选择使用HashMap,它可以将商品名称映射到购买数量,解决了商品名称和数量分开的问题。以下是使用HashMap的代码示例:

java 复制代码
import java.util.HashMap;
import java.util.Map;

class ShoppingCart {
    private static ShoppingCart instance = new ShoppingCart();
    private Map<String, Integer> products;

    private ShoppingCart() {
        products = new HashMap<>();
    }

    public static synchronized ShoppingCart getInstance() {
        return instance;
    }

    public void addProduct(String name, int quantity) {
        if (products.containsKey(name)) {
            products.put(name, products.get(name) + quantity);
        } else {
            products.put(name, quantity);
        }
    }

    public void printCart() {
        for (Map.Entry<String, Integer> entry : products.entrySet()) {
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
    }
}

使用HashMap解决了大部分问题,但仍然有一个问题:HashMap不保证键值对的顺序。在某些情况下,我们需要按添加顺序输出商品清单,这就需要更换数据结构。

LinkedHashMap的选择

为了解决顺序问题,我们将HashMap替换为LinkedHashMapLinkedHashMapHashMap的不同之处在于它维护了插入顺序,从而保证了按顺序输出。

java 复制代码
import java.util.LinkedHashMap;
import java.util.Map;

class ShoppingCart {
    private static ShoppingCart instance = new ShoppingCart();
    private Map<String, Integer> products;

    private ShoppingCart() {
        products = new LinkedHashMap<>();
    }

    public static synchronized ShoppingCart getInstance() {
        return instance;
    }

    public void addProduct(String name, int quantity) {
        if (products.containsKey(name)) {
            products.put(name, products.get(name) + quantity);
        } else {
            products.put(name, quantity);
        }
    }

    public void printCart() {
        for (Map.Entry<String, Integer> entry : products.entrySet()) {
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
    }
}

LinkedHashMap和HashMap的异同

HashMap

  • 基于哈希表实现,键值对存储无序。

  • 查找和插入操作的时间复杂度为O(1)。

  • 不维护元素的插入顺序。

LinkedHashMap

  • 继承自HashMap,内部使用双向链表来维护键值对的插入顺序。

  • 同样具备HashMap的查找和插入效率,时间复杂度为O(1)。

  • 适用于需要按插入顺序或访问顺序遍历元素的场景。

单例模式中的线程安全性

getInstance的线程安全性

在单例模式中,确保getInstance方法的线程安全性至关重要。在多线程环境下,如果不加以保护,可能会导致创建多个实例。我们可以通过两种方式来确保线程安全:使用同步(synchronized)关键字和静态初始化。

使用synchronized关键字

通过在getInstance方法上添加synchronized关键字,可以确保同一时间只有一个线程能够执行该方法,从而保证了单例实例的唯一性。虽然这种方法很直观,但会带来一定的性能开销,因为每次调用getInstance方法时都需要进行同步。

代码实现:

java 复制代码
class ShoppingCart {
    private static ShoppingCart instance;
    private Map<String, Integer> products;

    private ShoppingCart() {
        products = new LinkedHashMap<>();
    }

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

    public void addProduct(String name, int quantity) {
        if (products.containsKey(name)) {
            products.put(name, products.get(name) + quantity);
        } else {
            products.put(name, quantity);
        }
    }

    public void printCart() {
        for (Map.Entry<String, Integer> entry : products.entrySet()) {
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
    }
}
使用静态初始化

静态初始化利用Java类加载机制,在类加载时就创建实例,从而天然地避免了多线程并发问题。静态初始化的方式既简单又高效,因为不需要额外的同步开销。

代码实现:

java 复制代码
class ShoppingCart {
    private static final ShoppingCart instance = new ShoppingCart();
    private Map<String, Integer> products;

    private ShoppingCart() {
        products = new LinkedHashMap<>();
    }

    public static ShoppingCart getInstance() {
        return instance;
    }

    public void addProduct(String name, int quantity) {
        if (products.containsKey(name)) {
            products.put(name, products.get(name) + quantity);
        } else {
            products.put(name, quantity);
        }
    }

    public void printCart() {
        for (Map.Entry<String, Integer> entry : products.entrySet()) {
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
    }
}
两种方法的比较
  1. 同步(synchronized)方法

    • 优点:实现简单,延迟实例化(即只有在第一次使用时才创建实例)。

    • 缺点 :每次调用getInstance方法时都需要进行同步,带来性能开销。

  2. 静态初始化方法

    • 优点:实现简单且高效,不需要进行同步,实例在类加载时即被创建,避免了线程安全问题。

    • 缺点:无法延迟实例化,如果实例创建的开销较大且不一定会使用到,可能会造成资源浪费。

单例模式在多线程环境中的实现有多种方式,选择哪种方式主要取决于具体的应用场景和需求。通过在getInstance方法上添加synchronized关键字,可以确保线程安全性,但会有一定的性能开销。而静态初始化方法则在保证线程安全的同时具有更好的性能,但会在类加载时就创建实例。

总结

通过这个例子,我们不仅理解了单例模式的基本概念和实现方法,还学习了如何选择合适的数据结构来解决实际问题。LinkedHashMap的使用保证了插入顺序,从而满足了按顺序输出商品清单的需求。同时,线程安全的单例模式确保了购物车管理器在多线程环境下的正确性。希望通过这个案例,大家能更好地掌握单例模式及其在实际开发中的应用。

完整代码

java 复制代码
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Scanner;

class ShoppingCart {
    private static ShoppingCart instance = new ShoppingCart();
    private Map<String, Integer> products;

    private ShoppingCart() {
        products = new LinkedHashMap<>();  // 使用 LinkedHashMap 保证插入顺序
    }

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

    public void addProduct(String name, int quantity) {
        if (products.containsKey(name)) {
            products.put(name, products.get(name) + quantity);
        } else {
            products.put(name, quantity);
        }
    }

    public void printCart() {
        for (Map.Entry<String, Integer> entry : products.entrySet()) {
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
    }
}

public class Main {
    public static void main(String[] args) {
        ShoppingCart cart = ShoppingCart.getInstance();
        Scanner scanner = new Scanner(System.in);

        String inputLine;
        while (scanner.hasNextLine()) {
            inputLine = scanner.nextLine();

            if ("exit".equalsIgnoreCase(inputLine)) {
                break;
            }

            String[] parts = inputLine.split(" ");

            if (parts.length == 2) {
                String name = parts[0];
                int quantity;

                try {
                    quantity = Integer.parseInt(parts[1]);
                    cart.addProduct(name, quantity);
                } catch (NumberFormatException e) {
                    System.out.println("输入错误,请重新输入");
                }
            } else {
                System.out.println("输入错误,请重新输入");
            }
        }

        scanner.close();
        cart.printCart();
    }
}
相关推荐
GoodStudyAndDayDayUp6 分钟前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
信号处理学渣14 分钟前
matlab画图,选择性显示legend标签
开发语言·matlab
红龙创客15 分钟前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
jasmine s24 分钟前
Pandas
开发语言·python
装不满的克莱因瓶36 分钟前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb
n北斗44 分钟前
常用类晨考day15
java
biomooc44 分钟前
R 语言 | 绘图的文字格式(绘制上标、下标、斜体、文字标注等)
开发语言·r语言
骇客野人1 小时前
【JAVA】JAVA接口公共返回体ResponseData封装
java·开发语言
black^sugar1 小时前
纯前端实现更新检测
开发语言·前端·javascript
404NooFound1 小时前
Python轻量级NoSQL数据库TinyDB
开发语言·python·nosql