Skip to content

单例模式

基础概念

什么是单例模式?

答案: 单例模式(Singleton Pattern)确保一个类只有一个实例,并提供一个全局访问点。

使用场景

  • 配置管理器
  • 数据库连接池
  • 线程池
  • 日志对象
  • 缓存

优点

  • 节省内存资源
  • 避免资源的多重占用
  • 全局访问点

缺点

  • 违反单一职责原则
  • 难以扩展
  • 隐藏依赖关系

实现方式

1. 饿汉式(静态常量)

答案:

java
public class Singleton {
    // 类加载时就创建实例
    private static final Singleton INSTANCE = new Singleton();

    // 私有构造器
    private Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

优点

  • 线程安全
  • 实现简单

缺点

  • 类加载时就创建,可能造成内存浪费

2. 饿汉式(静态代码块)

答案:

java
public class Singleton {
    private static Singleton instance;

    static {
        instance = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}

特点:与静态常量方式类似,只是将实例化放在静态代码块中。

3. 懒汉式(线程不安全)

答案:

java
public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    // 线程不安全
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

问题:多线程环境下可能创建多个实例。

示例

java
// 线程1和线程2同时执行
Thread1: if (instance == null)  // true
Thread2: if (instance == null)  // true
Thread1: instance = new Singleton()
Thread2: instance = new Singleton()  // 创建了两个实例

4. 懒汉式(同步方法)

答案:

java
public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    // 同步方法,线程安全
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

优点:线程安全

缺点:效率低,每次获取实例都要同步

5. 双重检查锁(DCL)

答案:

java
public class Singleton {
    // 必须使用volatile
    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;
    }
}

为什么需要两次检查?

java
// 第一次检查:避免不必要的同步
if (instance == null) {  // 线程A和B都通过
    synchronized (Singleton.class) {
        // 第二次检查:避免重复创建
        if (instance == null) {  // 线程A创建后,线程B再次检查
            instance = new Singleton();
        }
    }
}

为什么需要volatile?

java
instance = new Singleton();
// 实际分为3步:
// 1. 分配内存空间
// 2. 初始化对象
// 3. 将instance指向内存地址

// 可能发生指令重排序:1 -> 3 -> 2
// 导致其他线程获取到未初始化的对象

优点

  • 线程安全
  • 延迟加载
  • 效率高

推荐使用

6. 静态内部类

答案:

java
public class Singleton {

    private Singleton() {
    }

    // 静态内部类
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

原理

  • 外部类加载时,内部类不会加载
  • 调用getInstance()时,才加载内部类
  • 类加载机制保证线程安全

优点

  • 线程安全
  • 延迟加载
  • 实现简单

推荐使用

7. 枚举

答案:

java
public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("单例方法");
    }
}

// 使用
Singleton.INSTANCE.doSomething();

优点

  • 线程安全
  • 防止反射攻击
  • 防止反序列化创建新对象
  • 实现简单

《Effective Java》推荐方式

破坏单例

如何通过反射破坏单例?

答案:

java
public class Test {
    public static void main(String[] args) throws Exception {
        // 正常获取
        Singleton s1 = Singleton.getInstance();

        // 反射获取
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton s2 = constructor.newInstance();

        System.out.println(s1 == s2);  // false,创建了两个实例
    }
}

防御方式

java
public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
        // 防止反射攻击
        if (instance != null) {
            throw new RuntimeException("单例模式禁止反射创建");
        }
    }

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

如何通过序列化破坏单例?

答案:

java
public class Test {
    public static void main(String[] args) throws Exception {
        Singleton s1 = Singleton.getInstance();

        // 序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.obj"));
        oos.writeObject(s1);
        oos.close();

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.obj"));
        Singleton s2 = (Singleton) ois.readObject();
        ois.close();

        System.out.println(s1 == s2);  // false,创建了两个实例
    }
}

防御方式

java
public class Singleton implements Serializable {
    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;
    }

    // 防止反序列化创建新对象
    private Object readResolve() {
        return instance;
    }
}

Spring中的单例

Spring Bean的单例?

答案:

Spring的单例是容器级别的单例,不是JVM级别的单例。

java
@Component
public class UserService {
    // Spring默认单例
}

// 配置
@Bean
@Scope("singleton")  // 默认值
public UserService userService() {
    return new UserService();
}

Spring单例的特点

  • 一个Spring容器中只有一个实例
  • 不同容器可以有多个实例
  • 线程不安全(需要注意成员变量)

线程安全问题

java
@Service
public class UserService {
    // 线程不安全
    private int count = 0;

    public void increment() {
        count++;  // 多线程环境下会出问题
    }
}

解决方案

java
@Service
public class UserService {
    // 方案1:使用ThreadLocal
    private ThreadLocal<Integer> count = ThreadLocal.withInitial(() -> 0);

    // 方案2:使用原子类
    private AtomicInteger count = new AtomicInteger(0);

    // 方案3:不使用成员变量,使用局部变量
    public void process() {
        int count = 0;  // 局部变量,线程安全
    }
}

实际应用

数据库连接池

答案:

java
public class ConnectionPool {
    private static volatile ConnectionPool instance;
    private List<Connection> pool;
    private static final int POOL_SIZE = 10;

    private ConnectionPool() {
        pool = new ArrayList<>();
        for (int i = 0; i < POOL_SIZE; i++) {
            pool.add(createConnection());
        }
    }

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

    public synchronized Connection getConnection() {
        if (pool.isEmpty()) {
            return createConnection();
        }
        return pool.remove(0);
    }

    public synchronized void releaseConnection(Connection conn) {
        pool.add(conn);
    }

    private Connection createConnection() {
        // 创建数据库连接
        return null;
    }
}

配置管理器

答案:

java
public class ConfigManager {
    private static volatile ConfigManager instance;
    private Properties properties;

    private ConfigManager() {
        properties = new Properties();
        try {
            properties.load(new FileInputStream("config.properties"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

    public String getProperty(String key) {
        return properties.getProperty(key);
    }
}

// 使用
String dbUrl = ConfigManager.getInstance().getProperty("db.url");

日志工具

答案:

java
public class Logger {
    private static volatile Logger instance;
    private PrintWriter writer;

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

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

    public synchronized void log(String message) {
        writer.println(new Date() + " - " + message);
        writer.flush();
    }
}

// 使用
Logger.getInstance().log("应用启动");

对比总结

实现方式线程安全延迟加载防反射防序列化推荐度
饿汉式⭐⭐⭐
懒汉式(不安全)
懒汉式(同步)⭐⭐
双重检查锁⭐⭐⭐⭐
静态内部类⭐⭐⭐⭐⭐
枚举⭐⭐⭐⭐⭐

推荐使用

  • 一般场景:静态内部类
  • 需要防止反射和序列化:枚举
  • 简单场景:饿汉式

练习题

  1. 为什么双重检查锁需要使用volatile关键字?
  2. 静态内部类为什么是线程安全的?
  3. 枚举如何防止反射攻击?
  4. Spring的单例和设计模式的单例有什么区别?
  5. 如何在分布式环境下实现单例?

Released under the MIT License.