java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java 单例模式

详解Java设计模式之单例模式

作者:十二Y

单例模式是一种创建型设计模式,它的目的是确保一个类只有一个实例,并提供一个全局访问点来访问该实例,在单例模式中,类自身负责创建自己的唯一实例,并确保在系统中只有一个实例存在,本文详细介绍了Java设计模式中的单例模式,感兴趣的同学可以参考阅读

1、单例模式介绍

单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。

通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你 实例化多个对象。一个最好的办法就是,让类自身负责保存它的唯一实例。 这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例 的方法。

1.1 单例模式结构图

Singleton类,定义一个GetInstance操作,允许客户访问它的唯一实例。GetInstance是一个静态方法,主要负责创建自己的唯一实例。

/**
 * @author Shier
 * CreateTime 2023/5/14 9:23
 * 单例模式
 */
public class Singleton {
    private static Singleton singleton;
    /**
     * 无参构造 防止外部代码利用new来实例化的可能
     */
    private Singleton() {
    }
    /**
     * 只能通过此途径获取Singleton实例
     * @return
     */
    public static Singleton getInstance() {
        // 为空,则创建实例
        if (singleton == null) {
            singleton = new Singleton();
        }
        // 不为空,则已创建,直接返回实例
        return singleton;
    }
}

客户端:

/**
 * @author Shier
 * CreateTime 2023/5/14 9:27
 */
public class SingletonClient {
    public static void main(String[] args) {
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        if (instance1 == instance2) {
            System.out.println("两个是同一个实例对象");
        }
    }
}

最终得到的:“两个是同一个实例对象”,因此单例模式保证了可以被唯一实例化。当然单例模式不止这些好处。下面再来细说

2、具体例子说明

假设我们有一个流水号生成器,用于在系统中生成唯一的流水号。这个流水号生成器需要保证在整个系统中只有一个实例存在,以确保生成的流水号是唯一的。也就是类似于我们在外面吃饭点菜呀什么的,买单后,商家会给你一个单号,到你了,就让你去拿属于你的饭菜。

2.1 不使用单例模式

首先,我们创建一个名为SerialNumberGenerator的类,用于生成流水号:

/**
 * @author Shier
 * CreateTime 2023/5/14 9:30
 * 流水号例子
 */
public class SerialNumberGenerator {
    private int serialNumber;
    /**
     * 外界可以实例化SerialNumberGenerator
     */
    public SerialNumberGenerator() {
        serialNumber = 0;
    }
    public int generateSerialNumber() {
        serialNumber++;
        return serialNumber;
    }
}

现在,我们可以在不同的地方创建多个SerialNumberGenerator的实例并尝试生成流水号:

/**
 * @author Shier
 * CreateTime 2023/5/14 9:31
 * 客户端
 */
public class Main {
    public static void main(String[] args) {
        SerialNumberGenerator generator1 = new SerialNumberGenerator();
        int serialNumber1 = generator1.generateSerialNumber();
        System.out.println("流水号1: " + serialNumber1);
        SerialNumberGenerator generator2 = new SerialNumberGenerator();
        int serialNumber2 = generator2.generateSerialNumber();
        System.out.println("流水号2: " + serialNumber2);
    }
}

结果:

流水号1: 1
流水号2: 1

不使用单例模式存在的问题:

2.2 使用单例模式

使用单例模式的实现方式有多种,这里我将使用线程安全的懒汉式单例模式,具体实现如下:

懒汉式单例模式下面再介绍

/**
 * @author Shier
 */
public class SerialNumberGenerator1 {
    private static volatile SerialNumberGenerator instance;
    private int serialNumber;
    /**
     * 防止直接实例化SerialNumberGenerator1
     */
    private SerialNumberGenerator1() {
        serialNumber = 0;
    }
    /**
     * 获取SerialNumberGenerator1的唯一方式
     * @return
     */
    public static SerialNumberGenerator getInstance() {
        if (instance == null) {
            synchronized (SerialNumberGenerator.class) {
                if (instance == null) {
                    instance = new SerialNumberGenerator();
                }
            }
        }
        return instance;
    }
    /**
     * 生成的流水号
     * @return
     */
    public int generateSerialNumber() {
        synchronized (SerialNumberGenerator.class) {
            serialNumber++;
            return serialNumber;
        }
    }
}

在这个实现中,我们使用了一个静态变量instance来保存SerialNumberGenerator的唯一实例。getInstance()方法返回这个实例,如果它不存在,则会创建一个新的实例。generateSerialNumber()方法负责生成流水号,使用synchronized关键字来确保线程安全。

现在,我们可以在不同的地方调用SerialNumberGenerator.getInstance().generateSerialNumber()来生成流水号,并确保它们是唯一的:

/**
 * @author Shier
 */
public class Main1 {
    public static void main(String[] args) {
        int serialNumber1 = SerialNumberGenerator1.getInstance().generateSerialNumber();
        System.out.println("流水号 1: " + serialNumber1);
        int serialNumber2 = SerialNumberGenerator1.getInstance().generateSerialNumber();
        System.out.println("流水号 2: " + serialNumber2);
    }
}

通过使用单例模式,我们解决了不使用单例模式存在的问题:

使用单例模式来实现流水号生成器类,可以确保生成的流水号是唯一的,避免资源浪费,并保证生成的流水号顺序一致。所有部分都可以通过访问流水号生成器的全局访问点来获取唯一的流水号,从而简化了流水号的生成和管理。

3、单例模式实现方式

在上面例子当中使用到了懒汉式单例模式,下面我们再来看看单例模式的实现方式:

单例模式的实现方式有多种,以下是几种常见的实现方式:

以上是常见的几种单例模式的实现方式。每种实现方式都有其适用的场景和特点,选择哪种方式取决于具体的需求和设计考虑。同时,需要注意线程安全性和性能等因素,确保实现的单例模式符合要求。

下面再说说最常用的懒汉式和饿汉式单例模式实现

3.1 懒汉式单例模式

在第一次使用时才创建实例,称为懒汉式。实现方式是在类内部定义一个私有的静态变量作为实例,然后提供一个公有的静态方法来获取该实例。在方法内部判断实例是否已经存在,如果存在则直接返回,如果不存在则创建一个新的实例并返回。(我就是很懒,你不用我,我就不管你,当你用我的时候才回去给你提供需要的东西)

public class Singleton {
    private static Singleton instance;
    // 私有构造函数
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

在懒汉式单例模式中,我们将单例对象的实例化放在静态方法getInstance()中。当首次调用getInstance()时,会检查实例是否已经存在。如果实例为null,表示还未创建,则进行实例化。这种方式实现了延迟加载,只有在需要使用单例对象时才会创建它。

需要注意的是,在多线程环境下,懒汉式单例模式需要考虑线程安全性。上述代码的实现并未考虑线程安全,可能会导致多个线程同时创建实例。为了解决这个问题,可以在getInstance()方法中添加线程同步机制,如使用synchronized关键字或者双重检查锁定等方式,来确保在多线程环境下仅有一个实例被创建。

下面再来说多线程单例模式

3.2 饿汉式单例模式

饿汉式单例模式: 在类加载时就创建实例,称为饿汉式。实现方式是在类内部定义一个私有的静态变量,并直接创建实例赋值给它,然后提供一个公有的静态方法来获取该实例。(快要饿死了,很需要吃东西,所以说菜一上来就立马开吃,不管你三七二十四)

public class Singleton {
    private static final Singleton instance = new Singleton();
    // 私有构造函数
    private Singleton() {
    }
    public static Singleton getInstance() {
        return instance;
    }
}

在饿汉式单例模式中,我们将单例对象的实例化放在静态常量中,并且将构造函数设为私有,防止外部代码创建实例。在类加载时,静态常量instance就会被创建,并且通过公共的静态方法getInstance()返回该实例。由于实例在类加载时就被创建,因此可以保证单例的唯一性。

3.3 多线程的单例模式

3.3.1 同步方法

同步方法(Synchronized Method):在getInstance()方法上使用synchronized关键字,确保在同一时间只有一个线程可以进入方法,从而避免并发创建多个实例。这种方式简单易行,但可能存在性能问题,因为每次调用getInstance()都需要进行同步。

public class Singleton {
    private static Singleton instance;
    // 私有构造函数
    private Singleton() {
    }
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

3.3.2 双重检查锁定

双重检查锁定(Double-Checked Locking):使用双重检查锁定机制,在getInstance()方法内使用synchronized块,只在实例为null时才进行同步,避免了每次调用都进行同步的开销。这种方式在多线程环境下能够保证线程安全性,同时也具有较好的性能。

public class Singleton {
    private static volatile Singleton instance;
    // 私有构造函数
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

需要注意的是,这种方式需要将instance变量声明为volatile,以确保在多线程环境下的可见性和禁止指令重排序。

3.3.3 静态内部类

静态内部类(Static Inner Class):利用类的静态内部类来实现单例模式。静态内部类在首次使用时加载,且只加载一次,因此保证了线程安全性和延迟加载。

public class Singleton {
    private Singleton() {
        // 私有构造函数
    }
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

在多线程环境下,上述实现方式都能够确保只有一个实例被创建,并提供线程安全的访问。选择哪种方式取决于具体的需求和性能要求。此外,还可以结合其他技术,如使用枚举类型实现单例,因为枚举类型的实例是线程安全且唯一的。

需要注意的是,在某些情况下,可能需要权衡线程安全和性能之间的取舍。在高并发环境下,可以考虑使用其他的并发控制方式,如使用锁、使用线程安全的并发容器等来实现单例模式。

4、单例模式总结

单例模式的核心思想是将类的实例化过程控制在一个特定的范围内,以确保只有一个实例被创建并且全局可访问。这种模式在需要共享资源或避免重复创建相同对象的场景中非常有用。

以下是单例模式的一般实现步骤:

单例模式的优点:

单例模式缺点:

因此,应谨慎使用单例模式,并根据具体需求和设计考虑选择适当的实现方式,以确保单例的正确性和适用性。

单例模式使用场景:

以上就是详解Java设计模式之单例模式的详细内容,更多关于Java 单例模式的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文