本部分介绍单例模式,从懒汉式单例讲起,介绍懒汉式模式遇到的各种问题:多线程、指令重排序,及其在解决问题的基础上的各种演进。之后介绍饿汉式单例,饿汉式单例相对简单,但是失去了延迟加载的优势。还会介绍序列化、反射等对单例模式的破坏与预防;并引申出相对完美的枚举单例。还扩展介绍了容器单例,以及ThreadLocal单例。
一. 概述
1. 定义:保证一个类仅有一个实例,并提供一个全局访问点
2. 类型:创建型
3. 适用场景:确保任何情况下都绝对只有一个实例,如:应用配置、线程池、数据库连接池。
4. 优缺点:
4.1 优点:在内存里只有一个实例,减少内存开销;避免对资源的多重占用
4.2 缺点:没有接口,扩展困难
5. 重点:私有构造器、线程安全、延迟加载、指令重排序、序列化和反序列化安全、反射攻击
6. 实用技能:反编译、内存原理、多线程Debug
7. 相关设计模式:工厂模式(一般把工厂类设计为单例模式的)、享元模式(通过享元模式和单例模式的结合,完成单例对象的获取,这种情况下享元模式类似于单例模式的工厂)
二、懒汉式单例:多线程Debug、指令重排序
1. 懒汉式:懒汉式可以理解为这种方式比较懒,有拖延症,其实为延迟加载,实例的创建要到必须的时候才进行。与之相对应的是饿汉式,这种方式比较积极,在类加载的时候就创建实例。这里先介绍懒汉式。
2. 线程不安全的懒汉式单例及其逐步优化
我们对单例模式逐步演化,从最基本的开始:
1 public class LazySingleton { 2 private static LazySingleton lazySingleton = null; // 空的实例 3 private LazySingleton() {}; // 私有构造器 4 public static LazySingleton getInstance() { // 公有方法获取实例 5 if (lazySingleton == null) { 6 lazySingleton = new LazySingleton(); 7 } 8 return lazySingleton; 9 } 10 }
这种懒汉式单例模式,在多线程的情况下是不安全的。考虑这么一种情况,有两个线程0和1,当线程0运行到第6行创建实例但还没赋值时切换到线程1,线程1运行到第5行判断lazySingleton为空,继续运行,这样就创建了两个实例。虽然最后赋值给lazySingleton的是一个单例,但在多个线程的情况下却会创建多个单例,如果单例占用内存较多,则很有可能造成系统故障。下面用多线程Debug的方式模拟两个线程的情况。
多线程安全问题创建线程类和测试类,代码如下:
1 // 线程类 2 public class T implements Runnable{ 3 4 @Override 5 public void run() { 6 LazySingleton lazySingleton = LazySingleton.getInstance(); 7 System.out.println(Thread.currentThread().getName() + " " + lazySingleton); 8 } 9 10 }
// 测试类 public class Test { public static void main(String[] args) { Thread t0 = new Thread(new T()); Thread t1 = new Thread(new T()); t0.start(); t1.start(); System.out.println("The end..."); } }
上述代码直接运行的话,输出的结果是一样的,但是当我们Debug就会发现创建了多个实例对象。
多线程debug:
1)首先在线程类的run()第6行打个断点(注意:断点的位置一定要正确,run方法或者run以后调用的方法里,否则的话,程序跑完了,debug模式里也只有一个主线程在跑),然后点击debug,程序运行会停在断点位置,观察debug一栏,会看到多个线程。如下图
上图展现了断点的位置和多个线程。
2)切换某个线程时,适用鼠标点击就可以。在此首先执行线程1,进入getInstance方法,执行到单例类的第6行,此时还未给lazySingleton赋值,该变量仍然为null。如下图所示:
3)这时,鼠标点击线程0,切换到0线程,同样执行到单例类第6行,由于lazySingleton为null,所以可以通过if。如下图所示:
4)切换回线程1,执行到单例类第8行,这时可以看到lazySingleton已经赋值,值为:26,如下图所示:
5)再切换回线程0,执行同样的执行到单例类第8行,这时可以看到,lazySingleton的值已经改变为:31,如下图所示。
6)执行程序到最后输出结果如下图所示:
上述过程使用了多线程debug的技能,输出结果一样,这是因为后一个线程重新赋值了,并且是在重新赋值后进行的return,但是创建了两个单例对象。若在第5步不切换回线程0,而是直接让线程1运行结束,再切换回线程0,让其运行结束,那么输出的将是两个不同的结果。
对于上述线程不安全的懒汉式单例模式,采用加sychronized关键字的方式改进:
1 public synchronized static LazySingleton getInstance2() { // synchronized锁静态类 2 if (lazySingleton == null) { 3 lazySingleton = new LazySingleton(); 4 } 5 return lazySingleton; 6 }
synchroized锁静态类使方法变成同步方法,注意synchroized加在静态方法上锁的是类的class文件,synchroized加在非静态方法上锁的是堆中的对象。上述代码还有另一种写法:
1 public static LazySingleton getInstance3() { 2 synchronized (LazySingleton.class) { // 锁类 3 if (lazySingleton == null) { 4 lazySingleton = new LazySingleton(); 5 } 6 } 7 return lazySingleton; 8 }
上述两种代码效果一样,都是锁class。因为锁的范围过大, 所以会影响性能。下面有更优化的方式:兼顾性能、线程安全,同时是懒加载的。
双重检查式懒汉:
1 public class LazyDoubleCheckSingleton { 2 private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; // 空的实例 3 private LazyDoubleCheckSingleton() {}; // 私有构造器 4 5 public static LazyDoubleCheckSingleton getInstance() { // 公有方法获取实例 6 if (lazyDoubleCheckSingleton == null) { // 检查1 7 synchronized (LazyDoubleCheckSingleton.class) { // 锁类 8 if (lazyDoubleCheckSingleton == null) { // 检查2 9 lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); 10 } 11 } 12 } 13 return lazyDoubleCheckSingleton; 14 } 15 }
上述代码看似安全高效,在第一次创建单例对象后,不需要再次进入sychronized。但想象不到的安全隐患却潜藏于第6行和第9行,这涉及到另一个知识点:指令重排序。在第9行看似一个步骤,其实涉及到对象的创建过程,主要有三个步骤:1. 分配内存,2. 初始化对象,3. 指针指向内存地址。这三个步骤中2 3的顺序是可以改变的,即顺序可以为123或132。当初始化顺序为132时,若执行到3,切换另一个线程,该线程执行到第6行,lazyDoubleCheckSingleton不为空,然后第13行返回,因为此时为执行初始化步骤2,则返回的实例对象未初始化,所以会影响接下来的进程。
为了消除指令重排序造成的影响,可以采取禁止指令重排序或指令重排序对其他线程不可见的方式。
1)禁止指令重排序可以使用volatile关键字,详情点击链接。上述代码也就改成了
1 public class LazyDoubleCheckSingleton { 2 private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; // 加了 volatile 3 private LazyDoubleCheckSingleton() {}; // 私有构造器 4 5 public static LazyDoubleCheckSingleton getInstance() { // 公有方法获取实例 6 if (lazyDoubleCheckSingleton == null) { // 检查1 7 synchronized (LazyDoubleCheckSingleton.class) { // 锁类 8 if (lazyDoubleCheckSingleton == null) { // 检查2 9 lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); 10 } 11 } 12 } 13 return lazyDoubleCheckSingleton; 14 } 15 }
2)防止其他线程看到指令重排序的方式可以采用静态内部类的方式,代码如下:
1 public class StaticInnerClassSingleton { 2 private StaticInnerClassSingleton() {}; // 私有构造方法 3 4 private static class InnerClass{ 5 private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton(); 6 } 7 8 public static StaticInnerClassSingleton getInstance() { 9 return InnerClass.staticInnerClassSingleton; 10 } 11 }
JVM在类的初始化阶段,执行类的初始化时,JVM会获取一个锁,这个锁会同步多个线程对一个类的初始化。上述特性可以实现基于静态内部类的、线程安全的、延迟初始化方案。这样当一个线程执行类的初始化时,其他线程会被锁在外面。
触发类初始化的情况有以下5种:1. 有一个A类型的实例被创建;2. A类中声明的静态方法被调用;3. A类中的静态成员被赋值;4. A类中的静态成员被使用,且该成员不是常量成员;5. A类是顶级类,且该类中有嵌套的断言语句。
假设线程0获取到StaticInnerClassSingleton 对象的初始化锁,这时线程0执行该静态内部类的初始化。这时即使初始化步骤2. 初始化对象,3. 指针指向内存地址,之间存在重排序,但是线程1也是无法看到的。所以这里的关键就在于InnerClass的初始化锁被哪个线程拿到,哪个线程就执行初始化。
总结:对于初始的懒汉式单例,由于存在多线程不安全的情况,所以需要加sycnrhnized关键字;但该关键字会降低效率,所以出现了双重检查机制;对于双重检查机制,存在指令重排序的问题,为防止指令重排序使用了volatile关键字、或使指令重排序对其他线程不可见使用了静态内部类。
在上述叙述中,问题用红色字体标出,解决方案用绿色字体标出。
二、饿汉式单例:
1. 饿汉式:在类加载的时候,就完成实例化。
1 public class HungrySingleton { 2 private final static HungrySingleton HUNGRY_SINGLETON = new HungrySingleton(); // 类加载时就初始化 3 private HungrySingleton() {} // 私有构造方法 4 5 public static HungrySingleton getInstance() { 6 return HUNGRY_SINGLETON; 7 } 8 }
上述为饿汉式的基本模式,优点为:写法简单、类加载时就完成初始化避免了线程同步问题。缺点是没有延迟加载的效果,单例类一般比较大,如果这个类从始至终没有被用过,会造成内存的浪费。总体来说,饿汉式是最简单的,如果资源浪费少的话,这种模式非常方便。上述代码还有另外一种实现方式:
1 public class HungrySingleton { 2 private final static HungrySingleton HUNGRY_SINGLETON; 3 static { 4 HUNGRY_SINGLETON = new HungrySingleton(); // 类加载时就初始化 5 } 6 7 private HungrySingleton() {} // 私有构造方法 8 9 public static HungrySingleton getInstance() { 10 return HUNGRY_SINGLETON; 11 } 12 }
三、序列化破坏单例解决方案与 原理分析
可以思考这样一个问题:当把单例对象序列化到一个文件中,然后再把它反序列化出来,这样生成的对象和原来的对象还是同一个吗?
下面使用饿汉式测试,测试前饿汉单例类先实现Serializable接口,然后编写测试类如下:
1 public class Test { 2 public static void main(String[] args) throws Exception { 3 // 序列化写 4 HungrySingleton instance = HungrySingleton.getInstance(); 5 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file")); 6 oos.writeObject(instance); 7 8 // 序列化读 9 File file = new File("singleton_file"); 10 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); 11 HungrySingleton newInstance = (HungrySingleton)ois.readObject(); 12 13 // 比较 14 System.out.println(instance); 15 System.out.println(newInstance); 16 System.out.println(instance == newInstance); 17 } 18 }
运行测试类输出发现,instance和newInstance并不一样,序列化破坏了单例模式生成了不同的对象。为了解决上述问题并理解其原理,需要探究ObjectInputStream.readObject()的源码。在readObject()方法中调用了readObject0(),在readObject0()方法中的switch语句中调用了readOrdinaryObject方法()。下面是该方法的源码,关键处已注释。
1 private Object readOrdinaryObject(boolean unshared) 2 throws IOException 3 { 4 if (bin.readByte() != TC_OBJECT) { 5 throw new InternalError(); 6 } 7 8 ObjectStreamClass desc = readClassDesc(false); 9 desc.checkDeserialize(); 10 11 Class<?> cl = desc.forClass(); 12 if (cl == String.class || cl == Class.class 13 || cl == ObjectStreamClass.class) { 14 throw new InvalidClassException("invalid class descriptor"); 15 } 16 17 Object obj; 18 try { 19 obj = desc.isInstantiable() ? desc.newInstance() : null; // 反射创建对象 20 } catch (Exception ex) { 21 throw (IOException) new InvalidClassException( 22 desc.forClass().getName(), 23 "unable to create instance").initCause(ex); 24 } 25 26 passHandle = handles.assign(unshared ? unsharedMarker : obj); 27 ClassNotFoundException resolveEx = desc.getResolveException(); 28 if (resolveEx != null) { 29 handles.markException(passHandle, resolveEx); 30 } 31 32 if (desc.isExternalizable()) { 33 readExternalData((Externalizable) obj, desc); 34 } else { 35 readSerialData(obj, desc); 36 } 37 38 handles.finish(passHandle); 39 40 if (obj != null && 41 handles.lookupException(passHandle) == null && 42 desc.hasReadResolveMethod()) // 判断是否有readResolve方法 43 { 44 Object rep = desc.invokeReadResolve(obj); // 反射调用readResolve方法 45 if (unshared && rep.getClass().isArray()) { 46 rep = cloneArray(rep); 47 } 48 if (rep != obj) { 49 // Filter the replacement object 50 if (rep != null) { 51 if (rep.getClass().isArray()) { 52 filterCheck(rep.getClass(), Array.getLength(rep)); 53 } else { 54 filterCheck(rep.getClass(), -1); 55 } 56 } 57 handles.setObject(passHandle, obj = rep); 58 } 59 } 60 61 return obj; 62 }
在第19行,通过反射创建单例对象,此时反射创建的单例对象与getInstance()获得的对象不同,所以测试类中输出false。为使其相同,我们继续往下看。最后返回的是obj,在第57行有将rep赋值给obj的操作。为满足其条件,首先看到第42行,点进去看源码判断是否有readResolve()方法,在第44行反射调用readResolve()方法将其结果赋值给rep。因此我们在此处这样改写单例类:
1 public class HungrySingleton implements Serializable{ 2 private final static HungrySingleton HUNGRY_SINGLETON; 3 static { 4 HUNGRY_SINGLETON = new HungrySingleton(); // 类加载时就初始化 5 } 6 7 private HungrySingleton() {} // 私有构造方法 8 9 public static HungrySingleton getInstance() { 10 return HUNGRY_SINGLETON; 11 } 12 13 // readResolve方法 14 private Object readResolve() { 15 return HUNGRY_SINGLETON; 16 } 17 }
再次运行测试类,两个对象比较,返回ture。为更详细的了解,可以debug看其具体执行过程。同时注意,在上述序列化和反序列化的过程中,已经实例化对象了,只是没有返回。
四、反射攻击解决方案及 原理分析
同样用简单的饿汉模式进行演示,反射攻击的测试类如下:
1 public class Test { 2 3 public static void main(String[] args) throws Exception { 4 Class objCla = HungrySingleton.class; 5 Constructor constructor = objCla.getDeclaredConstructor(); 6 constructor.setAccessible(true); // 把权限置为ture,放开权限 7 8 HungrySingleton instance = HungrySingleton.getInstance(); 9 HungrySingleton newInstance = (HungrySingleton)constructor.newInstance(); 10 11 System.out.println(instance); 12 System.out.println(newInstance); 13 System.out.println(instance == newInstance); 14 } 15 }
上述代码输出结果为false,对于饿汉式单例,由于它在类加载时就已经生成了对象,因此我们可以通过改动构造方法来防止在类加载后再次创建对象。具体代码如下:
public class HungrySingleton implements Serializable{ private final static HungrySingleton HUNGRY_SINGLETON; static { HUNGRY_SINGLETON = new HungrySingleton(); // 类加载时就初始化 } private HungrySingleton() { // 私有构造方法 if (HUNGRY_SINGLETON != null) { // 抛出异常禁止反射调用 throw new RuntimeException("单例构造器禁止反射调用"); } } public static HungrySingleton getInstance() { return HUNGRY_SINGLETON; } }
运行后发现,反射调用构造方法时会抛出异常。但是上述方式仅适用于静态内部类和饿汉式的方式。
对于不是在类加载时就创建单例的情况,不可以使用上述方式。
五、枚举单例、原理源码及反编译
对于枚举单例,将主要关注它在序列化和反射攻击中的表现。枚举单例代码如下:
1 public enum EnumInstance { 2 INSTANCE; 3 private Object data; // 测试的主要为枚举类持有的对象data 4 5 public Object getData() { 6 return data; 7 } 8 9 public void setData(Object data) { 10 this.data = data; 11 } 12 13 public static EnumInstance getInstance() { 14 return INSTANCE; 15 } 16 }
1)序列化测试类的代码如下:
1 public class Test { 2 public static void main(String[] args) throws Exception { 3 EnumInstance instance = EnumInstance.getInstance(); 4 instance.setData(new Object()); 5 6 // 枚举单例类测试序列化 7 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file")); 8 oos.writeObject(instance); 9 File file = new File("singleton_file"); 10 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); 11 EnumInstance newInstance = (EnumInstance)ois.readObject(); 12 13 System.out.println(instance.getData()); 14 System.out.println(newInstance.getData()); 15 System.out.println(instance.getData() == newInstance.getData()); 16 } 17 }
运行测试类后,两个instance输出结果一样。接下来通过源码了解枚举不受序列化影响的原因:打开ObjectInputStream.readObject()的源码。在readObject()方法中调用了readObject0(),在readObject0()方法中的switch语句中调用了readEnum方法()。下面是该方法的源码,关键处已注释
private Enum<?> readEnum(boolean unshared) throws IOException { if (bin.readByte() != TC_ENUM) { throw new InternalError(); } ObjectStreamClass desc = readClassDesc(false); if (!desc.isEnum()) { throw new InvalidClassException("non-enum class: " + desc); } int enumHandle = handles.assign(unshared ? unsharedMarker : null); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null) { handles.markException(enumHandle, resolveEx); } String name = readString(false); // 获取枚举对象的名称 Enum<?> result = null; Class<?> cl = desc.forClass(); if (cl != null) { try { @SuppressWarnings("unchecked") Enum<?> en = Enum.valueOf((Class)cl, name); // 获取枚举常量,因为枚举中name是唯一的,并且对应一个枚举常量,因此这里获得的是唯一的常量对象,没有创建新的对象 result = en; } catch (IllegalArgumentException ex) { throw (IOException) new InvalidObjectException( "enum constant " + name + " does not exist in " + cl).initCause(ex); } if (!unshared) { handles.setObject(enumHandle, result); } } handles.finish(enumHandle); passHandle = enumHandle; return result; }
2)反射的测试类代码如下:
1 public class Test { 2 public static void main(String[] args) throws Exception { 3 Class objCla = EnumInstance.class; 4 Constructor constructor = objCla.getDeclaredConstructor(String.class, int.class); // 枚举类的构造方法带有两个参数 5 constructor.setAccessible(true); // 把权限置为ture,放开权限 6 7 EnumInstance instance = EnumInstance.getInstance(); 8 EnumInstance newInstance = (EnumInstance)constructor.newInstance("haha", 666); 9 10 System.out.println(instance); 11 System.out.println(newInstance); 12 System.out.println(instance == newInstance); 13 } 14 }
上述代码为枚举反射的测试代码,运行上述代码可以发现运行不到10行,因为在第8行会报错:“java.lang.IllegalArgumentException: Cannot reflectively create enum objects”。具体的可以在运行时打开Constructor类查看报错处的源码。
3)枚举类本身的优势
了解枚举类本身,要把它反编译。这里反编译枚举类使用的时JAD,可以到这里下载。下载完成后,解压,配置环境变量即可使用,具体可百度。这里对EnumInstance类进行反编译,命令为:
jad EnumInstance.class
反编译结果的后缀名为.jad,此处使用Notepad++打开反编译后的结果如下,关键处已注释:
1 public final class EnumInstance extends Enum // final --> 类不能被继承 2 { 3 4 private EnumInstance(String s, int i) // 私有构造器 5 { 6 super(s, i); 7 } 8 9 public Object getData() 10 { 11 return data; 12 } 13 14 public void setData(Object data) 15 { 16 this.data = data; 17 } 18 19 public static EnumInstance getInstance() 20 { 21 return INSTANCE; 22 } 23 24 public static EnumInstance[] values() 25 { 26 EnumInstance aenuminstance[]; 27 int i; 28 EnumInstance aenuminstance1[]; 29 System.arraycopy(aenuminstance = ENUM$VALUES, 0, aenuminstance1 = new EnumInstance[i = aenuminstance.length], 0, i); 30 return aenuminstance1; 31 } 32 33 public static EnumInstance valueOf(String s) 34 { 35 return (EnumInstance)Enum.valueOf(pattern/creational/singletion/EnumInstance, s); 36 } 37 38 public static final EnumInstance INSTANCE; // 静态的类变量 39 private Object data; 40 private static final EnumInstance ENUM$VALUES[]; 41 42 static // 静态块加载 43 { 44 INSTANCE = new EnumInstance("INSTANCE", 0); 45 ENUM$VALUES = (new EnumInstance[] { 46 INSTANCE 47 }); 48 } 49 }
从反编译结果能看出,枚举类的构造器是私有的,并且类变量是static final,且在静态块中加载。并且有序列化和反射方面的优势,所以枚举类在创建单例对象上具备原生优势。
六、基于容器的单例模式
基于容器的单例模式,类似于享元模式。代码如下:
1 public class ContainerSingleton { 2 private ContainerSingleton() {} 3 4 private static Map<String, Object> singletonMap = new HashMap<String, Object>(); // 用map存储单例 5 // hashmap不是线程安全的 6 public static void putInstance(String key, Object instance) { 7 if (key != null && key.length() != 0 && instance != null) { 8 if (!singletonMap.containsKey(key)) { 9 singletonMap.put(key, instance); 10 } 11 } 12 } 13 14 public static Object getInstance(String key) { 15 return singletonMap.get(key); 16 } 17 }
此处使用map存储单例,HashMap不是线程安全的,但是在类加载时直接加载HashMap这样用也可以,但要考虑具体情况。考虑下数情况,有两个线程,线程0先put进kv,然后get数据;线程2再put进kv,然后get数据。这事两个线程使用同样的key不同的value那么获得的结果是不一样的;若线程0先put,然后线程1put,然后再get,那么获得的结果是一样的。
若将HashMap改成HashTable会变成线程安全的,但是会影响性能;若是改成ConcurrentHashMap,在此场景中,使用了静态的ConcurrentHashMap并且直接操作了map,ConcurrentHashMap并不是绝对的线程安全。综上,不考虑反射、序列化等情况,容器单例模式也是有一定适用场景。
容器可以统一管理单例对象,节省资源,但线程并不安全。
七、ThreadLocal线程单例(可保证线程唯一,不能保证全局唯一)
使用ThreadLocal类创建在线程内唯一的单例,代码如下:
1 public class ThreadLocalInstance { 2 private static final ThreadLocal<ThreadLocalInstance> THREAD_LOCAL 3 = new ThreadLocal<ThreadLocalInstance>() { // 匿名内部类 4 protected ThreadLocalInstance initialValue() { // 重写方法 5 return new ThreadLocalInstance(); 6 } 7 }; 8 9 private ThreadLocalInstance() {} 10 11 public static ThreadLocalInstance getInstance() { 12 return THREAD_LOCAL.get(); 13 } 14 }
修改T类如下:
1 public class T implements Runnable{ 2 3 @Override 4 public void run() { 5 ThreadLocalInstance instance = ThreadLocalInstance.getInstance(); 6 System.out.println(Thread.currentThread().getName() + " " + instance); 7 } 8 9 }
测试:
1 public class Test { 2 public static void main(String[] args) { 3 ThreadLocalInstance instance = ThreadLocalInstance.getInstance(); 4 System.out.println(instance); 5 System.out.println(instance); 6 System.out.println(instance); 7 System.out.println(instance); 8 System.out.println(instance); 9 10 Thread t0 = new Thread(new T()); 11 Thread t1 = new Thread(new T()); 12 t0.start(); 13 t1.start(); 14 15 System.out.println("The end..."); 16 } 17 }
运行上述代码可以发现main线程中的输出结果是一样的,main,t0,t1的输出结果各不相同。ThreadLocal隔离了多个线程对资源的访问冲突,对于多线程资源共享的问题,使用同步锁是时间换空间的,使用ThreadLocal是空间换时间。
八、源码中的应用
1)java.lang.Runtime类,属于饿汉式单例。
2)java.awt.Desktop类的getDesktop属于容器单例,但是加了各种sychronized进行同步控制。