单例模式大汇总

简介:
看了多方资料,整理下单例设计模式,有不少值得相互探究的地方,你就会发现就这一个小小的单例模式竟然映射出N多知识。我在这里把问题综述出来,一起相互探讨。 

单例涉及到的相关文章如下: 
                反射、枚举与单例 
                序列化与单例 
                类加载器与单例 

本文则主要是讲多线程与单例。 
单例模式首先分为懒汉式和饿汉式。所谓饿汉式即一开始就创建出单例对象,懒汉式则为当需要使用的时候才会去创建出单例对象。
 
先看下饿汉式: 
?
1
2
3
4
5
6
7
8
9
10
public final class Singleton {
 
     private static final Singleton instance= new Singleton();
     
     private Singleton(){}
     
     public static Singleton getInstance(){
         return instance;
     }
}

1 私有化构造器,使得别人无法再创建新对象。 
问题1:即使私有化构造器,别人仍然可以通过反射机制来创建新对象,要是这样的话,下面的很多单例方法都不再是单例。然而枚举除外。 

2 这种饿汉式的方式在类加载器加载Singleton的时候就会去初始化创建一个Singleton实例,类加载器加载Singleton时线程安全的,所以这种方式不存在线程安全问题。 

懒汉式:有时候为了在使用的时候才去创建单例对象,就要采用懒汉式
 
?
1
2
3
4
5
6
7
8
9
10
11
12
13
public final class Singleton {
 
     private static Singleton instance= null ;
     
     private Singleton(){}
     
     public static Singleton getInstance(){
         if (instance== null ){
             instance= new Singleton();
         }
         return instance;
     }
}

这种方式即在需要的时候才会去创建单例对象。很明显,大家都知道这种方式引入了线程安全问题,所以要对getInstance方法加上锁,如下:  
?
1
2
3
4
5
6
7
8
9
10
11
12
13
public final class Singleton {
 
     private static Singleton instance= null ;
     
     private Singleton(){}
     
     public static synchronized Singleton getInstance(){
         if (instance== null ){
             instance= new Singleton();
         }
         return instance;
     }
}

这样的话,每个线程要执行getInstance方法时,synchronized对他们进行了同步,保证并发情况下只有一个线程在执行getInstance方法。这种做法的的确解决了线程安全问题,但是却造成了很大的性能开销。因为instance只需要在第一次创建时进行同步,创建后每次获取时不需要再进行同步,所以我们要进一步改进:  
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class Singleton {
 
     private static Singleton instance= null ;
     
     private Singleton(){}
     
     public static Singleton getInstance(){
         if (instance== null ){
             synchronized (Singleton. class ) {
                 instance= new Singleton();
             }
         }
         return instance;
     }
}

这种方式即缩小了同步的范围,保证了在单例对象创建出来后,每次获取时不需要再进行同步,但是又造成了一个问题,即不能保证instance=new Singleton()只被执行一次,所以又要改进,需要在同步的代码中再次检查是否已经创建,如下:  
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final 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()实际上分为三个过程: 
1 分配内存 
2 对Singleton的一些初始化工作包括构造函数的执行 
3 对instance变量赋值内存地址 
然而对于第2步和第3步,不同的编译器由于执行了优化导致他们的执行顺序并不一致,即发生了重排序,对于重排序参见这篇infoq上的文章http://www.infoq.com/cn/articles/java-memory-model-2 
也就是线程1当执行到第2步的时候,instance就已经有值了,此时线程2执行getInstance方法的最外层的if(instance==null)判断就会直接返回。然而该对象还没有真正的完成初始化,还不能正常使用。此时线程2如果去使用该对象,就会出问题了。 

为了解决这个问题,就是不允许第2步和第3步进行重排序,使用volatile来解决,如下:
 
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final class Singleton {
 
     private volatile static Singleton instance= null ;
     
     private Singleton(){}
     
     public static  Singleton getInstance(){
         if (instance== null ){
             synchronized (Singleton. class ) {
                 if (instance== null ){
                     instance= new Singleton();
                 }
             }
         }
         return instance;
     }
}

只需要在Singleton instance变量上加上volatile修饰,就可以禁止重排序。我们知道synchronized 即保证可见性又保证了互斥性,而volatile则仅仅是保持了可见性,而这里volatile又起到禁止重排序的功能(我也不懂,留给大神们去研究)。 

另一种解决方案是,基于类初始化的解决方案:
 
?
1
2
3
4
5
6
7
8
9
10
11
12
public final class Singleton {
 
     private static class SingletonHolder{
         public static Singleton instance= new Singleton();
     }
     
     private Singleton(){}
     
     public static  Singleton getInstance(){
         return SingletonHolder.instance;
     }
}

这也是一种常见的懒汉式单例,接下来我们就要分析分析它是如何解决多线程问题的。 
当多个线程执行SingletonHolder.instance时,会首先进行类的初始化,即多个线程可能同时去初始化同一个类,这方面对于jvm来说是进行了细致的同步,每个类都有一个初始化锁,来确保只能有一个线程来初始化类。当线程A获取了SingletonHolder类的初始化锁,线程B则需要等待,线程A就要去执行SingletonHolder的静态变量表达式、静态代码块等初始化工作,然后就能确保Singleton instance=new Singleton()只被一个线程来执行。 
总的来说,此种方法是依靠jvm对类和接口的同步来实现单例线程安全的。具体jvm对于类和接口初始化的同步过程可以见这篇文章http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization 
相关文章
|
SQL 数据格式
视图有哪些特点?哪些使用场景?
视图有哪些特点?哪些使用场景?
|
设计模式 前端开发 开发者
css 三栏布局的实现
css 三栏布局的实现
238 0
|
9月前
|
安全 Linux 数据安全/隐私保护
Linux权限揭秘“Root与Sudo”
Root用户是Linux系统中的超级用户,拥有对系统的完全控制权。Root用户几乎可以执行任何命令,修改任何文件,甚至删除系统上的所有内容。因此,Root用户的使用需要非常谨慎,以避免潜在的安全风险。
450 6
|
12月前
|
XML JSON API
如何使用Python将字典转换为XML
本文介绍了如何使用Python中的`xml.etree.ElementTree`库将字典数据结构转换为XML格式。通过定义递归函数处理字典到XML元素的转换,生成符合标准的XML文档,适用于与旧系统交互或需支持复杂文档结构的场景。示例代码展示了将一个简单字典转换为XML的具体实现过程。
214 1
|
消息中间件 关系型数据库 MySQL
[flink 实时流基础] 输出算子(Sink)
[flink 实时流基础] 输出算子(Sink)
826 1
|
网络协议 数据安全/隐私保护 安全
|
并行计算 监控 网络协议
西门子PLC常用的通讯接口和通讯协议有哪些?RS232、RS485、PPI、MPI、Modbus、Profibus、Uss的特点
西门子PLC常用的通讯接口和通讯协议有哪些?RS232、RS485、PPI、MPI、Modbus、Profibus、Uss的特点
西门子PLC常用的通讯接口和通讯协议有哪些?RS232、RS485、PPI、MPI、Modbus、Profibus、Uss的特点
|
存储 算法 数据可视化
python多种算法对比图解实现 验证二叉树搜索树【力扣98】
python多种算法对比图解实现 验证二叉树搜索树【力扣98】
|
Android开发
Android保存图片到相册(适配android 10以下及以上)
Android保存图片到相册(适配android 10以下及以上)
325 1
|
算法 数据处理 C语言
【C++迭代器深度解析】C++迭代器类型之间的继承关系与反向迭代器的独特性
【C++迭代器深度解析】C++迭代器类型之间的继承关系与反向迭代器的独特性
258 0