单例模式是一种非常经典的设计模式,主要是为了保证一个类只有一个实例,控制资源的消耗。比如Spring容器的实例对象和数据库连接池的实例,都是为了可以复用而避免创建更多的对象。 单例模式有两种实现:一是懒汉式;二是饿汉式。
1. 懒汉式,线程不安全
这种方式在类加载时不初始化。在需要的时候才创建对象,节约资源。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
2. 懒汉式,线程安全
在getInstance()方法上使用synchronized关键字加同步锁。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3. 饿汉式
类加载时就创建好实例对象,保证了线程安全。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
4. 双重检查锁
有同学会很纳闷这种方式为什么需要volatile关键字来修饰成员变量,这个就涉及到JUC了,其实就是Java多线程的一些底层原理。volatile关键字保证了可见性和防止指令重排。
在双重检查锁定的单例模式实现中,如果不使用volatile,可能会发生以下情况:
- 线程A进入
getInstance()方法,并执行到synchronized块内部。 - 线程A执行
new Singleton(),这涉及到三个操作:分配内存、初始化对象、将对象引用指向分配的内存地址。 - 由于指令重排序,对象初始化可能在对象引用赋值之前完成,但线程A的写入操作(将对象引用指向分配的内存地址)可能还没有同步到主内存。
- 线程B进入
getInstance()方法,并检查singleton是否为null。由于线程A的写入操作还没有同步到主内存,线程B可能会看到一个null值,然后线程B也会尝试创建一个新的Singleton实例。
使用volatile关键字可以防止这种情况,因为volatile会确保对singleton变量的写入操作会立即同步到主内存,并且其他线程读取singleton变量时会从主内存读取最新值。
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
5. 静态内部类
这种方式在类加载时,因为内部类是不会被加载的,只有通过外部类访问内部类成员时才会被加载,所以很好的实现了延迟加载的特性,同时又保证了线程安全。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}