代码实验02:设计模式-单例模式

目录

单例模式的定义

单例模式的分类

单例模式的Java实现

多线程环境下的懒汉式单例

单例模式的应用

Spring的单例模式底层实现


本文脑图​

前面讲了设计模式的介绍,传送门:设计模式概述

单例模式是创建型设计模式的一种,也是最简单的一种设计模式。

单例模式的定义

Ensure a class has only one instance,and provide a global point of access toit.

上面是单例模式的英文阐述,意思是确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。单例模式的主要作用是确保一个类只有一个实例存在。

单例模式的分类

在Java语言的环境下,单例模式可以分为两种。

  • 懒汉式单例

  • 饿汉式单例

饿汉式单例是指类一加载就实例化对象,就好比一个老汉吃胡萝卜都懒得煮熟,直接就生吃,懒汉式单例因此得名。

相反,懒汉式单例是指类在加载时不实例化对象,只有等到第一次用的时候才去实例化。

饿汉式单例主要是考虑避免造成资源浪费,因为不确定这个单例对象是否会被用到,所以等到要用了我才去创建,这样就避免了资源浪费,要知道创建对象是非常耗费资源的,特别是大对象。这就好比线上商店卖货,有订单了,我才生产商品。

单例模式的Java实现

网上有些人朋友认为设计模式只有在写Java时才有用,实际上这种观点有失偏颇,设计模式强调的是复用性理念,把很多问题的共性特征抽取出来形成一套可复用的解决方案就变成了设计模式。当然目前狭义的软件设计模式指的还是GoF中提出的23种模式。

饿汉式单例的代码实现

    public class Singleton {
        // 实例化单例对象
        private static Singleton singleton = new Singleton();
        // 私有化构造方法,防止被外部创建对象
        private Singleton() {}
        // 对外提供方法获取该对象
        public static Singleton getSingleton() {
            return singleton;
        }
    }    

懒汉式单例的代码实现

    public class Singleton {
        // 先定义单例对象为null,不实例化
        private static Singleton singleton = null;
        // 私有化构造方法,防止被外部创建对象
        private Singleton() {}
        // 对外提供方法获取该对象
        public static Singleton getSingleton() {
            // 当调用该方法时先判断对象是否为null,如果为null才实例化
            if (singleton == null) {
                singleton = new Singleton();
            }
            return singleton;
        }
    }

稍微学过多线程的同学就会发现饿汉式单例在多线程环境下有问题,可能会创建出多个对象来,就违背了单例模式的定义了,我们来实验一下:

public class MainTest {
    public static void main(String[] args) {
// 创建50个线程获取单例对象,我们来看多次运行后单例对象的内存地址是否一样,

如果出现不一样则说明创建了多个对象。

for (int i = 0; i < 50; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Singleton singleton = Singleton.getSingleton();
// 打印该单例对象的内存引用地址
System.out.println(singleton);
}
}).start();
}
}
}

多次运行后结果如下:

可以看到明细出来了三个不同的对象了。

那么多线程环境下的单例模式如何确保对象的唯一性呢?

多线程环境下的懒汉式单例

懒汉式单例为什么会存在线程安全问题?

这是因为多个线程去调用getInstance方法来获取Singleton的实例,那么就有可能发生这样一种情况,当第一个线程在执行if(singleton == null)时,此时singleton是为null的进入语句。在还没有执行singleton = new Singleton()时(此时instance是为null的),巧了,这时候第二个线程也进入了if(singleton == null)这个语句,因为之前进入这个语句的线程中还没有执行singleton = new Singleton(),所以它也会执行singleton = new Singleton()来实例化Singleton对象,因为第二个线程也进入了if语句所以它会实例化Singleton对象。

这样就导致了实例化了两个Singleton对象。

如何解决呢?

分三步:

  1. 加同步锁

  2. 双重检查判断,也就我们说的double check

  3. 使用volatile关键字禁止指令重排序

最完整的懒汉式单例模式

public class Singleton {
    // 先定义单例对象为null,不实例化
    // 第二层锁,volatile关键字禁止指令重排
    private static volatile Singleton singleton = null;
​
    // 私有化构造方法,防止被外部创建对象
    private Singleton() {}
​
    // 对外提供方法获取该对象
    public static Singleton getSingleton() {
        // 当调用该方法时先判断对象是否为null,如果为null才实例化
        // 第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
        if (singleton == null) {
            // 第一层锁,保证只有一个线程进入
            synchronized (Singleton.class) {
                // 第二层检查
                if(singleton==null) {
                    //volatile关键字作用为禁止指令重排,保证返回Singleton对象一定在创建对象后
                    singleton = new Singleton();
                    //singleton = new Singleton语句为非原子性,实际上会执行以下内容:
                    //(1)在堆上开辟空间;(2)属性初始化;(3)引用指向对象
                    //假设以上三个内容为三条单独指令,因指令重排可能会导致执行顺序为1->3->2(正常为1->2->3),当单例模式中存在普通变量需要在构造方法中进行初始化操作时,单线程情况下,顺序重排没有影响;但在多线程情况下,假如线程1执行singleton=new Singleton()语句时先1再3,由于系统调度线程2的原因没来得及执行步骤2,但此时已有引用指向对象也就是singleton!=null,故线程2在第一次检查时不满足条件直接返回singleton,此时singleton为null(即str值为null)
                    //volatile关键字可保证singleton=new Singleton()语句执行顺序为123,因其为非原子性依旧可能存在系统调度问题(即执行步骤时被打断),但能确保的是只要singleton!=0,就表明一定执行了属性初始化操作;而若在步骤3之前被打断,此时singleton依旧为null,其他线程可进入第一层检查向下执行创建对象
                }
            }
        }
        return singleton;
    }
}

单例模式的应用

1. 单例模式的优点

■ 由于单例模式在内存中只有一个实例,减少了内存的开支,特别是一个对象需要频繁地创建、销毁,而且创建或销毁的性能又无法优化时,单例模式的优势就非常明显。

■ 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多资源时,如读取配置、产生其他依赖对象时,则可以通过在启用时直接产生一个单例对象,然后用永久驻留内存的方式来解决。

■ 单例模式可以避免对资源的多重占用,例如,一个写文件动作,由于只有一个实例存在于内存中,避免了对同一个资源文件的同时操作。

■ 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有数据表的映射处理。

2. 单例模式的缺点

■ 单例模式无法创建子类,扩展困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。

■ 单例模式对测试不利。在并行开发环境中,如果采用单例模式的类没有完成,是不能进行测试的;单例模式的类通常不会实现接口,这也妨碍了使用mock的方式虚拟一个对象。

■ 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要用单例模式取决于环境,单例模式把“要单例”和业务逻辑融合在一起。

3. 单例模式的使用场景

■ 生成唯一序列号

■ 需要一个共享访问点或共享数据:如web页面的计数器

■ Spring框架中,每个Bean 默认就是单例的

■ Java 基础类库中的 java.lang.Runtime 类也采用了单例模式

Spring的单例模式底层实现

前面也提到了单例模式的缺点,单例模式无法创建子类,也就是不能被继承,所以就无法扩展。

为了克服单例模式不能被继承的缺点,我们可以使用另外一种特殊化的单例模式,它被称为单例注册表。

import java.util.HashMap;
 public class RegSingleton {
    static private HashMap<String, Object> registry = new HashMap();
    //静态块,在类被加载时自动执行
    static {
        RegSingleton rs = new RegSingleton();
        registry.put(rs.getClass().getName(), rs);
    }
    //受保护的默认构造函数,如果为继承关系,则可以调用,克服了单例类不能为继承的缺点  
    protected RegSingleton() {
    }
    //静态工厂方法,返回此类的唯一实例  
    public static RegSingleton getInstance(String name) {
        if (name == null) {
            name = "RegSingleton";
        }
        // 这里仅作为演示,不考虑线程安全问题
        if (registry.get(name) == null) {
            try {
                registry.put(name, Class.forName(name).newInstance());
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return (RegSingleton) registry.get(name);
    }
 }

那么Spring对单例的底层实现,到底是饿汉式单例还是懒汉式单例呢?

都不是。Spring框架对单例的支持是采用单例注册表的方式进行实现的,源码如下:

public abstract class AbstractBeanFactory implements ConfigurableBeanFactory{  
       /** 
        * 充当了Bean实例的缓存,实现方式和单例注册表相同 
        */  
       private final Map singletonCache=new HashMap();  
       public Object getBean(String name)throws BeansException{  
           return getBean(name,null,null);  
       }  
    ...  
       public Object getBean(String name,Class requiredType,Object[] args)throws BeansException{  
          //对传入的Bean name稍做处理,防止传入的Bean name名有非法字符(或则做转码)  
          String beanName=transformedBeanName(name);  
          Object bean=null;  
          //手工检测单例注册表  
          Object sharedInstance=null;  
          //使用了代码锁定同步块,原理和同步方法相似,但是这种写法效率更高  
          synchronized(this.singletonCache){  
             sharedInstance=this.singletonCache.get(beanName);  
           }  
          if(sharedInstance!=null){  
             ...  
             //返回合适的缓存Bean实例  
             bean=getObjectForSharedInstance(name,sharedInstance);  
          }else{  
            ...  
            //取得Bean的定义  
            RootBeanDefinition mergedBeanDefinition=getMergedBeanDefinition(beanName,false);  
             ...  
            //根据Bean定义判断,此判断依据通常来自于组件配置文件的单例属性开关  
            //<bean id="date" class="java.util.Date" scope="singleton"/>  
            //如果是单例,做如下处理  
            if(mergedBeanDefinition.isSingleton()){  
               synchronized(this.singletonCache){  
                //再次检测单例注册表  
                 sharedInstance=this.singletonCache.get(beanName);  
                 if(sharedInstance==null){  
                    ...  
                   try {  
                      //真正创建Bean实例  
                      sharedInstance=createBean(beanName,mergedBeanDefinition,args);  
                      //向单例注册表注册Bean实例  
                       addSingleton(beanName,sharedInstance);  
                   }catch (Exception ex) {  
                      ...  
                   }finally{  
                      ...  
                  }  
                 }  
               }  
              bean=getObjectForSharedInstance(name,sharedInstance);  
            }  
           //如果是非单例,即prototpye,每次都要新创建一个Bean实例  
           //<bean id="date" class="java.util.Date" scope="prototype"/>  
           else{  
              bean=createBean(beanName,mergedBeanDefinition,args);  
           }  
    }  
    ...  
       return bean;  
    }  
 }

我是Seven,一个不懈努力的程序猿,希望本文能对你有所裨益

Web页面是如何呈现的?

如何看懂Apache Log4j 远程代码执行漏洞原理?

细数Java8-14的那些经典特性,语言的车轮正在滚滚向前...

要使用消息队列,以下这些你都要知道...

GC如何判断一个对象是否为垃圾?深度剖析三色标记算法原理

设计模式概述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Seven的代码实验室

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值