浅析设计模式之四 单例模式

  (单例模式) singleton pattern
       
     场景:
        线程池/缓存/对话框/处理偏好设置/注册表/日志        打印机/显卡驱动     java的Calender
      面临问题:
         程序的行为异常
         资源使用过量
         结果不一致
     全局变量:
        程序开始时创建,对象消耗资源又使用的较少或者没有使用 ==> 资源浪费
        利用静态类变量,静态方法,访问修饰符也可以实现同样效果    
     单例:
        确保程序中使用的全局资源只有一份,管理共享的资源
 
    现在实现一个巧克力工厂的锅炉
/**
 * 巧克力锅炉
 * @author Dougest
 * 2017年6月28日   上午10:32:03
 * 
 */
public class ChocolateBoiler {
    private volatile boolean empty;
    private volatile boolean boiled;//沸腾
    
    public ChocolateBoiler() {
        empty = true;
        boiled = false;
    }
    //空了就填东西
    public void fill() {
        if(isEmpty()) {
            empty = false;
            boiled = false;
        }
    }
    //烧沸就排出来
    public void drain() {
        if(!isEmpty() && isBoiled()) 
            empty = true;
    }
    //开始煮
    public void boil() {
        if(!isEmpty() && !isBoiled()) 
            boiled = true;
    }
    
 
    public boolean isEmpty() {
        return empty;
    }
 
    public boolean isBoiled() {
        return boiled;
    }
}

 

这个时候如果存在多余一个的锅炉就会发生很可怕的事,现在把他设计成 单件
public class ChocolateBoiler {
    private volatile boolean empty;
    private volatile boolean boiled;//沸腾
    
    private ChocolateBoiler() {
        empty = true;
        boiled = false;
    }
    
    public static ChocolateBoiler getInstance(){
        return new ChocolateBoiler();
    }
    
    //空了就填东西
    public void fill() {
        if(isEmpty()) {
            empty = false;
            boiled = false;
        }
    }
    //烧沸就排出来
    public void drain() {
        if(!isEmpty() && isBoiled()) 
            empty = true;
    }
    //开始煮
    public void boil() {
        if(!isEmpty() && !isBoiled()) 
            boiled = true;
    }
    
 
    public boolean isEmpty() {
        return empty;
    }
 
    public boolean isBoiled() {
        return boiled;
    }
    
}

 

很简单
单例模式
        确保一个类只有一个实例,并提供一个全局的访问点
 
注意:
    单件模式的类也可以是一般的类,具有一般的数据和方法
    getInstance()是一个静态方法,任何地方都可以访问
    他可以延迟实例化
 
    继续看上面的代码,尽管我们使用单件模式来改造他,但是多线程中fill()方法仍然可以在加热过程中加入原料,造成材料溢出
public class ChocolateBoiler {
    private volatile boolean empty;
    private volatile boolean boiled;//沸腾
    private static ChocolateBoiler chocolateBoiler;
    private ChocolateBoiler() {
        empty = true;
        boiled = false;
    }
    
    public static synchronized ChocolateBoiler getInstance(){
        if(chocolateBoiler == null) {
            chocolateBoiler = new ChocolateBoiler();
        }
        return chocolateBoiler;
    }

 

改善
    1    加锁似乎能够避免这个问题,但是一旦设置好chocolateBoiler 变量就不再需要同步这个方法了,之后的每次调用这个方法,
        同步都是一种累赘(拖累性能==>同步一个方法会吧程序执行效率降低100倍)
        我们试着改善多线程(如果你对getInstance()负担无所要求,没有频繁使用,可以不用考虑)
 
 
2 如果程序总是被创建并且使用单例对象或者在创建和运行时负担不是太重,可以"eagerly"的创建对象
public class Singleton {
    private static Singleton singleton = new Singleton();
    private Singleton(){}
    public static Singleton getInstance() {
        return singleton;
    }
}

 

利用这个做法,我们依赖JVM在加载这个类时马上创建此唯一实例
JVM保证在任何线程访问singleton静态变量之前,一定先创建此实例
 
3.使用双重检查加锁,在getInstance()中减少使用同步
double-checked locking,首先检查是否实例已经被创建了,如果未创建,才同步创建,这样一来,只有第一次同步,是我们想要的
public class Singleton {
    //volatile关键字 在1.4版本以上双重检查加锁失效
    private volatile static Singleton singleton;
    private Singleton(){}
    public static Singleton getInstance() {
        if(singleton == null) {//不存在则进入同步块
            synchronized(Singleton.class) {
                if(singleton == null) //不存在则实例化
                    singleton = new Singleton();
            }
        }
        return singleton;
    }
    
}

 在这里我要特别解释一下上面代码中的volatile关键字

Java内存模型(jmm)并不限制处理器的重排序,执行 singleton = new Singleton();时,他可能并不是原子语句,执行过程中经历了三大步骤(详情可见下面反汇编内容):
1.为对象分配内存
2.初始化实例对象
3.引用instance指向分配的空间
处理器会进行指令重排序优化(关于处理器可见我另外一篇文章-- 链接),所以并不能保证这三个步骤按顺序执行
当优化重排后执行顺序为1,3,2时,这样在线程1遇到3时,singleton 已经不是null值了;但是执行到2时判断判断singleton !=null,而直接返回singleton 引用,但是此时singleton 还没有初始化完毕,因此造成程序崩溃
而这个volatile的作用就相对避免了这种情况(但是不能百分之百避免)
 
至于这个关键字的作用,我就懒得自己总结了,贴一段国外资料-- 链接:

Java中的volatile

Java支持volatile关键字,但它被用于其他不同的用途。当volatile用于一个作用域时,Java保证如下:

  1. (适用于Java所有版本)读和写一个volatile变量有全局的排序。也就是说每个线程访问一个volatile作用域时会在继续执行之前读取它的当前值,而不是(可能)使用一个缓存的值。(但是并不保证经常读写volatile作用域时读和写的相对顺序,也就是说通常这并不是有用的线程构建)。
  2. (适用于Java5及其之后的版本)volatile的读和写建立了一个happens-before关系,类似于申请和释放一个互斥锁。

使用volatile会比使用更快,但是在一些情况下它不能工作。volatile使用范围在Java5中得到了扩展,特别是双重检查锁定现在能够正确工作。

 
 
上面的代码可以大大的减少getInstance()的时间消耗
有兴趣可以cmd切到文件夹下编译成class文件,然后使用javap命令反汇编看一下,如下

 


 对照我的博客一步步来   http://www.cnblogs.com/dougest/p/7067710.html
 
public class com.hola.design.singleton.Singleton {                                                      
  public static com.hola.design.singleton.Singleton getInstance();                                      
    Code:                                                                                               
       0: getstatic     #2                  //从类中获取字段
       3: ifnonnull     37                   //不等于null则跳转                                                           
       6: ldc           #3                     //  把常量中的项压入栈顶        
       8: dup                                 //复制栈顶部的一个字长的内容                                                        
       9: astore_0                       //将引用类型存入局部变量0中                                                  
      10: monitorenter                  // 进入并获取对象监视器                                               
      11: getstatic     #2                  // 从类中获取字段    
      14: ifnonnull     27                  //不等于null则跳转                                                            
      17: new           #3                  // 创建类实例               
      20: dup                                   //复制栈顶部的一个字长的内容                                                          
      21: invokespecial #4              // 调用需要特殊处理的实例方法                                      
      24: putstatic     #2                  // 设置类中静态字段的值    
      27: aload_0                           // 从局部变量0中装载引用类型值                                                           
      28: monitorexit                      //释放并退出对象监视器                                                             
      29: goto          37                  //无条件跳转                                                           
      32: astore_1                         //将引用类型或returnAddress类型值存入局部变量1                                                             
      33: aload_0                          //将引用类型存入局部变量0中                                                              
      34: monitorexit                     //释放并退出对象监视器                                                              
      35: aload_1                           //从局部变量1中装载引用类型值                                                           
      36: athrow                            //跑出异常                                                            
      37: getstatic     #2                  // Field singleton:Lcom/hola/design/singleton/Singleton;  从类中获取字段  
      40: areturn                              //从方法中返回引用类型的数据                                                         
    Exception table:                                                                                    
       from    to  target type                                                                          
          11    29    32   any                                                                          
          32    35    32   any                                                                          
}                                                                                                       
                                                                                                        
具体javap的东西可以自行我不多解释
 
这里讲一下我 个人对单例模式的理解
    单例模式只是将一个对象做成唯一的存在,整个系统都能访问;
    不管系统有多大,我们只存在这一个对象
    为了保证这个对象被创建时只有一个,我们对他进行了特定的操作
     这个对象本身没有什么迷惑的地方,我们创建他只是为了调用他的某个方法,使用他的某个功能
    这个功能也可能会对资源操作,涉及到资源可能就会线程问题,涉及到线程问题就会想到并发,锁,同步异步等等
    ---个人理解结束---
 
问题:两个类加载器可能有机会创建各自的单例??
有可能     每个类加载器都定义了一个命名空间,如果有两个以上的类加载器有可能会加载同一个类 == > 从程序上看一个类会被加载多次
单例也会如此,如果程序中有多个类加载器就要小心了
解决办法:自行指定类加载器,并指定同一个类加载器
 
要点:
    单件模式确保程序中一个类最多有一个实例
    单件模式支持提供访问这个实例的全局点
    java中实现单件模式一定要有私有构造器,一个静态方法,一个静态变量
    确定性能和资源上的限制,然后小心的选择适当的方案实现单件,已解决多线程的问题(我们必须认定所有的程序都是多线程的)
    jdk1.4以下双重检查加锁会失效
    多个类加载器可能会导致多个单件实例
    JVM1.2之前版本必须建立单件注册表,避免垃圾回收

转载于:https://www.cnblogs.com/dougest/p/7089475.html

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值