题目:设计一个类,只能生成该类的一个实例。
- 单例模式分为懒汉式(需要才去创建对象)和饿汉式(创建类的实例时就去创建对象)
饿汉式
属性实例化对象
public class HugerSingletonTest {
//该对象的引用不可修改
private static final HugerSingletonTest ourInstance = new HugerSingletonTest();
public static HugerSingletonTest getInstance() {
return ourInstance;
}
private HugerSingletonTest() {}
}
在静态代码块实例对象
public class Singleton {
private static Singleton ourInstance;
static {
ourInstance = new Singleton();
}
public static Singleton getInstance() {
return ourInstance;
}
private Singleton() {}
}
分析:饿汉式单例模式只要调用了该类,就会实例化一个对象,但有时我们只需要调用该类中的一个方法,而不需要实例化一个对象,所以饿汉式是比较消耗资源的。
懒汉式
非线程安全
public class Singleton {
private static Singleton ourInstance;
private static Singleton getInstance() {
if(null == ourInstance) {
ourInstance = new Singleton();
}
return outInstance;
}
private Singleton() {}
}
分析:如果有两个线程同时调用 getInstance() 方法,则会创建两个实例化对象,所以是非线程安全的。
线程安全:给方法加锁
public class Singleton {
private static Singleton ourInstance;
public synchronized static Singleton getInstance() {
if(null == ourInstance) {
ourInstance = new Singleton();
}
return ourInstance;
}
private Singleton() {}
}
分析:如果有多个线程调用 getInstance() 方法,当一个线程获取该方法,其他线程就必须等待,消耗资源。
线程安全:双重检查锁(同步代码块)
public class Singleton {
private static Singleton ourInstance;
public static Singleton getInstance() {
if(null == ourInstance) {
synchronized (Singleton.class) {
if(null == ourInstance) {
ourInstance = new Singleton();
}
}
}
return ourInstance;
}
private Singleton() {}
}
分析:为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了。然后空对象的线程进入同步代码块,如果不加第二次空对象检查,两个线程都获取了同步代码块,一个线程先进入了同步代码块,另一个则在等待,此时先进入的线程创建了实例对象,而后线程也进入同步代码块时,也会创建一个实例对象,此时就会创建两个实例对象,所以需要在线程进入同步代码块后再次j进行空对象检查,才能确保只创建一个实例对象。
线程安全:静态内部类
public class Singleton {
private static class SingletonHodler {
private static final Singleton ourInstance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHodler.ourInstance;
}
private Singleton() {}
}
分析:
- 静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。具体来说当 Singleton 第一次被加载时,并不需要去加载 Singleton,只有当 getInstance() 方法第一次被调用时,使用 INSTANCE 的时候,才会导致虚拟机加载 SingletonHodler 类。这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
- 为何线程安全:虚拟机会保证一个类的类构造器 < clinit >() 在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器 < clinit >(),其他线程都需要阻塞等待,直到活动线程执行 < clinit >() 方法完毕。特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行 < clinit >() 方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行 < clinit >() 方法,因为在同一个类加载器下,一个类型只会被初始化一次。如果在一个类的 < clinit >() 方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。
线程安全:枚举
enum SingletonTest {
INSTANCE;
public void doSomething() {}
}
分析:默认枚举实例的创建是线程安全的,而且还能防止反序列化重新创建新对象,但是在枚举中的其它任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。
线程安全:使用ThreadLocal
public class Singleton {
private static final ThreadLocal<Singleton> tlSingleton =
new ThreadLocal<Singleton>() {
@Override
protected Singleton initialValue() {
return new Singleton();
}
};
public static Singleton getInstance() {
return tlSinglrton.get();
}
private Singleton() {}
}
分析:ThreadLocal 会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而 ThreadLocal 采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
指令重排序
其实创建一个对象,往往包含三个过程。对于singleton = new Singleton(),这不是一个原子操作,在 JVM 中包含的三个过程。
- 给 singleton 分配内存
- 调用 Singleton 的构造函数来初始化成员变量,形成实例
- 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)
但是,由于JVM会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是 1-3-2,则在 3 执行完毕、2 未执行之前,被l另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了。
针对这种情况,我们有什么解决方法呢?那就是把singleton声明成 volatile
public class Singleton {
//volatile的作用是:保证可见性、禁止指令重排序,但不能保证原子性
private volatile static Singleton ourInstance;
public static Singleton getInstance() {
if (null == ourInstance) {
synchronized (Singleton.class) {
if (null == ourInstance) {
ourInstance = new Singleton();
}
}
}
return ourInstance;
}
private Singleton() {
}
}