Java设计模式之单例模式

一、单例模式概述

1.1.模式介绍

单例模式是结构最简单的设计模式,在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于被外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。

 

1.2.模式定义

单例模式(Singleton Pattern)定义:单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。

 

二、单例模式结构与分析

2.1.模式结构

单例模式只包含一个 Singleton(单例角色)类:在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以使用它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个 Singleton 类型的静态对象,作为外部共享的唯一实例。

 

2.2.模式分析

单例模式的目的是保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式包含的角色只有一个,就是单例类——Singleton。单例类拥有一个私有构造函数,确保用户无法通过 new关键字直接实例化它。除此之外,该模式中包含一个静态私有成员变量与静态公有的工厂方法,该工厂方法负责检验实例的存在性并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。
一般情况下,单例模式的实现代码如下:

public class Singleton {
    private static Singleton instance = null;//静态私有成员变量
    //私有构造函数
    
    private Singleton() {
    }

    //静态共有工厂方法,返回唯一实例
    public static Singleton getInstance() {
        if (instance == null)
            instance = new Singleton();
        return instance;
    }
}

 

三、单例模式实例

下面将举个例子来更好的理解单例模式。

 

3.1.身份证号码

在现实生活中,身份证号码具有唯一性,同一个人不允许有多个身份证号码,第一次申请身份证时将给居民分配一个身份证号码,如果之后因为遗失等原因补办时,还是使用原来的身份证号码,不会产生新的号码。现使用单例模式模拟该场景。

实例代码:

public class IdCardNo {
    private static IdCardNo instance = null;
    private String no;

    private IdCardNo() {
    }

    public String getNo() {
        return no;
    }

    //setNo私有方法设置身份证号码
    private void setNo(String no) {
        this.no = no;
    }

    public static IdCardNo getInstance() {
        if (instance == null) {
            System.out.println("首次办理身份证,将分配新号码");
            instance = new IdCardNo();
            instance.setNo("0000000000");
        } else {
            System.out.println("非首次办理身份证,将获取旧号码");
        }
        return instance;
    }
}

测试代码:

public class IdCardNoTest {
    public static void main(String[] args) {
        IdCardNo id1, id2;
        id1 = IdCardNo.getInstance();
        id2 = IdCardNo.getInstance();

        String str1, str2;
        str1 = id1.getNo();
        str2 = id2.getNo();
        System.out.println("第一次号码:" + str1);
        System.out.println("第二次号码:" + str2);
    }
}

结果及分析:

首次办理身份证,将分配新号码
非首次办理身份证,将获取旧号码
第一次号码:0000000000
第二次号码:0000000000

两次创建的IdCardNo对象内存地址均相同,是同一个对象;号码no属性的值不仅相同,其地址也一样,是同一个成员属性。

 

四、单例模式再分类

4.1懒汉模式

public class Singleton {
    private static Singleton instance;
    
    private Singleton() {
        // 私有构造函数,防止外部直接实例化
    }
    
    public static Singleton getInstance() {
        if (instance == null) {  // 只有在第一次调用时才创建实例
            instance = new Singleton();
        }
        return instance;
    }
    
    // 其他成员方法
}

 

在这个例子中,Singleton 类通过将构造函数设为私有,确保其他类无法直接实例化它。getInstance 方法通过检查实例是否已经存在来返回类的唯一实例。如果实例不存在,它将被创建;如果已经存在,它将直接返回现有实例。

这里需要注意,懒汉模式的实现在多线程环境下可能会出现问题,因为当多个线程同时检查实例是否为null时,可能会导致创建多个实例。要解决这个问题,可以采用加锁的方式来保证线程安全,或者采用双重检查锁定(Double-Checked Locking)等方式。

4.1.1双重检查锁定(Double-Checked Locking)并使用 volatile 关键字

双重检查锁定是一种常见的在多线程环境下使用懒汉模式的实现方式。其核心思想是在获取实例的过程中进行双重检查,以保证只有第一次创建实例时进行同步操作。
以下是使用双重检查锁定实现单例模式的示例代码:

public class Singleton {
    private volatile static Singleton instance;

    private Singleton() {
        // 私有构造函数,防止外部直接实例化
    }

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

    // 其他成员方法
}

在这个例子中,首先检查实例是否已经创建,若未创建,则进入同步块。在同步块中再次检查实例是否为空,如果是,则创建实例。外层的if判断是为了避免每次调用getInstance()方法都进入同步块,提升性能。双重检查锁定通过使用volatile关键字来确保在实例初始化期间,多线程能正确地处理实例。

在上述的双重检查锁定实现方式中,使用了volatile关键字来保证实例的可见性和有序性。volatile关键字用于保证变量在多线程环境下的可见性,即当一个线程修改了共享变量的值,其他线程能够立即看到最新的值。这样就可以防止一个线程在使用未完全构造的实例时发生问题。

 

4.2饿汉模式

public class Singleton {
    private static Singleton instance = new Singleton();
    
    private Singleton() {
        // 私有构造函数,防止外部直接实例化
    }
    
    public static Singleton getInstance() {
        return instance;
    }
    
    // 其他成员方法
}

在这个例子中,Singleton 类在类加载时就创建了实例,并将其赋值给静态变量instance。由于在类加载时就创建了实例,所以这种实现方式被称为饿汉模式。

饿汉模式的优点是实现简单,线程安全,无需考虑多线程环境下的同步问题。然而,它的缺点是不具备延迟加载的特性。即使在没有使用该实例的情况下,它仍然会被创建和初始化。

根据具体的需求,你可以选择懒汉模式或饿汉模式来实现单例类。懒汉模式适用于延迟加载的场景,而饿汉模式适用于对资源的实例进行提前初始化并且需要确保线程安全性的场景。

 

4.3使用静态内部类实现单例模式

在上面的示例中,我展示了使用懒汉模式和饿汉模式实现单例模式的 Java 代码。这两种方式都是常见的单例模式实现方法,它们都有各自的优缺点和适用场景。除此之外,还可以使用静态内部类实现单例模式,这种方法既能实现延迟加载,又能保证线程安全,是一种比较优雅的实现方式。

下面是基于静态内部类的单例模式示例代码:

public class Singleton {
    private Singleton() {
        // 私有构造函数,防止外部直接实例化
    }
    
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    
    // 其他成员方法
}

在这个示例中,当第一次加载 Singleton 类时并不会初始化 SingletonHolder,只有在第一次调用 getInstance() 方法时才会导致 SingletonHolder 被加载,这样既实现了延迟加载,也能保证线程安全性,因为类的静态初始化阶段是线程安全的。

通过静态内部类实现的单例模式,兼具了懒汉模式和饿汉模式的优点,是一种推荐的单例模式实现方法。

 

五、单例模式的优缺点

单例模式具有以下优点和缺点:

优点:

  1. 对于某些只需要一个实例的对象,可以避免频繁地创建和销毁对象,提高了内存和性能的利用效率。
  2. 提供了对唯一实例的集中化控制和管理,使得对该实例的访问变得简单和统一。
  3. 全局可访问,方便在系统的不同部分中共享和调用该实例。

缺点:

  1. 违反了单一职责原则(SRP),因为该类既要承担自己的功能,又要负责管理自己的实例。这可能导致代码膨胀和耦合性增加。
  2. 可能会引入全局状态,使得程序的调试和测试变得困难,也增加了代码的复杂性和维护成本。
  3. 不适合需要多个实例的场景,因为单例模式只能创建一个实例,无法满足多实例的需求。
  4. 可能存在并发访问的问题,特别是在多线程环境下,需要对单例实例的访问加锁来保证线程安全性,这可能会导致性能下降。

因此,在使用单例模式时,需要权衡其优点和缺点,并在合适的场景下进行使用。如果确实有需要全局管理且只需要一个实例的对象,单例模式是一种简单有效的设计模式选择。

 

你可能感兴趣的:(java,设计模式,单例模式)