设计模式——单例模式
单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
实际应用
在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。
- 一个打印机可以链接两个电脑,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。
- 每台计算机可以有若干通信端口,避免一个通信端口同时被两个请求同时调用。
- 一台计算机可以用多个进程或线程,不能让多个线程或者进程对同一个应用文件进行修改和访问
单例模式的特点
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
单例模式的优点
1. 节约系统资源:由于单例模式只允许创建一个实例,可以避免多次创建对象导致资源的浪费。
2. 方便资源共享:可以使用单例模式来管理全局变量或共享资源,方便数据共享和通信。
3. 全局访问点:通过单例模式可以提供一个全局的访问点,方便对对象的访问和操作。
单例模式的缺点
1. 破坏了单一职责原则:单例类的职责过重,既要负责自身的功能,又要负责管理自己的生命周期。
2. 难以扩展:由于单例类的实例被固定,难以扩展和修改。
3. 可能引起并发安全问题:多线程环境下,如果不加锁或者其他保护机制,可能会引起并发安全问题。
单例模式的几种实现方式
1、饿汉式(静态常量)
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
package com.example.singletontest;
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
2、饿汉式(静态代码块)
这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。
package com.example.singletontest;
/**
* 饿汉式单例模式(静态代码块)
*/
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
3.懒汉式 (线程不安全)
这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
/**
* 懒汉式单例模式,线程不安全
*/
public class Singleton {
private static Singleton instance;
private Singleton (){
System.out.println("构造方法");
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
4.懒汉式 (线程安全)
这种方式解决上面第三种实现方式的线程不安全问题,具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。
/**
* 懒汉式单例模式,线程安全
*/
public class Singleton {
private static Singleton instance;
private Singleton (){
System.out.println("构造方法");
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
5、双检锁/双重校验锁(DCL,即 double-checked locking)
进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。
这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。
优点:
1. 延迟加载:只有在需要时才会创建实例,避免了提前创建实例导致的资源浪费。
2. 节约性能:只有在第一次创建实例时才进行同步操作,避免了多次重复同步导致的性能损耗。
3. 线程安全:通过双重检查,在多线程环境下可以保证单例对象的创建过程线程安全。
缺点:
1. 可能会出现指令重排序问题:在一些特定的平台或编译器优化情况下,可能会出现指令重排序,导致双重检查失效。
2. 实现复杂:双重检查锁需要进行双重检查,实现起来比较复杂,容易出错。
3. 不适用于旧版Java:在旧版Java中,由于内存模型的问题,双重检查锁存在线程安全性隐患,需要使用volatile关键字进行修饰。
/**
* 懒汉式单例模式,双重检查
*/
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){
System.out.println("构造方法");
}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
6、登记式/静态内部类
通过一个静态内部类来持有单例实例。在这种方式中,单例实例是在静态内部类被加载时进行初始化,实现了延迟加载和线程安全。
优点:
1. 线程安全:静态内部类只会被加载一次,保证了单例实例的线程安全性。
2. 延迟加载:单例实例的初始化是在静态内部类被加载时进行的,实现了延迟加载,节约了资源。
3. 简洁优雅:相比其他单例实现方式,静态内部类实现方式简洁明了,易于理解和维护。
缺点:
1. 无法传递参数:静态内部类的加载是在getInstance()方法中进行的,无法向构造函数传递参数,不适用于需要传入参数的场景。
2. 依赖JVM类加载机制:静态内部类的实现依赖JVM类加载机制,需要保证类加载时的线程安全性。
3. 无法防止反射攻击:静态内部类的实现方式无法防止反射攻击,可以通过反射调用私有构造函数来创建多个实例。
/**
* 懒汉式单例模式,内部类
*/
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){
System.out.println("构造方法");
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
7、枚举
枚举单例模式是一种使用枚举类型来实现单例模式的方式。在枚举中定义一个枚举常量INSTANCE,该枚举常量就是单例实例。枚举类型在Java中保证在任何情况下都只会被实例化一次,因此枚举单例模式是天然的线程安全的。
枚举单例模式的实现非常简单,只需要定义一个枚举类,其中只包含一个实例,且该实例在类加载时初始化。枚举单例模式避免了多线程同步的问题,同时也能防止反射和序列化的问题。
要使用枚举单例模式,只需要通过Singleton.INSTANCE来访问单例实例,无需实现复杂的单例模式逻辑。此外,枚举类型还具有序列化机制,可防止反序列化重新创建新的实例。
优点:
1. 线程安全:枚举类的实例由JVM保证只会被实例化一次,在任何情况下都是单例的,因此线程安全。
2. 简洁明了:枚举单例模式实现简单,直观,易于理解和维护。
3. 可防止反射攻击和序列化问题:枚举类型的实例在JVM中只会有一个,防止了反射攻击,而且枚举类默认实现了Serializable接口,可以防止序列化问题。
缺点:
1. 无法懒加载:枚举单例模式在类加载时就会初始化实例,无法实现延迟加载,会造成一定的资源浪费。
2. 无法传递参数:枚举类型不支持传递参数给构造函数,因此无法灵活地创建实例。
3. 限制较多:枚举类型不能被继承、不能被打破等,有一定的限制。
/**
* 枚举单例模式
*/
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}