MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java设计模式:单例模式的实现与优化

2021-07-106.6k 阅读

单例模式概述

在软件开发中,单例模式是一种创建型设计模式,它确保一个类仅有一个实例,并提供一个全局访问点。这种模式在许多场景下都非常有用,例如数据库连接池、线程池、日志记录器等,这些组件在整个应用程序中通常只需要一个实例,以避免资源的重复创建和不必要的开销。

单例模式的实现方式

饿汉式单例

饿汉式单例是最简单的单例实现方式之一。在类加载时,就立即创建单例实例。

public class EagerSingleton {
    // 私有静态成员变量,在类加载时就初始化
    private static final EagerSingleton instance = new EagerSingleton();

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

    // 公共静态方法,提供全局访问点
    public static EagerSingleton getInstance() {
        return instance;
    }
}

优点

  1. 实现简单,在类加载时就创建实例,不存在线程安全问题。

缺点

  1. 如果单例实例在整个应用程序生命周期中不一定会被使用,那么类加载时就创建实例会造成资源浪费。

懒汉式单例(线程不安全)

懒汉式单例在需要使用实例时才进行创建,这是一种延迟加载的策略。

public class LazySingleton {
    // 私有静态成员变量,初始化为null
    private static LazySingleton instance;

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

    // 公共静态方法,提供全局访问点
    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

优点

  1. 实现了延迟加载,只有在需要时才创建实例,节省资源。

缺点

  1. 线程不安全。在多线程环境下,可能会有多个线程同时判断 instance == null,从而导致创建多个实例。

懒汉式单例(线程安全 - 同步方法)

为了解决懒汉式单例在多线程环境下的线程安全问题,可以使用 synchronized 关键字修饰 getInstance 方法。

public class ThreadSafeLazySingleton {
    // 私有静态成员变量,初始化为null
    private static ThreadSafeLazySingleton instance;

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

    // 公共静态方法,提供全局访问点,使用synchronized关键字保证线程安全
    public static synchronized ThreadSafeLazySingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeLazySingleton();
        }
        return instance;
    }
}

优点

  1. 保证了线程安全,实现了延迟加载。

缺点

  1. 由于 getInstance 方法被 synchronized 修饰,在多线程环境下,每次调用该方法都需要获取锁,性能较低。

双重检查锁(DCL)单例

双重检查锁(Double-Checked Locking)单例模式通过两次检查 instance == null,并在同步块内再次检查,既保证了线程安全,又提高了性能。

public class DoubleCheckedLockingSingleton {
    // 私有静态成员变量,使用volatile关键字保证可见性和禁止指令重排
    private static volatile DoubleCheckedLockingSingleton instance;

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

    // 公共静态方法,提供全局访问点
    public static DoubleCheckedLockingSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckedLockingSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckedLockingSingleton();
                }
            }
        }
        return instance;
    }
}

优点

  1. 兼顾了线程安全和性能,只有在实例未创建时才会进入同步块,减少了锁竞争。

缺点

  1. 实现相对复杂,需要注意 volatile 关键字的使用,以防止指令重排导致的问题。在Java 5.0 之前,volatile 的语义不够完善,可能会导致 DCL 失效。

静态内部类单例

静态内部类单例模式利用了Java类加载机制的特性来实现延迟加载和线程安全。

public class StaticInnerClassSingleton {
    // 私有构造函数,防止外部实例化
    private StaticInnerClassSingleton() {}

    // 静态内部类,只有在调用getInstance方法时才会加载
    private static class SingletonHolder {
        private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }

    // 公共静态方法,提供全局访问点
    public static StaticInnerClassSingleton getInstance() {
        return SingletonHolder.instance;
    }
}

优点

  1. 实现了延迟加载,同时利用类加载机制保证了线程安全,不需要额外的同步操作。
  2. 代码简洁,易于理解。

缺点

  1. 虽然实现相对简单,但对于不熟悉Java类加载机制的开发者来说,理解起来可能有一定难度。

枚举单例

枚举单例是Java中实现单例模式的一种简洁且安全的方式。

public enum EnumSingleton {
    INSTANCE;

    // 可以添加其他成员变量和方法
    private String data;

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }
}

优点

  1. 简单高效,枚举类型本身就保证了实例的唯一性,并且是线程安全的。
  2. 可以防止反序列化创建新的实例,因为枚举类型在反序列化时会使用已有的枚举常量,而不会创建新的对象。

缺点

  1. 如果需要继承其他类,枚举类型无法实现,因为Java中枚举类不能继承其他类(只能实现接口)。

单例模式的优化

防止反射攻击

在上述实现方式中,除了枚举单例外,其他单例模式都可以通过反射来破坏单例性。例如,通过反射调用私有构造函数创建多个实例。

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class ReflectionAttack {
    public static void main(String[] args) {
        try {
            // 获取懒汉式单例类的Class对象
            Class<LazySingleton> clazz = LazySingleton.class;
            // 获取私有构造函数
            Constructor<LazySingleton> constructor = clazz.getDeclaredConstructor();
            // 允许访问私有构造函数
            constructor.setAccessible(true);
            // 通过反射创建实例
            LazySingleton instance1 = constructor.newInstance();
            LazySingleton instance2 = constructor.newInstance();
            System.out.println(instance1 == instance2); // 输出false,单例性被破坏
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) {
            e.printStackTrace();
        }
    }
}

为了防止反射攻击,可以在构造函数中添加逻辑判断,如果实例已经存在,则抛出异常。

public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {
        if (instance != null) {
            throw new RuntimeException("单例已存在,不能通过反射创建新实例");
        }
    }

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

防止反序列化破坏单例

对于实现了 Serializable 接口的单例类,如果不进行特殊处理,在反序列化时会创建新的实例,破坏单例性。

import java.io.*;

public class SerializableSingleton implements Serializable {
    private static final long serialVersionUID = 1L;
    private static SerializableSingleton instance = new SerializableSingleton();

    private SerializableSingleton() {}

    public static SerializableSingleton getInstance() {
        return instance;
    }

    // 防止反序列化创建新实例
    protected Object readResolve() {
        return instance;
    }
}

通过在单例类中添加 readResolve 方法,在反序列化时会调用该方法返回已有的实例,而不是创建新的实例。

动态代理与单例

在某些场景下,可能需要为单例对象添加额外的功能,例如日志记录、性能监控等。动态代理是一种很好的方式来实现这一点。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 定义一个接口
interface MyService {
    void doSomething();
}

// 单例类实现接口
class MyServiceImpl implements MyService {
    private static final MyServiceImpl instance = new MyServiceImpl();

    private MyServiceImpl() {}

    public static MyServiceImpl getInstance() {
        return instance;
    }

    @Override
    public void doSomething() {
        System.out.println("执行具体业务逻辑");
    }
}

// 动态代理处理类
class MyServiceInvocationHandler implements InvocationHandler {
    private Object target;

    public MyServiceInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("方法调用前,记录日志或进行性能监控");
        Object result = method.invoke(target, args);
        System.out.println("方法调用后,记录日志或进行性能监控");
        return result;
    }
}

public class DynamicProxySingleton {
    public static void main(String[] args) {
        MyService service = MyServiceImpl.getInstance();
        MyService proxyService = (MyService) Proxy.newProxyInstance(
                service.getClass().getClassLoader(),
                service.getClass().getInterfaces(),
                new MyServiceInvocationHandler(service)
        );
        proxyService.doSomething();
    }
}

通过动态代理,在不修改单例类代码的情况下,为单例对象的方法调用添加了额外的功能。

单例模式的应用场景

数据库连接池

在数据库操作中,频繁地创建和销毁数据库连接会消耗大量资源。使用单例模式创建数据库连接池,可以保证整个应用程序中只有一个连接池实例,提高连接的复用率,降低资源消耗。

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;

public class DatabaseConnectionPool {
    private static final DatabaseConnectionPool instance = new DatabaseConnectionPool();
    private List<Connection> connectionList;
    private String url;
    private String username;
    private String password;

    private DatabaseConnectionPool() {
        connectionList = new LinkedList<>();
        url = "jdbc:mysql://localhost:3306/mydb";
        username = "root";
        password = "password";
        for (int i = 0; i < 10; i++) {
            try {
                Connection connection = DriverManager.getConnection(url, username, password);
                connectionList.add(connection);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    public static DatabaseConnectionPool getInstance() {
        return instance;
    }

    public Connection getConnection() {
        if (connectionList.isEmpty()) {
            try {
                return DriverManager.getConnection(url, username, password);
            } catch (SQLException e) {
                e.printStackTrace();
                return null;
            }
        }
        return connectionList.remove(0);
    }

    public void releaseConnection(Connection connection) {
        connectionList.add(connection);
    }
}

线程池

线程池用于管理和复用线程,避免频繁创建和销毁线程带来的开销。单例模式可以确保整个应用程序中只有一个线程池实例。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolSingleton {
    private static final ThreadPoolSingleton instance = new ThreadPoolSingleton();
    private ExecutorService executorService;

    private ThreadPoolSingleton() {
        executorService = Executors.newFixedThreadPool(10);
    }

    public static ThreadPoolSingleton getInstance() {
        return instance;
    }

    public ExecutorService getExecutorService() {
        return executorService;
    }
}

日志记录器

在应用程序中,日志记录是非常重要的功能。使用单例模式创建日志记录器,可以确保整个应用程序使用同一个日志记录实例,方便统一管理日志输出。

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

public class LoggerSingleton {
    private static final LoggerSingleton instance = new LoggerSingleton();
    private FileWriter fileWriter;
    private PrintWriter printWriter;

    private LoggerSingleton() {
        try {
            fileWriter = new FileWriter("app.log", true);
            printWriter = new PrintWriter(fileWriter);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static LoggerSingleton getInstance() {
        return instance;
    }

    public void log(String message) {
        Date date = new Date();
        printWriter.println(date + " - " + message);
        printWriter.flush();
    }
}

单例模式与其他设计模式的结合

单例模式与工厂模式

工厂模式负责创建对象,而单例模式确保创建的对象是唯一的。将两者结合,可以在工厂类中使用单例模式来创建特定类型的对象。

// 产品接口
interface Product {
    void operation();
}

// 具体产品类
class ConcreteProduct implements Product {
    @Override
    public void operation() {
        System.out.println("执行具体产品的操作");
    }
}

// 单例工厂类
class SingletonFactory {
    private static final SingletonFactory instance = new SingletonFactory();

    private SingletonFactory() {}

    public static SingletonFactory getInstance() {
        return instance;
    }

    public Product createProduct() {
        return new ConcreteProduct();
    }
}

单例模式与观察者模式

观察者模式用于对象间的一对多依赖关系,当一个对象状态改变时,它的所有依赖对象都会收到通知并自动更新。单例模式可以用于创建被观察的对象,确保所有观察者观察的是同一个对象。

import java.util.ArrayList;
import java.util.List;

// 观察者接口
interface Observer {
    void update(String message);
}

// 具体观察者类
class ConcreteObserver implements Observer {
    private String name;

    public ConcreteObserver(String name) {
        this.name = name;
    }

    @Override
    public void update(String message) {
        System.out.println(name + " 收到通知: " + message);
    }
}

// 被观察的单例类
class SubjectSingleton {
    private static final SubjectSingleton instance = new SubjectSingleton();
    private List<Observer> observers;
    private String message;

    private SubjectSingleton() {
        observers = new ArrayList<>();
    }

    public static SubjectSingleton getInstance() {
        return instance;
    }

    public void attach(Observer observer) {
        observers.add(observer);
    }

    public void detach(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }

    public void setMessage(String message) {
        this.message = message;
        notifyObservers();
    }
}

总结

单例模式在Java开发中是一种非常实用的设计模式,它提供了一种控制对象实例化的方式,确保一个类在整个应用程序中只有一个实例。通过不同的实现方式,如饿汉式、懒汉式、双重检查锁、静态内部类和枚举等,可以满足不同场景下的需求。同时,通过优化措施,如防止反射攻击、防止反序列化破坏单例以及结合其他设计模式,可以进一步提升单例模式的稳定性和功能性。在实际应用中,根据具体的业务需求和场景,选择合适的单例实现方式是非常重要的。