balking 模式习题
1. 希望 doInit() 方法仅被调用一次,下面的实现是否有问题,为什么?
答:有问题。该类的对象,在多线程环境下会存在问题。比如t1和t2线程并发执行,相继进入init方法,且都读取到initialized值为false之后,执行doInit方法。这样就不满足题目要求。
public class TestVolatile {
volatile boolean initialized = false;
void init() {
if (initialized) {
return;
}
doInit();
initialized = true;
}
private void doInit() {
}
}
线程安全单例习题
单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象(即调用getInstance)时的线程安全,并思考注释中的问题。
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
实现1:
// 问题1:为什么加 final
// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例
public final class Singleton implements Serializable {
// 问题3:为什么设置为私有? 是否能防止反射创建新的实例?
private Singleton() {}
// 问题4:这样初始化是否能保证单例对象创建时的线程安全?
private static final Singleton INSTANCE = new Singleton();
// 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
1. 防止他的子类重写父类方法,破坏单例。
2.
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
看重点: 以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
也就是说如果你的单例实现了Serializable,反序列化出来的对象,是重新创建的对象了。解决方法,单例类中增加readResolve()方法,可以避免实例重复。
可以通过查看readObject()源码,看看是如何反序列化创建对象的,这个方法创建完对象之后会通过反射机制判断类中是否有readResolve()方法,如果有readResolve()方法,会通过反射机制调用这个方法。所以当你在单例类中写上readResolve()方法,是能够保证得到的同一个单例的,能够保证单例的全局唯一性。
补充: 但是避免不了序列化重复创建对象,实际上我们这种写法只是将反序列化创建的对象覆盖掉了,在执行过程中JVM还是创建了新的对象。
3. 防止其他类创建对象,保证单例。不能,反射可以得到constructor对象,设置构造器对象的setAccessible为true,暴力反射,调用构造方法,创建新的实例。
4.没有,静态成员变量的初始化是在类加载阶段完成的,类加载阶段由jvm保证代码的线程安全性。
5.用方法可以提供更好的封装性,可以实现懒惰的初始化,还可以提供泛型的支持。对创建单例变量有更好的控制。
实现2
// 问题1:枚举单例是如何限制实例个数的
// 问题2:枚举单例在创建时是否有并发问题
// 问题3:枚举单例能否被反射破坏单例
// 问题4:枚举单例能否被反序列化破坏单例
// 问题5:枚举单例属于懒汉式还是饿汉式
// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
enum Singleton {
INSTANCE;
}
2.1 枚举单例是如何限制实例个数的
由枚举控制,是单实例的。
2.2 枚举单例在创建时是否有并发问题没有。因为它也是静态成员变量。
2.3 枚举能否被反射破坏单例不能用反射破坏。拿不到构造方法,不能实现反射。
2.4 枚举单例能否被反序列化破坏单例枚举在实现的时候就继承了序列化的接口,不能被破坏。
2.5枚举单例属于懒汉式还是饿汉式饿汉式的,在类加载的时候就创建单例了。
2.6 枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做添加构造方法,实现初始化。
实现3
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
// 分析这里的线程安全, 并说明有什么缺点
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
将锁加在了Singleton.class,可以保证线程安全。同步锁的范围太大了,在上述地方会出现效率低下,第一次的访问的时候,通过同步锁创建对象,但是对象创建了之后,可以不通过同步锁访问了。
实现4:DCL
public final class Singleton {
private Singleton() { }
// 问题1:解释为什么要加 volatile ?
private static volatile Singleton INSTANCE = null;
// 问题2:对比实现3, 说出这样做的意义
public static Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (Singleton.class) {
// 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
if (INSTANCE != null) { // t2
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}
4.1 为什么要加volatile?
synchronized代码块里的指令可能会重排序,如INSTANCE = new Singleton();这行代码,详情看这篇博文。在多线程环境下,读线程里调用getInstance(),判断INSTANCE不为空,可能会拿到未初始化完的对象。
-----------------------------------------------------------------------------------------------------------------------
4.2 对比实现3,说出这样做的意义第一次调用的话,会继续创建对象,但是第二、第三次调用,可以直接判断,不用再通过同步代码了,提升了效率。
-----------------------------------------------------------------------------------------------------------------------
4.3 为什么还要在同步代码块中加是否为空的判断为了防止多次创建对象。第一个进入的线程已经创建成功单例,退出代码块。第二个线程从entrySet下来,获取锁,如果不进行判断,那么就还会创建一个对象。因此说如果有两个线程相继进到代码块时,可以防止多次创建对象。
实现5:
public final class Singleton {
private Singleton() { }
// 问题1:属于懒汉式还是饿汉式
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 问题2:在创建时是否有并发问题
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
1. 懒汉式,类加载也是懒汉式的,只有用到才会加载。利用静态内部类特点实现延迟加载。效率高。
2. 静态变量的赋值操作,是由jvm保证线程安全性。