在以前的 设计模式 - 单例模式(详解)看看和你理解的是否同样? 一文中,咱们提到了经过Idea
开发工具进行多线程调试、单例模式的暴力破坏的问题;因为篇幅缘由,如今单独开一篇文章进行演示:线程不安全的单例在多线程状况下为什么被建立多个、如何破坏单例。java
若是还不知道如何使用IDEA工具进行线程模式的调试,请先阅读我以前发的一篇文章: 你不知道的 IDEA Debug调试小技巧git
首先回顾简单线程不安全的懒汉式单例的代码以及测试程序代码:程序员
/** * @author eamon.zhang * @date 2019-09-30 上午10:55 */
public class LazySimpleSingleton {
private LazySimpleSingleton(){}
private static LazySimpleSingleton instance = null;
public static LazySimpleSingleton getInstance(){
if (instance == null) {
instance = new LazySimpleSingleton();
}
return instance;
}
}
// 测试程序
@Test
public void test() {
try {
ConcurrentExecutor.execute(() -> {
LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " : " + instance);
}, 2, 2);
} catch (Exception e) {
e.printStackTrace();
}
}
复制代码
对于这个单例,咱们毫无疑问认为它是线程不安全的,至于为何,接下来使用IDEA
工具的线程debug
模式来直观的找出答案。github
LazySimpleSingleton
的if (instance == null)
处:getInstance()
处:debug
,咱们能够在调试窗口找到咱们启动的线程:pool-1-thread-1
线程单步执行到if (instance == null)
断点处,观察instance
值为null
;pool-1-thread-1
执行到instance = new LazySimpleSingleton();
处等待初始化:pool-1-thread-2
一样单步执行到 if (instance == null)
断点处,此时观察instance
值也为null
(这就是咱们常说的两个线程同时执行到断代码处):pool-1-thread-2
执行到instance = new LazySimpleSingleton();
处等待初始化:if (instance == null)
的条件,都应该到对应的代码块中执行实例化操做,那么这两个线程就会分别初始化:线程 pool-1-thread-1
实例化后:segmentfault
切换线程 pool-1-thread-2
观察 instance
值已经被初始化了,可是,线程pool-1-thread-2
仍是会被实例化一遍:设计模式
线程pool-1-thread-2
实例化后:安全
你们是否一目了然了呢?bash
你们能够看到,虽然输出打印的对象是同一个,可是,确实是建立了两遍,只不过 pool-1-thread-2
实例化后将 pool-1-thread-1
实例化的对象值给覆盖了。多线程
当我将线程pool-1-thread-1
和线程pool-1-thread-2
同时执行到instance = new LazySimpleSingleton();
处而后先让pool-1-thread-1
执行完打印后,再将pool-1-thread-2
执行实例化操做,就会看到打印的对象会是不同的了:ide
这就是经过线程调试模式手动控制线程执行顺序来模拟还原多线程环境下,线程不安全的状况。
咱们明白了线程不安全的缘由是两个线程同时拿到的instance
资源都为null
,从而都进行实例化。那么有没有什么方法能解决呢?固然有,给 getInstance()
加 上 synchronized
关键字,使这个方法变成线程同步方法:
public class LazySimpleSingleton {
private LazySimpleSingleton(){}
private static LazySimpleSingleton instance = null;
public synchronized static LazySimpleSingleton getInstance(){
if (instance == null) {
instance = new LazySimpleSingleton();
}
return instance;
}
}
复制代码
当咱们将其中一个线程执行并调用 getInstance()
方法时,另外一个线程在调用 getInstance()
方法,线程的状态由 RUNNING
变成了MONITOR
,出现阻塞。直到第一个线程执行完,第二个线程才恢复 RUNNING
状态继续调用 getInstance()
方法
这就解决了以前所说的线程安全问题,可是这样子在线程数量比较多状况下,若是 CPU
分配压力上升,会致使大批量线程出现阻塞,从而致使程序运行性能大幅降低;为了解决线程安全和程序性能问题,因而乎有了咱们的双重检查式的单例。这里就再也不多说了。
通常状况下,咱们建立使用饿汉式单例或双重检查的懒汉式单例是没有问题的,可是在必定状况下,会发生单例被破坏。
实际状况下,公司一个程序员写了一个单例,可是另一个程序员,可能比较牛 X,写代码风格有点不同,他经过反射来调用别人写的接口,这就会出现此单例并不是彼单例的状况。这就破坏了单例。
在咱们写单例的时候,你们有没有注意到私有的构造方法前面的修饰符仅为 private
,若是咱们使用反射来调用其构造方法,而后,再调用 getInstance()
方法,应该就会有两个不一样的实例。
咱们之前面说单例的文章中的 LazyInnerClassSingleton
为例,编写反射调用测试代码:
@Test
public void testReflex() {
try {
// 很无聊的状况下,进行破坏
Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class;
// 经过反射拿到私有的构造方法
Constructor<LazyInnerClassSingleton> c = clazz.getDeclaredConstructor(null);
// 设置访问属性,强制访问
c.setAccessible(true);
// 暴力初始化两次,这就至关于调用了两次构造方法
LazyInnerClassSingleton o1 = c.newInstance();
LazyInnerClassSingleton o2 = c.newInstance();
// 只要 o1和o2 地址不相等,就能够说明这是两个不一样的对象,也就是违背了单例模式的初衷
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
复制代码
运行结果以下:
显然,是建立了两个不一样的实例。如今,咱们在其构造方法中作一些限制,一旦出现屡次重复建立,则直接抛出异常。来看优化后的代码:
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {
if(LazyHolder.INSTANCE != null){
throw new RuntimeException("不容许建立多个实例");
}
}
// 注意关键字final,保证方法不被重写和重载
public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
// 注意 final 关键字(保证不被修改)
private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
}
}
复制代码
再次调用:
至此,就避免了单例被反射破坏的问题。
另一种状况,可能会遇到,咱们须要将对象序列化到磁盘,下次使用时再从磁盘反序列化回来,反序列化的对象会被从新分配内存,那若是序列化的对象为单例,则就违背了单例模式的初衷。这也至关于破坏了单例。
咱们仍是以LazyInnerClassSingleton
为例,将LazyInnerClassSingleton
实现 Serializable
接口;
而后编写测试代码:
/** * @author eamon.zhang * @date 2019-10-08 下午3:06 */
public class SerializableTest {
public static void main(String[] args) {
LazyInnerClassSingleton s1 = null;
LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("LazyInnerClassSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("LazyInnerClassSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (LazyInnerClassSingleton)ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
执行测试代码:
能够看到,结果为两个不一样的对象。这一样违背了单例模式的初衷。那么咱们如何保证序列化的状况也能实现单例呢?其实也很简单,使用 readResolve()
方法便可:
public class LazyInnerClassSingleton implements Serializable {
private LazyInnerClassSingleton() {
if (LazyHolder.INSTANCE != null) {
throw new RuntimeException("不容许建立多个实例");
}
}
// 注意关键字final,保证方法不被重写和重载
public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
// 注意 final 关键字(保证不被修改)
private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
}
// 解决反序列化对象不一致问题
private Object readResolve() {
return LazyHolder.INSTANCE;
}
}
复制代码
你们确定会问,why?
为了一探究竟,咱们来看一下 JDK 源码,咱们进入 ObjectInputStream
类的 readObject()
方法:
public final Object readObject() throws IOException, ClassNotFoundException {
if (this.enableOverride) {
return this.readObjectOverride();
} else {
int outerHandle = this.passHandle;
Object var4;
try {
Object obj = this.readObject0(false);
this.handles.markDependency(outerHandle, this.passHandle);
ClassNotFoundException ex = this.handles.lookupException(this.passHandle);
if (ex != null) {
throw ex;
}
if (this.depth == 0L) {
this.vlist.doCallbacks();
this.freeze();
}
var4 = obj;
} finally {
this.passHandle = outerHandle;
if (this.closed && this.depth == 0L) {
this.clear();
}
}
return var4;
}
}
复制代码
咱们发现:readObject 中又调用了咱们重写的 readObject0()
方法,进入 readObject0()
方法:
private Object readObject0(boolean unshared) throws IOException {
...
try {
switch(tc) {
...
case 115:
var4 = this.checkResolve(this.readOrdinaryObject(unshared));
return var4;
...
} finally {
--this.depth;
this.bin.setBlockDataMode(oldMode);
}
return var4;
}
复制代码
咱们看到代码中调用了 ObjectInputStream
的 readOrdinaryObject()
方法,咱们继续进入看源码:
private Object readOrdinaryObject(boolean unshared) throws IOException {
...
if (cl != String.class && cl != Class.class && cl != ObjectStreamClass.class) {
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception var7) {
throw (IOException)(new InvalidClassException(desc.forClass().getName(), "unable to create instance")).initCause(var7);
}
...
}
}
复制代码
发现调用了 ObjectStreamClass
的 isInstantiable()
方法,而 isInstantiable()
里面的代码以下:
boolean isInstantiable() {
this.requireInitialized();
return this.cons != null;
}
复制代码
代码很是简单,就是判断一下构造方法是否为空,构造方法不为空就返回 true
,也就是说,只要有无参构造方法就会实例化;这时候,其实尚未找到为何加上readResolve()
方法就避免了单例被破坏的真正缘由,咱们再次回到ObjectInputStream
的 readOrdinaryObject()
方法继续往下看能够找到以下代码:
private Object readOrdinaryObject(boolean unshared) throws IOException {
...
if (obj != null && this.handles.lookupException(this.passHandle) == null && desc.hasReadResolveMethod()) {
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
if (rep != null) {
if (rep.getClass().isArray()) {
this.filterCheck(rep.getClass(), Array.getLength(rep));
} else {
this.filterCheck(rep.getClass(), -1);
}
}
obj = rep;
this.handles.setObject(this.passHandle, rep);
}
}
...
}
复制代码
判断无参构造方法是否存在以后,又调用了 hasReadResolveMethod()
方法:
boolean hasReadResolveMethod() {
this.requireInitialized();
return this.readResolveMethod != null;
}
复制代码
逻辑很是简单,就是判断readResolveMethod
是否为空,不为空就返回 true
。那么 readResolveMethod
是在哪里赋值的呢? 经过全局查找找到了赋值代码在私有方法 ObjectStreamClass()
方法中给 readResolveMethod
进行赋值,来看代码:
ObjectStreamClass.this.readResolveMethod = ObjectStreamClass.getInheritableMethod(cl, "readResolve", (Class[])null, Object.class);
复制代码
代码的逻辑其实就是经过反射找到一个无参的 readResolve()
方法,而且保存下来,如今再回到 ObjectInputStream
的 readOrdinaryObject()
方法继续往下看,若是readResolve()
存在则调用 invokeReadResolve()
方法:
Object invokeReadResolve(Object obj) throws IOException, UnsupportedOperationException {
this.requireInitialized();
if (this.readResolveMethod != null) {
try {
return this.readResolveMethod.invoke(obj, (Object[])null);
} catch (InvocationTargetException var4) {
Throwable th = var4.getTargetException();
if (th instanceof ObjectStreamException) {
throw (ObjectStreamException)th;
} else {
throwMiscException(th);
throw new InternalError(th);
}
} catch (IllegalAccessException var5) {
throw new InternalError(var5);
}
} else {
throw new UnsupportedOperationException();
}
}
复制代码
咱们能够看到在 invokeReadResolve()
方法中用反射调用了 readResolveMethod()
方法。 经过JDK
源码分析咱们能够看出,虽然,增长 readResolve()
方法返回实例,解决了单例被破坏的问题。可是,咱们经过分析源码以及调试,咱们能够看到实际上实例化了两 次,只不过新建立的对象没有被返回而已.
那若是,建立对象的动做发生频率增大,就 意味着内存分配开销也就随之增大;为了解决这个问题,咱们推荐使用注册式单例。
咱们在前文中说到了,咱们极力推荐使用枚举类型的单例;接下来咱们分析一下缘由:
使用 Java
反编译工具 Jad
(自行下载),解压后,使用命令行调用:
./jad ~/IdeaProjects/own/java-advanced/01.DesignPatterns/design-patterns/build/classes/java/main/com/eamon/javadesignpatterns/singleton/enums/EnumSingleton.class
复制代码
会在当前目录生成一个 EnumSingleton.jad
文件,咱们使用 vscode
打开这个文件查看:
public final class EnumSingleton extends Enum {
public static EnumSingleton[] values()
{
return (EnumSingleton[])$VALUES.clone();
}
public static EnumSingleton valueOf(String name) {
return (EnumSingleton)Enum.valueOf(com/eamon/javadesignpatterns/singleton/enums/EnumSingleton, name);
}
private EnumSingleton(String s, int i) {
super(s, i);
instance = new EnumResource();
}
public Object getInstance() {
return instance;
}
public static final EnumSingleton INSTANCE;
private Object instance;
private static final EnumSingleton $VALUES[];
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}
复制代码
请注意这段代码:
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
复制代码
原来枚举类单例在静态代码块中就给INSTANCE
赋了值,是饿汉式单例的实现方式。那么一样的,咱们可否经过反射和序列化方式进行破坏呢?
先分析经过序列化方式:
咱们仍是回到JDK
源码:在 ObjectInputStream
的 readObject0()
方法中有以下代码:
private Object readObject0(boolean unshared) throws IOException {
...
case 126:
var4 = this.checkResolve(this.readEnum(unshared));
...
return var4;
}
复制代码
咱们看到 readObject0()
中调用了readEnum()
方法,跟进该方法:
private Enum<?> readEnum(boolean unshared) throws IOException {
if (this.bin.readByte() != 126) {
throw new InternalError();
} else {
ObjectStreamClass desc = this.readClassDesc(false);
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " + desc);
} else {
int enumHandle = this.handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
this.handles.markException(enumHandle, resolveEx);
}
String name = this.readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
Enum<?> en = Enum.valueOf(cl, name);
result = en;
} catch (IllegalArgumentException var9) {
throw (IOException)(new InvalidObjectException("enum constant " + name + " does not exist in " + cl)).initCause(var9);
}
if (!unshared) {
this.handles.setObject(enumHandle, result);
}
}
this.handles.finish(enumHandle);
this.passHandle = enumHandle;
return result;
}
}
}
复制代码
咱们发现枚举类型其实经过类名和 Class 对象类找到一个惟一的枚举对象。所以,枚举对象不可能被类加载器加载屡次。
那么是否能够经过反射进行破坏呢?咱们先来执行如下反射破坏枚举类的测试代码:
@Test
public void testEnum(){
try {
// 很无聊的状况下,进行破坏
Class<EnumSingleton> clazz = EnumSingleton.class;
// 经过反射拿到私有的构造方法
Constructor<EnumSingleton> c = clazz.getDeclaredConstructor(null);
// 设置访问属性,强制访问
c.setAccessible(true);
// 暴力初始化两次,这就至关于调用了两次构造方法
EnumSingleton o1 = c.newInstance();
EnumSingleton o2 = c.newInstance();
// 只要 o1和o2 地址不相等,就能够说明这是两个不一样的对象,也就是违背了单例模式的初衷
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
复制代码
执行结果:
报的是 java.lang.NoSuchMethodException
异常,意思是没找到无参的构造方法。
那么咱们来看一下 java.lang.Enum
的源码,咱们发现它只有一个protected
的构造方法:
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
复制代码
那咱们来作一个这样的测试:
@Test
public void testEnum1() {
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor(String.class, int.class);
c.setAccessible(true);
EnumSingleton enumSingleton = (EnumSingleton) c.newInstance("Eamon", 666);
} catch (Exception e) {
e.printStackTrace();
}
}
复制代码
发现控制台输出以下错误:
意思就是不能用反射来建立枚举类型。至于为何,咱们仍是来看 JDK
源码,进入Constructor
的newInstance()
方法中:
public T newInstance(Object... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
if (!this.override) {
Class<?> caller = Reflection.getCallerClass();
this.checkAccess(caller, this.clazz, this.clazz, this.modifiers);
}
if ((this.clazz.getModifiers() & 16384) != 0) {
throw new IllegalArgumentException("Cannot reflectively create enum objects");
} else {
ConstructorAccessor ca = this.constructorAccessor;
if (ca == null) {
ca = this.acquireConstructorAccessor();
}
T inst = ca.newInstance(initargs);
return inst;
}
}
复制代码
原来,在源码中对枚举类型进行了强制性的判断(16384
表明枚举类型),若是是枚举类型,直接抛异常。到此为止也就说明了为何《Effective Java》推荐使用枚举来实现单例的缘由: JDK
枚举的语法特殊性,以及反射也为枚举保驾护航,让枚举式单例成为一种比较优雅的实现。
本文中所涉及的源码可在 github 上找到,相关的测试代码在 test 包下:github.com/eamonzzz/ja…