设计模式(6)-单例模式


更多关于设计模式的文章请点击设计模式之禅(0)-目录页


单件模式是面向对象设计模式中常用的一种设计模式,它主要的作用是使得某个对象在全局程序中只有唯一的一个实例,并且在全局中只有一个创建实例的访问点。这种模式通常采用在线程池、连接池、单一记录器等的使用和实现中。

一、单件模式的实现

单件模式Singleton Pattern)在23种设计模式中属于比较简单的那一类,隶属于创建型模式。它的主要思想是在全局中只有一个获得实例的创建点,并且将构造函数私有,不能由外部程序随意地创建它的实例。
在这里插入图片描述

1.1 饿汉式

饿汗式的单例模式,顾名思义即在程序启动的时候将单例类直接初始化,这种最简单的一种单例模式实现:

  • SingletonObject0
/**
 * @Description: 单件模式构造(饿汗式)
 * @CreateDate: Created in 2018/11/29 10:54
 * @Author: <a href="https://blog.csdn.net/pbrlovejava">arong</a>
 */
public class SingletonObject0 {
    private static SingletonObject0 singletonObject = new SingletonObject0();
    //构造函数私有,外部类无法直接创建该对象
    private SingletonObject0(){
    }

    //全局唯一的获得实例点
    public static SingletonObject0 getInstance() {
        return  singletonObject;
    }
}

饿汗式实现有一个缺点,就是程序启动时即在全局创建了唯一的一个单例对象,但是若程序长时间都没有用过这个对象,会导致其过早初始化,浪费内存的浪费

1.2 懒汉式

懒汉式的实现指的是在程序中有使用到单例对象时才进行初始化。如果以时空角度来看,懒汉式是典型的使用时间换取空间,懒加载会需要时机判断,但是无需一开始初始化,节约内存分配;而饿汉式是典型的空间换取时间,在程序一开始初始化占用了内存,但是却无需进行时机判断。

  • SingletonObject1
/**
 * @Description: 单件模式构造(懒汉式)
 * @CreateDate: Created in 2018/11/29 10:54
 * @Author: <a href="https://blog.csdn.net/pbrlovejava">arong</a>
 */
public class SingletonObject1 {
    private static SingletonObject1 singletonObject;
    //构造函数私有,外部类无法直接创建该对象
    private SingletonObject1(){
    }

    //全局唯一的获得实例点
    public static SingletonObject1 getInstance(){
        //判断对象是否已经被创建,没有被创建则新建对象
        if( singletonObject == null){
            singletonObject = new SingletonObject1();
        }
        return singletonObject;
    }
}

通过私有化构造器并且提供唯一的实例获得点,就可以实现SingletonObject在全局中只存在唯一的一个实例了。

     @Test
        public void fun1() {
            SingletonObject1 singletonObject1 = SingletonObject1.getInstance();
            SingletonObject1 singletonObject2 = SingletonObject1.getInstance();
            System.out.println(singletonObject1+"\n"+singletonObject2);
        }

在这里插入图片描述

二、在并发下单件模式的改进

2.1 使用闭锁测试单件模式的正确性

上面写的单件模式代码似乎已经可以在全局中只产生一个实例了,可是如果在多线程模式下,以上代码还能实现单件吗?我使用了闭锁来测试并发时是否仍然能获得唯一的单件类:【关于闭锁的使用可以阅读我的一篇文章:Java并发编程(9)-使用闭锁测试并发线程安全性

 @Test
    public void concurrentTest(){
        //开始闭锁
        CountDownLatch startLatch = new CountDownLatch(1);
        for (int i = 0; i < 5; i++) {
            new Thread() {
              public void run(){
                  try {
                      //线程运行至闭锁处等待
                      startLatch.await();
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  //5个线程并发执行任务
                 new MyTask().run();
                }

            }.start();
        }

        //线程全部集结在闭锁下,开锁
        startLatch.countDown();
    }

    //任务类
    public class MyTask implements Runnable{

        @Override
        public void run() {
            //获得单例类
            SingletonObject1 instance = SingletonObject1.getInstance();
            //打印地址
            System.out.println(instance);
        }
    }

在这个测试中,我只让5个线程去并发地获得单件类实例,可是却出现了问题,单件的实例并不唯一了:
在这里插入图片描述

2.2 同步式

要使得单件模式在并发条件下仍然正确,最简单的方式就是使用同步锁来限制同时读取实例的线程数,这个做法很安全,但是会影响速度:

  • SingletonObject3
/**
 * @Description: 单件模式构造(同步式)
 * @CreateDate: Created in 2018/11/29 10:54
 * @Author: <a href="https://blog.csdn.net/pbrlovejava">arong</a>
 */
public class SingletonObject3 {
    private static SingletonObject3 singletonObject;
    //构造函数私有,外部类无法直接创建该对象
    private SingletonObject3(){
    }

	 //全局唯一的获得实例点
    public static synchronized SingletonObject3 getInstance(){
        //判断对象是否已经被创建,没有被创建则新建对象
        if( singletonObject == null){
            singletonObject = new SingletonObject1();
        }
        return  singletonObject;
    }
2.3 二重检查加锁

对于同步锁而言,还可以使用更加轻量的volatile关键字来实现线程之间对实例状态的检查加锁:

public class SingletonObjec4 {
   private volatile static SingletonObject4 singletonObject;

   //构造方法私有化,只能在本类中调用构造方法
   private SingletonObject(){
   }
   /**
    *@description 获得全局唯一的实例(二重检查加锁)
    *@author arong
    *@date 2018/11/27
    *@param:
    *@return com.iteason.singletonPattern.SingletonObject
    */
   public static SingletonObject4 getInstance(){
       //一重检查
       if(singletonObject == null) {
     	  	//加锁
           synchronized (singletonObject){
               //二重检查
               if (singletonObject == null) {
            	   singletonObject = new SingletonObject();
               }
           }
       }
           return singletonObject;
   }
}
2.4. 典型错误-缺失volatile

在双重检查加锁的单例模式中,如果单例对象不使用volatile进行修饰的话,因为创建对象的重排序的原因,将初始化和分配内存这两个步骤颠倒,那么会导致创建对象时的误判,导致返回一个空对象。

一个错误的双重检查加锁Demo

public class Singleton {
    private Singleton instance = null;
    private Singleton() {}
    
    public Singleton getInstance() {
        // 先判断对象是否已经初始化
        if (instance == null) {
            synchronized (instance) {
                // 确定对象没有初始化
                if (instance == null) {
                    return new Singleton();
                }
            }
        }
        return object;
    }
}

上述代码发生错误的原因在于return new Singleton()这行代码,这行代码并不是一个原子性的的操作,它可以分为以下三个阶段:

1.分配对象内存
2.初始化对象
3.将对象引用到内存空间

其中,在将对象引用到内存空间时,instance就不等于null了。

在单线程情况下,2和3指令发生重排序,但是由于as-if-serial语义,其执行结果也是不变的:
在这里插入图片描述
在多线程情况下,有可能会发生以下的执行顺序,导致程序执行结果出错:
A线程在执行到第3步的时候即设置instance指向内存空间,此时B线程判断其不为空,返回instance,但此时instance并未进行初始化,所以出现了错误,返回了一个空对象。
在这里插入图片描述

解决该问题的方法是在instance前加上volatile修饰,这样就能禁止重排序。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

程序员不鸣

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

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

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

打赏作者

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

抵扣说明:

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

余额充值