单例模式
基础概念
什么是单例模式?
答案: 单例模式(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("应用启动");对比总结
| 实现方式 | 线程安全 | 延迟加载 | 防反射 | 防序列化 | 推荐度 |
|---|---|---|---|---|---|
| 饿汉式 | ✓ | ✗ | ✗ | ✗ | ⭐⭐⭐ |
| 懒汉式(不安全) | ✗ | ✓ | ✗ | ✗ | ✗ |
| 懒汉式(同步) | ✓ | ✓ | ✗ | ✗ | ⭐⭐ |
| 双重检查锁 | ✓ | ✓ | ✗ | ✗ | ⭐⭐⭐⭐ |
| 静态内部类 | ✓ | ✓ | ✗ | ✗ | ⭐⭐⭐⭐⭐ |
| 枚举 | ✓ | ✗ | ✓ | ✓ | ⭐⭐⭐⭐⭐ |
推荐使用:
- 一般场景:静态内部类
- 需要防止反射和序列化:枚举
- 简单场景:饿汉式
练习题
- 为什么双重检查锁需要使用volatile关键字?
- 静态内部类为什么是线程安全的?
- 枚举如何防止反射攻击?
- Spring的单例和设计模式的单例有什么区别?
- 如何在分布式环境下实现单例?