设计模式之单例模式

设计模式(Design pattern):

是一套被反复使用, 多数人知晓的, 经过分类遍目的,代码设计经验的总结.

 

目的:

使用设计模式是为了可重用代码, 让代码更容易被他人理解, 保证代码的可靠性

 

单例模式(Singleton): 

保证整个应用中某个实例有且只有一个

 

有些对象我们只需要一个: 配置文件, 工具类, 线程池, 缓存, 日志对象等.

如果创造出多个实例就会导致训多问题: 比如占用过多资源,不一致的结果等

 

单例模式三要素:

1. 静态的私有的本类型的变量

2. 构造方法私有化

3. 提供一个公共的静态的入口点方法


//第一种: 饿汉式  
public class SingletonParrent01 {  
    public static void main(String[] args){  
        Singleton s1 = Singleton.getInstance();  
        Singleton s2 = Singleton.getInstance();  
        System.out.println(s1 == s2);  
    }  
 
class Singleton{  
      
    private Singleton(){}  
      
    private static Singleton instance = new Singleton();;  
      
    public static Singleton getInstance(){  
        return instance;  
   }  
} 

类加载阶段创建对象并且只创建一次

不好处无论这个类是否被使用,都会创建所以很多创建过程是无用的.



//第二种: 懒汉式lazy loaded   
public class SingletonParrent2 {  
    public static void main(String[] args){  
        Singleton s1 = Singleton.getInstance();  
        Singleton s2 = Singleton.getInstance();  
        System.out.println(s1 == s2);  
        System.out.println(s1);  
        System.out.println(s2);  
    }  
}  
  
class Singleton{  
    private Singleton(){}  
    private static Singleton instance;  
    public static Singleton getInstance(){  
        if(instance == null)  
            instance = new Singleton();  
          
        return instance;  
    }  
}  

上面的代码很清楚,也很简单。然而就像那句名言:“80%的错误都是由20%代码优化引起的。单线程下,这段代码没有什么问题,可是如果是多线程,麻烦就来了。

 

我们来分析一下:

线程A希望使用Singleton,调用getInstance()方法。因为是第一次调用,A就发现instancenull的,于是它开始创建实例,就在这个时候,CPU发生时间片切换,线程B开始执行,它要使用Singleton,调用getInstance()方法,同样检测到instancenull——注意,这是在A检测完之后切换的,也就是说A并没有来得及创建对象——因此B开始创建。B创建完成后,切换到A继续执行,因为它已经检测完了,所以A不会再检测一遍,它会直接创建对象。这样,线程AB各自拥有一个Singleton的对象——单例失败!

所以懒汉式是线程不安全的

 

所以,我们用加锁来试一下.


public class Singleton {   
  
      private static Singleton instance = null;   
      
    public synchronized static Singleton getInstance() {   
        if(instance == null) {   
          instance = new Singleton();   
        }   
          return instance;   
      }   
      
    private Singleton() {}       
}  

这样的用法,在性能上会有所下降,因为每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了

 

但是不管是否为Null, 只要调用getInstance()方法都会进入synchronized同步代码块速度会很慢性能很低.让我们来分析一下,究竟是整个方法都必须加锁,还是仅仅其中某一句加锁就足够了?我们为什么要加锁呢?分析一下出现lazy loaded的那种情形的原因。原因就是检测null的操作和创建对象的操作分离了。如果这两个操作能够原子地进行,那么单例就已经保证了。于是,我们开始修改代码:


//double-checked locking  
public class Singleton {   
  
    private static Singleton instance = null;   
  
    public static Singleton getInstance() {   
        if (instance == null) {   
            synchronized (Singleton.class) {   
                if (instance == null) {   
                    instance = new Singleton();   
                }   
            }   
        }   
        return instance;   
        }   
  
    private Singleton() {   
  
    }   
}  

首先判断instance是不是为null,如果为null,加锁初始化;如果不为null,直接返回instance



从源头检查:

下面我们开始说编译原理。所谓编译,就是把源代码翻译成目标代码——大多数是指机器代码——的过程。针对Java,它的目标代码不是本地机器代码,而是虚拟机代码。编译原理里面有一个很重要的内容是编译器优化。所谓编译器优化是指,在不改变原来语义的情况下,通过调整语句顺序,来让程序运行的更快。这个过程成为reorder。要知道,JVM只是一个标准,并不是实现。JVM中并没有规定有关编译器优化的内容,也就是说,JVM实现可以自由的进行编译器优化。

 

下面来想一下,创建一个变量需要哪些步骤呢?

一个是申请一块内存,调用构造方法进行初始化操作,另一个是分配一个指针指向这块内存。这两个操作谁在前谁在后呢?JVM规范并没有规定。那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。

 

下面我们来考虑这么一种情况:线程A开始创建SingletonClass的实例,此时线程B调用了getInstance()方法,首先判断instance是否为null。按照我们上面所说的内存模型,A已经把instance指向了那块内存,只是还没有调用构造方法,因此B检测到instance不为null,于是直接把instance返回了——问题出现了,尽管instance不为null,但它并没有构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,因为里面还没有收拾。此时,如果BAinstance构造完成之前就是用了这个实例,程序就会出现错误了!

 

 

解决方案:

说了这么多,难道单例没有办法在Java中实现吗?其实不然!在JDK 5之后,Java使用了新的内存模型。volatile关键字有了明确的语义——JDK1.5之前,volatile是个关键字,但是并没有明确的规定其用途——volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整!因此,只要我们简单的把instance加上volatile关键字就可以了。

/* 
   volatile关键字 
*/  
public class Singleton {   
  
  private volatile static Singleton instance = null;   
  
  public static Singleton getInstance() {   
    if (instance == null) {   
      synchronized (Singleton.class) {   
        if(instance == null) {   
          instance = new Singleton();   
        }   
      }   
    }   
    return instance;   
  }   
  
  private Singleton() {   
  
  }   
      
}  

而,这只是JDK1.5之后的Java的解决方案,那之前版本呢?其实,还有另外的一种解决方案,并不会受到Java版本的影响:


/* 
    静态内部类方式 
*/  
public class Singleton {   
      
  private static class SingletonInstance {   
    private static final Singleton instance = new Singleton();   
  }   
  
  public static Singleton getInstance() {   
    return SingletonInstance.instance;   
  }   
  
  private Singleton() {   
  
  }   
      
} 

在这一版本的单例模式实现代码中,我们使用了Java的静态内部类。这一技术是被JVM明确说明了的,因此不存在任何二义性。在这段代码中,因为SingletonClass没有static的属性,因此并不会被初始化。直到调用getInstance()的时候,会首先加载SingletonClassInstance类,这个类有一个staticSingletonClass实例,因此需要调用SingletonClass的构造方法,然后getInstance()将把这个内部类的instance返回给使用者。由于这个instancestatic的,因此并不会构造多次。

由于SingletonClassInstance是私有静态内部类,所以不会被其他类知道,同样,static语义也要求不会有多个实例存在。并且,JSL规范定义,类的构造必须是原子性的,非并发的,因此不需要加同步块。同样,由于这个构造是并发的,所以getInstance()也并不需要加同步。

至此,我们完整的了解了单例模式在Java语言中的时候,提出了两种解决方案。个人偏向于第二种,并且Effiective Java也推荐的这种方式。


几个例子:

/* 
 * 单例模式管理用户 
 * */  
public class UserManager {  
      
    private  static UserManager instance = new UserManager();  
      
    private UserManager(){}  
      
    public static UserManager getInstance(){  
        return instance;  
    }  
  
} 


/* 
   封装数据库操作 
*/  
public final class DbHelper {  
  
    private static DbHelper instance = null;  
   
    public static DbHelper getInstance() {  
        if (instance == null) {  
            // 给类加锁 防止线程并发  
            synchronized (DbHelper.class) {  
                if (instance == null) {  
                    instance = new DbHelper();  
                }  
            }  
        }  
        return instance;  
    }  
   
    private DbHelper() {}  
   
    // 获得连接getConnection()  
  
    // 关闭连接close()  
      
    //...  
}  








  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值