引言
单例模式是一种创建型设计模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。在实际开发中,很多应用场景需要控制某个类在整个系统中的实例数量,保证其唯一性并提供全局访问点。这样的需求常常出现在配置管理、日志记录、数据库连接池、文件系统操作等模块中。通过单例模式,我们可以有效避免内存资源的浪费,提高系统性能,并确保类的实例始终一致。在本文中,我们将深入探讨单例模式的本质、应用场景、常见的实现方式、单例模式的优缺点、选用场景等,实现方式除了我们常见的饿汉式、懒汉式,本文还提到了另外三种方式,通过缓存和延迟加载的思想实现单例,通过枚举来实现单例,还有Lazy initialization holder class
。 如有疏漏与不足,恳请不吝赐教,非常感谢!
单例模式的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式的本质:控制实例数目。单例模式是为了控制在运行期间,某些类的实例数目只能有一个。
一、场景问题
我们在开发过程中,项目中都有与应用相关的配置文件,这些配置文件很多是由项目开发人员自定义的,在配置文件中定义一些应用需要的参数。在实际的项目中,这种配置文件多采用xml
格式,也有采用properties
格式的,使用Java来读取properties
格式的配置文件比较简单。现在读取配置文件的内容,该如何实现呢
1.1、不用模式
读取配置文件so easy
这点小事情难不倒我,读取文件的内容,然后把文件内容放在相应的数据对象里面就可以了。我们以读取properties
为例,代码示例如下:
/**
* 读取配置文件
*/
public class AppConfig {
private String paramA;
private String paramB;
// 注意这里只有访问参数的get方法,没有设置参数的set方法
public String getParamA() {
return paramA;
}
public String getParamB() {
return paramB;
}
/**
* 构造方法
*/
public AppConfig() {
// 读取配置文件
readConfig();
}
private void readConfig(){
Properties p = new Properties();
InputStream in = null;
try {
// appConfig.properties文件放在了resources目录下了
in = AppConfig.class.getResourceAsStream("/appConfig.properties");
p.load(in);
this.paramA = p.getProperty("paramA");
this.paramB = p.getProperty("paramB");
}catch (Exception e){
System.out.println("读取文件报错e:" + e.getMessage());
e.printStackTrace();
}finally {
try {
in.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
@Override
public String toString() {
return "AppConfig{" +
"paramA='" + paramA + '\'' +
", paramB='" + paramB + '\'' +
'}';
}
}
/**
* 单例模式
*/
public class NoPatternMain {
public static void main(String[] args) {
AppConfig appConfig1 = new AppConfig();
AppConfig appConfig2 = new AppConfig();
System.out.println(appConfig1.toString());
System.out.println(appConfig2.toString());
}
}
/*打印结果:
AppConfig{hashCode='21685669'paramA='aaa', paramB='bbb'}
AppConfig{hashCode='2133927002'paramA='aaa', paramB='bbb'}
注意一下hashCode是不一样的,这里是创建了两个对象。
*/
1.2、有何问题
上面的实现很简单,很容易的就能实现要求的功能,真的这么简单吗? 仔细想想,有没有什么问题呢???
客户端使用这个类的时候,是通过new
一个AppConfig
的实例来得到配置文件内容的。如果在系统运行中,有很多地方都需要使用配置文件的内容,也就是说很多地方都需要创建AppConfig
对象的实例。换句话说,在系统运行期间,系统中会存在很多个AppConfig
的实例对象,这有什么问题吗???
当然有问题了,试想一下,每一个AppConfig
实例对象里面都封装着配置文件的内容,系统中多个AppConfig
实例对象,也就是说系统中会同时存在多份配置文件的内容,这样会严重浪费内存资源。如果配置文件内容较少,问题还小一点,如果配置文件内容本来就多的话,对于系统资源的浪费问题就大了。事实上,对于AppConfig
这种类,在运行期间,只需要一个实例对象就是够了。
把上面的描述进一步抽象一下,问题就出来了:在一个系统运行期间,某个类只需要一个类实例就可以了,那么应该怎样实现呢???
二、解决方案(重点)
用来解决上述问题的一个合理的解决方案就是单例模式(Singleton
)。仔细分析上面的问题,现在一个类能够被创建多个实例,问题的根源在于类的构造方法是公开的,也就是可以让类的外部来通过构造方法创建多个实例。换句话说,只要类的构造方法能让类的外部访问,就没有办法去控制外部来创建这个类的实例个数。
要想控制一个类只被创建一个实例,那么首要的问题就是要收回创建实例的权限,让类自身来负责自己类实例的创建工作,然后由这个类来提供外部可以访问这个类实例的方法,这就是单例模式的实现方式。
2.1、结构说明
Singleton
:负责创建Singleton
类自己的唯一实例,并提供一个getInstance
的方法,让外部来访问这个类的唯一实例。singletonData
:一些自定义的属性singletonOperation
:一些自定义的方法
2.2、使用模式
在Java中,单例模式的实现又分为两种,一种是懒汉式,一种是饿汉式。饿汉式、懒汉式是一种比较形象的称谓。在创建对象实例的处理上,有不同的实现方式,但是它们的目的一致的。代码示例如下:
2.2.1、懒汉式示例
/**
* 单例模式 懒汉式
*/
public class AppConfigLazy {
private String paramA;
private String paramB;
// 注意这里只有访问参数的get方法,没有设置参数的set方法
public String getParamA() {
return paramA;
}
public String getParamB() {
return paramB;
}
// 定义一个变量用来存储创建好的类实例,这里并不直接创建
// 懒汉式的体现
private static AppConfigLazy appConfig = null;
/**
* 定义一个方法来为客户端提供AppConfig类的实例
* @return AppConfig
*/
public static AppConfigLazy getInstance(){
if (appConfig == null){
appConfig = new AppConfigLazy();
}
return appConfig;
}
/**
* 私有构造方法
*/
private AppConfigLazy() {
// 读取配置文件
readConfig();
}
private void readConfig(){
Properties p = new Properties();
InputStream in = null;
try {
in = AppConfigLazy.class.getResourceAsStream("/appConfig.properties");
p.load(in);
this.paramA = p.getProperty("paramA");
this.paramB = p.getProperty("paramB");
}catch (Exception e){
System.out.println("读取文件报错e:" + e.getMessage());
e.printStackTrace();
}finally {
try {
in.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
@Override
public String toString() {
return "AppConfigLazy{" +
"hashCode='" + this.hashCode() + '\'' +
"paramA='" + paramA + '\'' +
", paramB='" + paramB + '\'' +
'}';
}
}
所谓懒汉式,既然是懒,那么在创建对象实例的时候就不着急,会一直等到第一次使用的时候才会创建,懒人嘛,总是推托不开的时候才去真正执行工作,因此在装载对象的时候不创建对象实例。
2.2.2、饿汉式示例
/**
* 单例模式 饿汉式
*/
public class AppConfigHungry {
private String paramA;
private String paramB;
// 注意这里只有访问参数的get方法,没有设置参数的set方法
public String getParamA() {
return paramA;
}
public String getParamB() {
return paramB;
}
// 定义一个变量用来存储创建好的类实例,直接在这里创建类实例只能创建一次
private static AppConfigHungry appConfig = new AppConfigHungry();
/**
* 定义一个方法来为客户端提供AppConfig类的实例
* @return AppConfig
*/
public static AppConfigHungry getInstance(){
return appConfig;
}
/**
* 私有构造方法
*/
private AppConfigHungry() {
// 读取配置文件
readConfig();
}
private void readConfig(){
Properties p = new Properties();
InputStream in = null;
try {
in = AppConfigHungry.class.getResourceAsStream("/appConfig.properties");
p.load(in);
this.paramA = p.getProperty("paramA");
this.paramB = p.getProperty("paramB");
}catch (Exception e){
System.out.println("读取文件报错e:" + e.getMessage());
e.printStackTrace();
}finally {
try {
in.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
@Override
public String toString() {
return "AppConfigHungry{" +
"hashCode='" + this.hashCode() + '\'' +
"paramA='" + paramA + '\'' +
", paramB='" + paramB + '\'' +
'}';
}
}
/**
* 单例模式 懒汉式 饿汉式
*/
public class UsePatternMain {
public static void main(String[] args) {
AppConfigLazy instanceLazy1 = AppConfigLazy.getInstance();
AppConfigLazy instanceLazy2 = AppConfigLazy.getInstance();
AppConfigLazy instanceLazy3 = AppConfigLazy.getInstance();
AppConfigHungry instanceHungry1 = AppConfigHungry.getInstance();
AppConfigHungry instanceHungry2 = AppConfigHungry.getInstance();
AppConfigHungry instanceHungry3 = AppConfigHungry.getInstance();
System.out.println(instanceLazy1);
System.out.println(instanceLazy2);
System.out.println(instanceLazy3);
System.out.println(instanceHungry1);
System.out.println(instanceHungry2);
System.out.println(instanceHungry3);
}
}
/* 打印结果
AppConfigLazy{hashCode='21685669'paramA='aaa', paramB='bbb'}
AppConfigLazy{hashCode='21685669'paramA='aaa', paramB='bbb'}
AppConfigLazy{hashCode='21685669'paramA='aaa', paramB='bbb'}
注意一下hashCode是一样的。
AppConfigHungry{hashCode='2133927002'paramA='aaa', paramB='bbb'}
AppConfigHungry{hashCode='2133927002'paramA='aaa', paramB='bbb'}
AppConfigHungry{hashCode='2133927002'paramA='aaa', paramB='bbb'}
注意一下hashCode是一样的。
懒汉式和饿汉式是单例模式的不同实现,目的是一样的
*/
所谓饿汉式,既然饿,那么在创建对象实例的时候就比较着急,饿了嘛,于是就在装载类的时候就创建对象实例。
三、模式介绍
3.1、认识单例模式
3.1.1、单例模式的功能
单例模式是用来保证这个类在运行期间只会被创建一个类实例,另外,单例模式还提供了一个全局唯一访问这个类实例的访问点,就是getInstance
方法。不管采用懒汉式还是饿汉式的实现方式,这个全局访问点是一样的。对于单例模式而言,不管采用何种实现方式,它都是只关心类实例的创建问题,并不关心具体的业务功能。
3.1.2、单例模式的范围(了解)
在多大范围内是单例呢?
观察上面的实现可以知道,目前Java里面实现的单例是一个虚拟机的范围。因为装载类的功能是虚拟机的,所以一个虚拟机在通过自己的ClassLoader
装载饿汉式实现单例类的时候就会创建一个类的实例。
这就意味着如果一个虚拟机里面有很多个ClassLoader
,而且这些ClassLoader
都装载某个类的话,就算这个类是单例,它也会产生很多个实例。当然,如果一个机器上有多个虚拟机,那么每个虚拟机里面都应该至少有一个这个类的实例,也就是说整个机器上就有很多个实例,更不会是单例了。
3.2、饿汉式和懒汉式实现(重点)
单例模式有两种典型的解决方案,一种叫懒汉式,另一种叫饿汉式。不管采用哪种方式,在运行期间都只会生成一个实例。而且这些类的全局访问点就是静态的getInstance
方法。
3.2.1、懒汉式
-
私有化构造方法
要想在运行期间控制某一个类的实例只有一个,首要的任务就是要控制创建实例的地方,也就是不能随随便便就可以创建类实例,否则就无法控制所创建的实例个数了。怎样才能让类的外部不能创建一个类的实例呢?把构造方法私有化。
-
提供获取实例的方法,并把获取实例的方法变成静态的,同时实现控制实例的创建
构造方法被私有化了,外部创建不了类实例就没有办法调用这个对象的方法,就实现不了功能调用。但是我们可以定义一个类方法
getlnstance
,通过这个类方法来创建实例,方便外面使用。在getlnstance
方法里面判断一下存放实例的属性有没有值,有则返回,没有创建后返回。 -
定义存储实例的属性,并把这个属性也定义成静态的
用一个属性来记录自己创建好的类实例。当第一次创建后,就把这个实例保存下来,以后就可以复用这个实例,而不是重复创建对象实例了。这个属性变量是在第一次创建时使用的,由于要在一个静态方法里面使用,所以这个属性被迫成为一个类变量,要强制加上
static
,这里并没有使用static
的特性。 -
示例代码如下:
public class Singleton { // 3、定义一个类变量,用来存储创建好的实例 // 缓存的思想,把频繁使用的数据存到内存中去 private static Singleton singleton = null; // 1、私有化构造方法,只能类内部使用,方便控制创建示例的数目 private Singleton() { } // 2、定义一个为外都提供实例的类方法 // 延迟加载思想的体现 public Singleton getInstance(){ // 判断存储实例的变量有没有值 if (singleton == null){ // 如果没有值创建实例赋给变量后在返回 singleton = new Singleton(); } // 如果有值直接返回 return singleton; } }
3.2.2、饿汉式
-
私有化构造方法
-
提供获取实例的方法,并把获取实例的方法变成静态的,方法内直接返回实例
实例在定义属性的时候已经创建好了,这里直接返回就行。
-
定义存储实例的属性,并把这个属性也定义成静态的,同时创建类的实例
static变量在类装载的时候进行初始化,多个实例的static变量会共享同一块内存区域。这里用到了static的特性。
-
代码示例:
public class SingletonHungry { // 3、定义一个类变量,用来存储创建好的实例 // 直接在这里创建实例,只能创建一次 private static SingletonHungry singleton = null; // 1、私有化构造方法,只能类内部使用,方便控制创建示例的数目 private SingletonHungry() { } // 2、定义一个为外都提供实例的类方法 public SingletonHungry getInstance(){ // 直接返回已经创建好的实例 return singleton; } }
3.3、延迟加载的思想
什么是延迟加载呢?通俗的来说,延迟加载就是一开始不要加载资源或者数据,一直等,等到马上就要使用这个资源或者数据了,躲不过去了才加载,所以也称Lazy Load
,不是懒惰哈,是延迟加载,这在实际开发中是一种很常见的思想,尽可能地节约资源。单例模式的懒汉式实现方式体现了延迟加载的思想。
public Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
3.4、缓存的思想
什么是缓存思想?当某些资源或者数据被频繁地使用,而这些资源或数据存储在系统外部,比如数据库、硬盘文件等,那么每次操作这些数据的时候都得从数据库或者硬盘上去获取,速度会很慢,将造成性能问题。一个简单的解决方法就是:把这些数据缓存到内存里面,每次操作的时候,先到内存里面找,看有没有这些数据,如果有,就直接使用,如果没有就获取它,并设置到缓存中,下一次访问的时候就可以直接从内存中获取了,从而节省大量的时间。当然,缓存是一种典型的空间换时间的方案。单例模式的懒汉式实现还体现了缓存的思想,缓存也是实际开发中常见的功能。
3.5、Java中缓存的基本实现
在Java开发中最常见的一种实现缓存的方式就是使用Map,基本步骤如下。
- 先到缓存里面查找,看看是否存在需要使用的数据。
- 如果没有找到,那么就创建一个满足要求的数据,然后把这个数据设置到缓存中,以备下次使用。如果找到了相应的数据,或者是创建了相应的数据,那就直接使用这个数据。
public class JavaCache {
/**
* 缓存数据容器,定义成map方便访问,直接根据key来获取value
*/
private Map<String,Object> map = new HashMap<String, Object>();
/**
* 从缓存中获取值
*
* @param key map 中的key
* @return key 对应的value
*/
public Object getValue(String key){
// 先从缓存中去取值
Object value = map.get(key);
// 判断缓存中有没有值
if (value == null){
// 如果没有值,去数据库或者读取文件等获取数据,这里只是演示,直接写个假的
value = "这是获取到的的value";
// 把获取到的value存到map中去
map.put(key,value);
}
// 此时把value返回
return value;
}
}
这里只是缓存的基本实现,还有很多功能都没有考虑,比如缓存的清除,缓存的同步等。当然,Java的缓存还有很多实现方式,也是非常复杂的,也有很多专业的缓存框架。
3.6、利用缓存实现单例模式
应用Java缓存的知识,可以变相实现Singleton
模式,也算是一个模拟实现吧。每次都先从缓存中取值。只要创建一次对象实例后,就设置了缓存的值,那么下次就不用再创建了。虽然不是很标准的做法,但是同样可以实现单例模式的功能。为了简单,先不去考虑多线程的问题,代码示例如下:
public class SingletonByMap {
private final static String DEFAULT_KEY = "cctop10";
private static Map<String,SingletonByMap> map = new HashMap<String, SingletonByMap>();
private SingletonByMap(){
}
public static SingletonByMap getInstance(){
SingletonByMap instance = map.get(DEFAULT_KEY);
if (instance == null){
instance = new SingletonByMap();
map.put(DEFAULT_KEY,instance);
}
return instance;
}
}
实现模式的方式有很多种,并不是只有懒汉式、饿汉式,上面这种也能实现单例所要求的功能,只不过实现比较麻烦,不是太好而已。,模式是经验的积累,模式的参考实现并不一定是最优的,对于单例模式,后面还会提到更好的实现方式。
3.7、单例模式的优缺点(重点)
3.7.1、时间和空间
懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。
饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断了,节省了运行时间。
3.7.2、线程安全
饿汉式是线程安全的,因为虚拟机保证只会装载一次,在装载类的时候是不会发生并发的。
懒汉式是线程不安全的,在多线程环境中可能会创建多个实例来。那怎样解决懒汉式线程安全的问题呢?双重检测加锁
public synchronized Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
上面这个示例应该也可以解决问题的,是的可以解决,但是这样还不是很好,因为这样会降低访问速度,每次都要判断。sychronized
关键字虽然能保证线程安全,但它的开销包括获取锁、线程上下文切换、锁竞争等,这些都会导致性能下降。要提高性能,可以通过减少同步的粒度、使用更细粒度的锁。那怎样才能更好呢?
使用双重检测加锁的方式来实现,就可以既实现线程安全,又能够使性能不受到很大的影响。所谓双重检查加锁机制,指的是:并不是每次进入getInstance
方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。来看一下代码示例
public class SingletonLazyPlus {
private volatile static SingletonLazyPlus instance = null;
private SingletonLazyPlus() {
}
public SingletonLazyPlus getInstance(){
if (instance == null){
synchronized (SingletonLazyPlus.class){
if (instance == null){
instance = new SingletonLazyPlus();
}
}
}
return instance;
}
}
双重检查加锁机制的实现会使用一个关键字volatile
,它的意思是:被volatile
修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是在第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。由于volatile
关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。
3.8、Lazy initialization holder class
根据上面的分析,常见的两种单例实现方式都存在小小的缺陷,那么有没有一种方案,既能够实现延迟加载,又能够实现线程安全呢?Lazy initialization holder class
方式,使用了类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。
什么是类级内部类?
- 有
static
修饰的成员式内部类。如果没有static
修饰的成员式内部类被称为对象级内部类。 - 类级内部类相当于其外部类的
static
成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。 - 类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。
- 类级内部类相当于其外部类的成员,只有在第一次被使用的时候才会被装载。
多线程缺省同步锁这是什么,从来没听说过?
在多线程开发中,为了解决并发问题,主要是通过使用synchronized
来加互斥锁进行同步控制。但是在某些情况中,JVM
已经隐含地为帮我们执行了同步,这些情况下就不用自己再来进行同步控制了。这些情况包括:
- 由静态初始化器(在静态字段上或
static{}
块中的初始化器)初始化数据时 - 访问final字段时
- 在创建线程之前创建对象时
- 线程可以看见它将要处理的对象时
解决思路
要想很简单地实现线程安全,可以采用静态初始化器的方式,它可以由JVM
来保证线程的安全性。比如前面的饿汉式实现方式。这样会浪费一定的空间吗?因为这种实现方式,会在类装载的时候就初始化对象,不管你需不需要。
如果现在有一种方法能够让类装载的时候不去初始化对象,那不就解决问题了?一种可行的方式就是采用类级内部类,在这个类级内部类里面去创建对象实例。这样一来,只要不使用到这个类级内部类,那就不会创建对象实例,从而同时实现延迟加载和线程安全。来看一下代码示例:
public class Singleton {
/**
* 私有化构造方法
*/
private Singleton() {
}
/**
* 类级内部类,也就是静态内部类,该内部类的实例和外部类的实例没有绑定关系,
* 而且只有被调用到的时候才会装载,从而实现了延迟加载。
*/
private static class SingletonInnerClass{
// 静态初始化器由JVM来保证线程安全。
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonInnerClass.instance;
}
}
当getInstance
方法第一次被调用的时候,它第一次读取SingletonInnerClass.instance
,导致SingletonInnerClass
类得到初始化。而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton
的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。这个模式的优势在于,getInstance
方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。
3.9、单例和枚举
简单回顾一下什么枚举?在 Java 中,枚举(enum
)是用来定义一组常量的特殊类型。enum
本质上是一个类,但它限制了该类只能有固定的几个实例。与传统的常量(如 static final
字段)相比,枚举具有更强的类型安全性、可维护性和可读性。还有一种表达是:一个类的对象,认为个数是有限且固定的,可以一一列举出来。
总结一下枚举的特点:
- Java的枚举类型本质上是功能齐全的类,可以拥有自己的属性和方法
- Java枚举通过公有的静态
final
域来为每个枚举常量创建实例 - 枚举类型是单例的泛型类,每个枚举常量实际上是该类的一个唯一实例。
接下来看一下枚举是怎样实现单例的,代码示例如下:
public enum SingletonEnum {
/**
* 定义一个枚举元素,它就代表了Singleton唯一的实例
*/
uniqueInstance("三国帅帅的超",18);
// 可以定义自己的属性和方法
private String name;
private Integer age;
SingletonEnum(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "SingletonEnum{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化的机制,并由JVM
从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。
3.10、何时选用
当需要控制一个类的实例只能有一个,而且客户只能从一个全局访问点访问它时,可以选用单例模式,这些功能恰好是单例模式要解决的问题。
总结
单例模式是为了保证某个类在整个系统中只有一个实例,并通过一个全局的访问点让所有部分都能共享这个实例。它的主要作用是避免重复创建对象,节省资源,确保数据的一致性。
这里我们通过几个问题总结一下全文:
- 单例模式的本质是什么?
- 饿汉式和懒汉式是怎样实现的,怎样保证线程安全?
- 除了饿汉式、懒汉式还有其他实现单例的方式吗?
- 缓存的思想、延迟加载的思想还记得吗?
- 单例模式优点是什么?