博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
单例模式
阅读量:7089 次
发布时间:2019-06-28

本文共 5215 字,大约阅读时间需要 17 分钟。

1、什么是单例模式?

  采取一定的办法保证在整个软件系统中,单例模式确保对于某个类只能存在一个实例。有如下三个特点:

  ①、单例类只能有一个实例

  ②、单例类必须自己创建自己的实例

  ③、单例类必须提供外界获取这个实例的方法

 

2、单例类的设计思想(Singleton)

  ①、外界不能创建这个类的实例,那么必须将构造器私有化。

1
2
3
4
5
6
7
public 
class 
Singleton {
    
//构造器私有化
    
private 
Singleton(){
         
    
}
 
}

  ②、单例类必须自己创建自己的实例,不能允许在类的外部修改内部创建的实例,所以将这个实例用 private 声明。为了外界能访问到这个实例,我们还必须提供 get 方法得到这个实例。因为外界不能 new 这个类,所以我们必须用 static 来修饰字段和方法。

1
2
3
4
5
6
7
//在类的内部自己创建实例
    
private 
static 
Singleton singleton = 
new 
Singleton();
 
    
//提供get 方法以供外界获取单例
    
public 
Singleton getInstance(){
        
return 
singleton;
    
}

 

3、单例模式之饿汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public 
class 
Singleton {
    
//构造器私有化
    
private 
Singleton(){
         
    
}
    
//在类的内部自己创建实例
    
private 
static 
Singleton singleton = 
new 
Singleton();
 
    
//提供get 方法以供外界获取单例
    
public 
static 
Singleton getInstance(){
        
return 
singleton;
    
}
     
}

  测试:

1
2
3
4
5
public 
static 
void 
main(String[] args) {
    
Singleton s1 = Singleton.getInstance();
    
Singleton s2 = Singleton.getInstance();
    
System.out.println(s1.equals(s2)); 
//true
}

  

  这种模式避免了多线程的同步问题,不过在 类装载的时候就进行了实例化,有可能这个实例化过程很长,那么就会加大类装载的时间;有可能这个实例现阶段根本用不到,那么创建了这个实例,也会浪费内存。没有达到 lazy-loading 的效果。

 

4、单例模式之懒汉模式(线程不安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//懒汉模式
public 
class 
Singleton {
    
//构造器私有化
    
private 
Singleton(){
         
    
}
    
//在类的内部自己创建实例的引用
    
private 
static 
Singleton singleton = 
null
;
 
    
//提供get 方法以供外界获取单例
    
public 
static 
Singleton getInstance(){
        
if
(singleton == 
null
){
            
singleton = 
new 
Singleton();
        
}
        
return 
singleton;
    
}
     
}

  这种方法达到了 lazy-loading 的效果,即我们在第一次需要得到这个单例的时候,才回去创建它的实例,以后再需要就可以不用创建,直接获取了。但是这种设计在多线程的情况下是不安全的。

 

  我们可以创建两个线程来看看这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public 
class 
ThreadSingleton 
extends 
Thread{
    
@Override
    
public 
void 
run() {
        
try 
{
            
System.out.println(Singleton.getInstance());
        
catch 
(Exception e) {
            
e.printStackTrace();
        
}
    
}
    
public 
static 
void 
main(String[] args) {
        
ThreadSingleton s1 = 
new 
ThreadSingleton();
        
s1.start(); 
//com.ys.pattern.Singleton@5994a1e9
         
        
ThreadSingleton s2 = 
new 
ThreadSingleton();
        
s2.start(); 
//com.ys.pattern.Singleton@40dea6bc
    
}
}

  很明显:最后输出结果的两个实例是不同的。这便是线程安全问题。那么怎么解决这个问题呢?

  参考这篇博客:Java多线程同步:

 

5、单例模式之懒汉模式(线程安全)

  这里我们采用同步代码块来达到线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//懒汉模式线程安全
public 
class 
Singleton {
    
//构造器私有化
    
private 
Singleton(){
         
    
}
    
//在类的内部自己创建实例的引用
    
private 
static 
Singleton singleton = 
null
;
 
    
//提供get 方法以供外界获取单例
    
public 
static 
Singleton getInstance() 
throws 
Exception{
        
synchronized 
(Singleton.
class
) {
            
if
(singleton == 
null
){
                
singleton = 
new 
Singleton();
            
}
        
}
        
return 
singleton;
    
}
     
}

  

6、单例模式之懒汉模式(线程安全)--双重校验锁

  分析:上面的例子我们可以看到,synchronized 其实将方法内部的所有语句都已经包括了,每一个进来的线程都要单独进入同步代码块,判断实例是否存在,这就造成了性能的浪费。那么我们可以想到,其实在第一次已经创建了实例的情况下,后面再获取实例的时候,可不可以不进入这个同步代码块?

  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//懒汉模式线程安全--双重锁校验
public 
class 
Singleton {
    
//构造器私有化
    
private 
Singleton(){
         
    
}
    
//在类的内部自己创建实例的引用
    
private 
static 
Singleton singleton = 
null
;
 
    
//提供get 方法以供外界获取单例
    
public 
static 
Singleton getInstance() 
throws 
Exception{
        
if
(singleton == 
null
){
            
synchronized 
(Singleton.
class
) {
                
if
(singleton == 
null
){
                    
singleton = 
new 
Singleton();
                
}
            
}
        
}
        
return 
singleton;
    
}
     
}

  以上的真的完美解决了单例模式吗?其实并没有,请看下面:

 

7、单例模式之最终版

  我们知道编译就是将源代码翻译成机械码的过程,而Java虚拟机的目标代码不是本地机器码,而是虚拟机代码。编译原理里面有个过程是编译优化,就是指在不改变原来语义的情况下,通过调整语句的顺序,来让程序运行的更快,这个过程称为 reorder。

  JVM 只是一个标准,它并没有规定有关编译器优化的内容,也就是说,JVM可以自由的实现编译器优化。

  那么我们来再来考虑一下,创建一个变量需要哪些步骤?

    ①、申请一块内存,调用构造方法进行初始化

    ②、分配一个指针指向该内存

  而这两步谁先谁后呢?也就是存在这样一种情况:先开辟一块内存,然后分配一个指针指向该内存,最后调用构造方法进行初始化。

 

  那么针对单例模式的设计,就会存在这样一个问题:线程 A 开始创建 Singleton 的实例,此时线程 B已经调用了 getInstance的()方法,首先判断 instance 是否为 null。而我们上面说的那种模型, A 已经把 instance 指向了那块内存,只是还没来得及调用构造方法进行初始化,因此 B 检测到 instance 不为 null,于是直接把  instance 返回了。那么问题出现了:尽管 instance 不为 null,但是 A 并没有构造完成,就像一套房子已经给了你钥匙,但是里面还没有装修,你并不能住进去。

  解决方案:使用 volatile 关键字修饰 instance

  我们知道在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。

  volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//懒汉模式线程安全--volatile
public 
class 
Singleton {
    
//构造器私有化
    
private 
Singleton(){
         
    
}
    
//在类的内部自己创建实例的引用
    
private 
static 
volatile 
Singleton singleton = 
null
;
 
    
//提供get 方法以供外界获取单例
    
public 
static 
Singleton getInstance() 
throws 
Exception{
        
if
(singleton == 
null
){
            
synchronized 
(Singleton.
class
) {
                
if
(singleton == 
null
){
                    
singleton = 
new 
Singleton();
                
}
            
}
        
}
        
return 
singleton;
    
}
     
}

 

到此我们完美的解决了单例模式的问题。但是 volatile  关键字是 JDK1.5 才有的,也就是 JDK1.5 之前是不能这样用的

 

PS:我们还可以使用 枚举类型 或静态内部类来实现单例模式

8、单例模式之枚举类

1
2
3
4
public 
enum 
Singleton{
    
INSTANCE;
    
private 
Singleton(){}
}

  

9、单例模式之静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public 
class 
InnerSingleton {
    
private 
InnerSingleton(){}
    
public 
static 
InnerSingleton getInstance(){
        
return 
Inner.instance;
    
}
    
static 
class 
Inner{
        
static 
InnerSingleton instance = 
new 
InnerSingleton();
    
}
 
    
public 
static 
void 
main(String [] args){
        
System.out.println(InnerSingleton.getInstance()==InnerSingleton.getInstance());
//true
        
System.out.println(InnerSingleton.getInstance().equals(InnerSingleton.getInstance()));
//true
 
    
}

 

单例模式的应用:

  1、windows 系统的回收站,我们能在任何盘符删除数据,但是最后都是到了回收站中

  2、网站的计数器,不过不采用单例模式,很难实现同步

  3、数据库连接池,可以节省打开或关闭数据库连接所引起的效率损耗,用单例模式来维护,可以大大降低这种损耗。

 

由上可以总结单例模式的应用场景:

  ①、资源共享

  ②、方便资源互相通信  

 

转载于:https://www.cnblogs.com/zhoanghua/p/9292300.html

你可能感兴趣的文章
JSP放入Jar包支持
查看>>
润乾报表使用json数据源的方法改进
查看>>
小蚂蚁学习PS切图之基础操作(2)——工具栏的介绍
查看>>
【Mybatis】- sqlSession工作流程
查看>>
mysql str_to_date字符串转换为日期
查看>>
jsp---EL运算符
查看>>
剥去中国崛起的外衣(画皮)!
查看>>
Oracle中的substr方法
查看>>
Mysql日期和时间函数总结
查看>>
创建逻辑卷 安装lvm命令
查看>>
不使用root身份运行Wireshark
查看>>
PageRank算法计算网页的价值
查看>>
js面向对象
查看>>
DEDECMS 修改广告链接地址
查看>>
抓住“扁平化”
查看>>
Python中method的参数传递详解
查看>>
Skia深入分析1——skia上下文
查看>>
Tiny Jpeg Decoder (JPEG解码程序) 源代码分析 1:解码文件头
查看>>
windows Server2008 下部署nginx
查看>>
利用https实现站点的访问
查看>>