线程安全问题
双重校验锁
public class Singleton {
private static Singleton instance = null;
// 私有构造函数
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
因为对象的创建过程并非是原子性的。在创建的过程中,由于指令重排的影响,导致多线程并发状态下,容易产生线程不安全的问题。
对象创建过程
正常过程
当虚拟机执行 instance = new Singleton这句代码时,会被分解成以下三个动作来执行:
异常过程
但是,这三个动作的执行顺序并非是一成不变的,有可能经过JVM和CPU的优化编译之后,这三个动作的执行顺序发生了改变,变成了这样:
异常举例
状态序列 | instance值 | 线程P1 | 线程P2 |
---|---|---|---|
1 | null | 竞争到锁,开始初始化 | |
2 | null | 执行1,给对象分配内存空间 | |
3 | 未初始化 | 执行3,指向内存地址 | 进入getInstance方法,发现instance不为null,返回未初始化的instance |
此时线程P2拿到的instance未进行初始化操作,存在安全隐患。
将instance对象声明为volatile,即可禁止指令重排序操作,解决上述问题。
破坏单例特性
反射破坏单例模式
public class Main {
public static void main(String[] args) throws Exception {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1);
System.out.println(instance2);
/*通过反射的方式直接调用私有构造器(通过在构造器里抛出异常可以解决此漏洞)*/
Class<Singleton> clazz = (Class<Singleton>) Class.forName("Singleton");
Constructor<Singleton> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true); // 跳过权限检查
Singleton instance3 = c.newInstance();
Singleton instance4 = c.newInstance();
System.out.println("通过反射的方式获取的对象instance3:" + instance3);
System.out.println("通过反射的方式获取的对象instance4:" + instance4);
}
}
// output
Singleton@299a06ac
Singleton@299a06ac
通过反射的方式获取的对象instance3:Singleton@383534aa
通过反射的方式获取的对象instance4:Singleton@6bc168e5
如上,即通过getInstance方法,能够正常获取唯一的单例对象。但通过反射能够任意地创建新的单例类对象。
public class Singleton {
private volatile static Singleton instance = null;
// 私有构造函数
private Singleton() throws Exception {
if (null != Singleton.instance) {
throw new Exception("请使用getInstance方法获取单例对象!");
}
}
public static Singleton getInstance() throws Exception {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
// output
Singleton@299a06ac
Singleton@299a06ac
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at Main.main(Main.java:188)
Caused by: java.lang.Exception: 请使用getInstance方法获取单例对象!
at Singleton.<init>(Singleton.java:11)
... 5 more
通过改造构造函数,能够解决已有单例对象情况下,通过反射创建单例对象的情况。
public class Main {
public static void main(String[] args) throws Exception {
/*通过反射的方式直接调用私有构造器(通过在构造器里抛出异常可以解决此漏洞)*/
Class<Singleton> clazz = (Class<Singleton>) Class.forName("Singleton");
Constructor<Singleton> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true); // 跳过权限检查
Singleton instance1 = c.newInstance();
Singleton instance2 = c.newInstance();
System.out.println("通过反射的方式获取的对象instance1:" + instance1);
System.out.println("通过反射的方式获取的对象instance2:" + instance2);
}
}
//output
通过反射的方式获取的对象instance1:Singleton@299a06ac
通过反射的方式获取的对象instance2:Singleton@383534aa
若是不调用getInstance方法,只通过反射创建单例对象,仍会引发同样的问题。
在单例类中加入flag标志位,可以解决,但反射能够改变成员变量的值,所以单例特性仍然能够被反射机制破坏。
解决方案
-
使用饿汉模式的单例
饿汉模式,在类加载时即初始化单例对象,再通过反射对单例类进行实例化,会引发异常。 -
使用枚举方式的单例
枚举单例类,java.lang.IllegalArgumentException: Cannot reflectively create enum objects
反序列化破坏单例模式
public class Main {
public static void main(String[] args) throws Exception {
Singleton instance1 = Singleton.getInstance();
try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("object"));
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("object"))) {
//将对象持久化到磁盘中
outputStream.writeObject(instance1);
outputStream.flush();
//从磁盘中反序列化成对象
Singleton instance2 = (Singleton) inputStream.readObject();
System.out.println("通过getInstance方法获取的对象instance1:" + instance1);
System.out.println("通过反序列化的方式获取的对象instance2:" + instance2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// output
通过getInstance方法获取的对象instance1:Singleton@27f674d
通过反序列化的方式获取的对象instance2:Singleton@48140564
显然,通过反序列方式得到的单例类对象,与原始的单例类对象已经不一样了。
阅读源码ObjectInputStream类发现原因
/**
* Reads and returns "ordinary" (i.e., not a String, Class,
* ObjectStreamClass, array, or enum constant) object, or null if object's
* class is unresolvable (in which case a ClassNotFoundException will be
* associated with object's handle). Sets passHandle to object's assigned
* handle.
*/
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
// 如果实现了构造函数通过反射生成新对象
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
handles.finish(passHandle);
// 如果实现了ReadResolve方法,则通过该方法生成对象
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
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方法,就替换为该方法返回的对象,如果未实现,则反序列化得到的是一个全新的对象。
那么实现了ReadSolve方法,即可解决上述问题。
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private volatile static Singleton instance = null;
// 私有构造函数
private Singleton() throws Exception {
if (null != Singleton.instance) {
throw new Exception("请使用getInstance方法获取单例对象!");
}
}
public static Singleton getInstance() throws Exception {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
//添加的readResolve方法
private Object readResolve() {
return instance;
}
}
// output
通过getInstance方法获取的对象instance1:Singleton@27f674d
通过反序列化的方式获取的对象instance2:Singleton@27f674d
一开始我认为,在单例类对象已经生成的情况下,通过反射实例化对象会抛出异常,但经过debug,发现最终通过如下代码,即使用native的方法分配了内存空间,并没有通过构造函数生成对象,也就跳过了构造函数的检查过程。
class BootstrapConstructorAccessorImpl extends ConstructorAccessorImpl {
private final Constructor<?> constructor;
BootstrapConstructorAccessorImpl(Constructor<?> var1) {
this.constructor = var1;
}
public Object newInstance(Object[] var1) throws IllegalArgumentException, InvocationTargetException {
try {
return UnsafeFieldAccessorImpl.unsafe.allocateInstance(this.constructor.getDeclaringClass());
} catch (InstantiationException var3) {
throw new InvocationTargetException(var3);
}
}
}
解决方案
- 实现ReadSolve方法
- 使用枚举方式的单例
枚举单例类,反序列化时,是通过枚举类的ValueOf方法进行实例化,不会出现上述问题。
总结
枚举是最好的单例实现。