美文网首页
Java单例模式详解

Java单例模式详解

作者: 在挖坑的猿 | 来源:发表于2017-10-31 14:47 被阅读0次
    • 目录
      • 一.什么是单例?
      • 二.有几种?
      • 三.应用场景
      • 四.注意的地方

    一.什么是单例?

    单例模式 保证一个类在内存中只有一个实例(对象),并提供一个访问它的全局访问点。

    通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的办法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法 —— 大话设计模式--第21章

    单例Boy.jpg

    二.单例有几种?

    具体区分为下面几种:

    • 饿汉式
    • 懒(饱)汉式
      • 线程不安全(单线程下操作的)
      • 线程安全
        • 方法加锁式
        • 双重判断加锁式DCL(Double-Check Locking)--- 方法加锁式加强版
    • 静态嵌套类式(推荐使用)
    • 枚举式(推荐使用)
    • 使用容器单例

    不和你多bb ,上代码

    b.jpg

    1.饿汉式

    public class Signleton{
        //对于一个final变量。  
        // 如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;  
        // 如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
       private final static Signleton instance = new Signleton();
       
       private Signleton(){
           
       }
       
       public static  Signleton getInstance(){
            return instance;
       }
    }
    

    解释: 拿时间换空间,因为实例对象在类加载过程中就会被创建,在getInstance()方法中只是直接返回对象引用。因为这种创建实例对象方式比较‘急’,所以称为饿汉式。

    优点:快很准,无需关心线程安全问题。

    缺点
      1.无论对象会不会被使用,在类加载的时候就创建对象了,这样会降低内存的使用率。
       2.如果在一个大环境下使用了过多的饿汉单例,则会生产出过多的实例对象,无论你是否要使用他们

    饿.jpg

    2.懒(饱)汉式

    (1)线程不安全(单线程下操作的)

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

    解释: Singleton的静态属性instance中,只有instance为null的时候才创建一个实例,构造函数私有,确保每次都只创建一个,避免重复创建。之所以被称为“懒汉”,因为它很懒,不急着生产实例,在需要的时候才会生产。

    优点:延迟加载

    缺点:只在单线程的情况下正常运行,在多线程的情况下,就会出问题。例如:当两个线程同时运行到判断instance是否为空的if语句,并且instance确实没有创建好时,那么两个线程都会创建一个实例。

    (2)线程安全

    方法加锁式
    public class Signleton{
      private static Signleton instance = null;
      
      private Signleton(){
      
      }
      //给方法加锁 若有ABCD线程使用 A线程先进入 BCD线程都需要等待A线程执行完毕释放锁才能获得锁执行该方法
      //这样效率较低 
      public syschronized static Signleton getInstance(){
         if(instance==null){
            instance = new Signleton();
         }
         return instance;
      }
    }
    

    解释:给方法添加[synchronized],使之成为同步函数。两个线程同时想创建实例,由于在一个时刻只有一个线程能得到同步锁,当第一个线程加上锁以后,第二个线程只能等待。第一个线程发现实例没有创建,创建之。第一个线程释放同步锁,第二个线程才可以加上同步锁,执行下面的代码。由于第一个线程已经创建了实例,所以第二个线程不需要创建实例。保证在多线程的环境下也只有一个实例。

    优点: 保证了线程安全。延时加载,用的时候才会生产对象。

    缺点: 需要保证同步,付出效率的代价。加锁是很耗时的。。。

    双重判断加锁式DCL
    public class Signleton {
      private static Signleton instance = null;
      
      private Signleton(){
      
      }
    
      //假设有ABCD 个线程 使用这个方法
      public static Signleton getInstance(){
      //BCD都进入了这个方法
        if(instance==null){
          //而A线程已经给第二个的判断加锁了 
          syschronized(Signleton.class){
             //这时A挂起,对象instance还没创建 ,故BCD都进入了第一个判断里面,并排队等待A释放锁
             //A唤醒继续执行并创建了instance对象,执行完毕释放锁。
             //此时到B线程进入到第二个判断并加锁,但由于B进入第二个判断时instance 不为null了  故需要再判断多一次  不然会再创建一次实例
              if(instance==null){
                 instance = new Signleton();
              }
           
          } 
        }
        return instance;
       }
    }
    

    解释:方法加锁式的优化。只有当instance为null时,需要获取同步锁,创建一次实例。当实例被创建,则无需试图加锁。。

    优点: 保证了线程安全。延时加载,用的时候才会生产对象。进行双重判断,当已经创建过实例对象后就无需加锁,提高效率。

    缺点: 编写复杂、难记忆。虽然是优化加锁式,但加锁始终会耗时。

    3.静态嵌套类式(推荐使用)
    public class Signleton{
       private Signleton{
       }
      
       //静态嵌套类  这里给个链接 区分静态嵌套类和内部类[静态嵌套类和内部类](http://blog.csdn.net/iispring/article/details/46490319)
       private static class  SignletonHolder{
          public static final Signleton instance = new Signleton();
      }
      
      public static Signleton getInstance(){
       return SignletonHolder.instance;
    ins't
      }
      
    }
    

    解释: 定义一个私有的静态嵌套类,在第一次用这个嵌套类时,会创建一个实例。而类型为SingletonHolder的类,只有在Singleton.getInstance()中调用,由于私有的属性,他人无法使用SingleHolder,不调用Singleton.getInstance()就不会创建实例。

    优点: 保证了线程安全。无需加锁,延迟加载,第一次调用Singleton.getInstance才创建实例。推荐使用。

    缺点: 无。

    4.枚举式(推荐使用)

    为了方便理解枚举式 这边简单介绍一下枚举(在jdk1.5后)

    //枚举Type
    public enum Type{
       A,B,C;
      private String type;
      Type(type){
         this.type = type;
     }
     public String getType(){
        return type;
      }
    }
    //可认为等于下面的
    public class Type{
      public static final Type A=new Type(A);
      public static final Type B=new Type(B);
      public static final Type C=new Type(C);
    ins't
    }
    

    所以Type.A.getType()为A.
    推荐去了解一下
    Java学习整理系列之Java枚举类型的使用
    Java学习整理系列之Java枚举类型的原理

    好了 开始介绍枚举式了 看代码

    public class Signleton{
    
    public static Signleton getInstance(){
       return SignletonEnum.INSTANCE.getInstance();
    }
    
    public enum SignletonEnum{
       INSTANCE;
       
       private Signleton instance;
       
       //由于JVM只会初始化一次枚举实例,所以instance无需加static 
       private SignletonEnum(){
            instance = new Signleton();
       }
       
       public getInstance(){
           return instance;   
       }
    }
    
    }
    

    解释: 定义内部的枚举,由于类加载时JVM只会初始化一次枚举实例,所以在构造函数中创建Signgleton对象并保证了这个对象实例唯一。
    通过调用枚举INSTANCE方法getInstance (SignletonEnum.INSTANCE.getInstance())获取实例对象。

    优点: 枚举提供了序列化机制--例如在我们要通过网络传输一个数据库连接的句柄,会提供很多帮助。

    单元素的枚举类型已经成为实现Singleton的最佳方法。Effective Java

    5.使用容器单例
    public class Singleton { 
       //用Map保存该单例
        private static Map<String, Object> objMap = new HashMap<>(); 
    
        private Singleton() { 
    
        } 
    
        public static void putObject(String key, String instance){ 
            if(!objMap.containsKey(key)){ 
                objMap.put(key, instance); 
            } 
        } 
    
        public static Object getObject(String key){ 
            return objMap.get(key); 
        } 
    }
    

    解释: 在程序开始的时候将单例类型注入到一个容器之中 ,在使用的时候再根据 key 值获取对应的实例,这种方式可以使我们很方便的管理很多单例对象,也对用户隐藏了具体实现类,降低了耦合度。

    缺点: 会造成内存泄漏。(所以我们一般在生命周期销毁的时候也要去销毁它) 。

    三.应用场景

    一般创建一个对象需要消耗过多的资源,如:访问I0和数据库等资源或者有很多个地方都用到了这个实例。

    四.注意的地方

    双重判断加锁式DCL :这种写法也并不是保证完全100%的可靠,由于 java 编译器允许执行无序,并且 jdk1.5之前的jvm ( java 内存模型)中的 Cache,寄存器到主内存的回写顺序规定,第二个和第三个执行是无法保证按顺序执行的,也就是说有可能1-2-3也有可能是1-3-2; 这时假如有 A 和 B 两条线程, A线程执行到3的步骤,但是未执行2,这时候 B 线程来了抢了权限,直接取走 instance 这时候就有可能报错。

    简单总结就是说jdk1.5之前会造成两个问题:

    1、线程间共享变量不可见性;

    2、无序性(执行顺序无法保证);

    当然这个bug已经修复了,SUN官方调整了JVM,具体了Volatile关键字,因此在jdk1.5之前只需要写成这样既可, private Volatitle static Singleton instance; 这样就可以保证每次都是从主内存中取,当然这样写或多或少的回影响性能,但是为了安全起见,这点性能牺牲还是值得。

    相关文章

      网友评论

          本文标题:Java单例模式详解

          本文链接:https://www.haomeiwen.com/subject/pqwqpxtx.html