懒汉
public class Lazy {
private Lazy() {
}
private static Lazy lazy;
public static Lazy getInstance(){
if (lazy == null) {
lazy = new Lazy();
}
return lazy;
}
}
双重检查锁
public class DoubleCheckSingle {
private DoubleCheckSingle() {
}
private volatile static DoubleCheckSingle instance;
public static DoubleCheckSingle getInstance(){
if (instance == null){
synchronized (DoubleCheckSingle.class){
if (instance == null){
instance = new DoubleCheckSingle();
}
}
}
return instance;
}
}
这里注意,第二个判空是为了多线程情况下的。
当两个线程都进行了第一次判空,线程1获取锁并进行了初始化,释放锁后线程2获得锁
若不进行判空则会生产两个对象。
volatile
是为了防止指令重排序,在初始化对象的三部中:
- 分配内存
- 初始化
- 引用指针指向对象
中的2、3部可能会发生顺序改变,从而导致线程1进行了1,3步骤后,线程2拿到了为初始化的对象直接使用的情况。
静态内部类初始化锁
public class InnerStaticSingle {
private InnerStaticSingle() {
}
private static class innerClass{
private final static InnerStaticSingle instance = new InnerStaticSingle();
}
public static InnerStaticSingle getInstance(){
return innerClass.instance;
}
}
静态内部类中对象的创建时在内部类初始化时进行的,该过程存在一个对象初始化锁,多线程情况下是安全的。
这种情况不会禁止指令重排序,但指令重排序发生在静态内部类的初始化过程中,对外界是不可察觉的。
饿汉
public class Hungary {
private final static Hungary instance = new Hungary();
private Hungary() {
}
public static Hungary getInstance(){
return instance;
}
}
单元素枚举
public enum EnumSingle {
INSTANCE {
protected void say(){
System.out.println(this.hashCode());
}
};
//加了抽象才能调用INSTANCE 实例的say()方法
protected abstract void say();
private Object object;
public Object getObject() {
return object;
}
public void setObject(Object object) {
this.object = object;
}
public static EnumSingle getInstance(){
return INSTANCE;
}
}
反射攻击
反射能够任意操作私有方法,即使是私有构造器也难逃反射的威力
private static void test5() throws Exception {
Constructor constructor = DoubleCheckSingle.class.getDeclaredConstructor();
constructor.setAccessible(true);
System.out.println(constructor.newInstance().hashCode());
System.out.println(DoubleCheckSingle.getInstance().hashCode());
}
上面的测试用例会产生两个对象。
解决方法
对于饿汉模式和静态内部类模式,这两种模式的对象生产发生在类的初始化时,因此可以在其私有构造器中加入代码
if (instance != null){
throw new Exception("不要用反射调戏我");
}
但对于懒汉模式这种延迟加载的,无论是判空还是增加计数/标志位等,均无法抵御反射攻击,因为反射同样可以更改计数器和标志位。因此这种模式的单例无法抵挡反射攻击。
对于枚举类型的单例模式,能够天然抵抗反射攻击
private static void test6() throws Exception{
Constructor constructor = EnumSingle.class.getDeclaredConstructor();
constructor.setAccessible(true);
System.out.println(constructor.newInstance().hashCode());
System.out.println(EnumSingle.getInstance().hashCode());
}
上面的测试用例会抛出异常:
java.lang.NoSuchMethodException
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
意思是找不到默认无参构造器,我们进入java.lang.Enum
的源码中发现,该类型只有一个有参构造器,没有无参构造器
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
对测试用例进行升级改造:
Constructor constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
constructor.newInstance("jwb",123);
结果抛出异常:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
信息很清楚,无法使用反射为枚举类型创建实例,这是因为在Constructor的源码中存在这个判断
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
即当判断是枚举类型时直接抛异常。
序列化破坏
如以下代码所示
private static void test4() throws IOException, ClassNotFoundException {
Hungary instance = Hungary.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hungary"));
oos.writeObject(instance);
File file = new File("hungary");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Hungary newInstance = (Hungary) ois.readObject();
System.out.println(instance.hashCode());
System.out.println(newInstance.hashCode());
System.out.println(instance == newInstance);
}
代码输出结果instance
和newInstance
属于不同对象,这就违反了单例模式一个对象的原则。
原因分析
ois.readObject()
会进入到方法readObject0()
,由于序列化的为对象,因此会进入到
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
在readOrdinaryObject()
中,存在obj = desc.isInstantiable() ? desc.newInstance() : null;
其中isInstantiable()的注释为
/**
* Returns true if represented class is serializable/externalizable and can
* be instantiated by the serialization runtime--i.e., if it is
* externalizable and defines a public no-arg constructor, or if it is
* non-externalizable and its first non-serializable superclass defines an
* accessible no-arg constructor. Otherwise, returns false.
*/
意思是类如果实现了serializable/externalizable
接口,并且可以在序列化过程中动态装载,会返回true
,因此obj
将会进行desc.newInstance()
操作生产一个新对象
继续往下走
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
hasReadResolveMethod()
的注释为
/**
* Returns true if represented class is serializable or externalizable and
* defines a conformant readResolve method. Otherwise, returns false.
*/
判断是否有含有一个readResolve()
方法
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
如果有会使用第一行的反射方法调用readResolve()
方法,得到对象,并与之前desc.newInstance()
产生的对象比对,如果不一样,以rep
为准并返回。因此只需要加一个readResolve()
方法会解决序列化问题,如下:
public class Hungary implements Serializable {
private final static Hungary instance = new Hungary();
private Hungary() {
}
public static Hungary getInstance(){
return instance;
}
private Object readResolve(){
return instance;
}
}
枚举模式的抗序列号破坏
枚举单例模式对序列号的适应性很大,在ObjectInputStream
类中的1570行可以看到
case TC_ENUM:
return checkResolve(readEnum(unshared));
进入readEnum()
可以发现:
Enum<?> en = Enum.valueOf((Class)cl, name);
即直接拿枚举里的唯一对象,而不是像obj = desc.isInstantiable() ? desc.newInstance() : null;
一样创建一个新对象。
从枚举类的反编译代码里也能看到,枚举类的实例对象是通过static
静态初始块初始化的。
枚举型单例模式的对象属性在经过反序列化后也能保证对象没有发生改变。
线程单例模式
这个曾经在有赞的面试中遇到过,实现不同线程拥有不同单例的情况,可以使用ThreadLocal
来实现
public class ThreadSingle {
private static final ThreadLocal<ThreadSingle> local = new ThreadLocal<ThreadSingle>(){
@Override
protected ThreadSingle initialValue() {
return new ThreadSingle();
}
};
private ThreadSingle() {
}
public static ThreadSingle getInstance(){
return local.get();
}
}
测试用例:
private static void test7(){
System.out.println("main thread :"+ThreadSingle.getInstance().hashCode());
System.out.println("main thread :"+ThreadSingle.getInstance().hashCode());
System.out.println("main thread :"+ThreadSingle.getInstance().hashCode());
new Thread( () -> System.out.println("thread 1 :"+ThreadSingle.getInstance().hashCode())).start();
new Thread( () -> {
System.out.println("thread 2 :"+ThreadSingle.getInstance().hashCode());
System.out.println("thread 2 :"+ThreadSingle.getInstance().hashCode());
System.out.println("thread 2 :"+ThreadSingle.getInstance().hashCode());
}).start();
}
结果:
main thread :460141958
main thread :460141958
main thread :460141958
thread 1 :554569462
thread 2 :1637915755
thread 2 :1637915755
thread 2 :1637915755
可以看到这种模式无法保证全局的单例,但是可以让每个线程自身所持有的对象为单例。