一个单例引发的学案

/ ARC / 没有评论 / 50浏览
欢迎关注微信公众号:程序员小圈圈
原文首发于:www.zhangruibin.com
本文出自于:RebornChang的博客
转载请标明出处^_^

一个单例引发的学案

很多时候我们都会碰到单例模式,笔者之前也写过单例模式的几种写法几种单例模式的写法,那么会写单例模式就真的会用单例模式了吗?双重加锁的volatile有什么用?在生产中我们怎样来科学的用单例模式?那么就看下文吧。

首先老生常谈的说下,什么是单例模式。

什么是单例模式

单例模式(Singleton),也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

那么知道了什么是单例模式,那在生产中怎么用呢?

什么时候适合用单例模式

  1. Windows的Task Manager(任务管理器)就是很典型的单例模式,想想看,是不是呢,你能打开两个windows task manager吗?

  2. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

  3. 网站的计数器,一般也是采用单例模式实现,否则难以同步。

  4. 应用程序的日志应用,一般都可用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。

  5. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。

  6. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为可用单例模式来维护,就可以大大降低这种损耗。

  7. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。

  8. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。

  9. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例。

总结起来就是两点:

(1)资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如日志文件,应用配置。

(2)控制资源的情况下,方便资源之间的互相通信。如线程池等。

那么我们就了然了,我们可以将全局配置使用单例实现下,比如下面的例子,我们使用典型单例模式,来保证全局唯一的省份配置信息。

生产中使用单例模式

知道了上面的单例模式的使用场景,那么我们模拟下,在生产中使用单例模式。

任务,系统中需要配置全局信息,省份及省份编码。

那我们可以将配置信息放在数据库中,缓存中,枚举类中等等。

那我们这里选择一种,将信息放到配置文件中,然后由单例类去读取文件信息,其他功能全局调用。

那接下来就说下怎么做:

首先我们要确定下要使用dom4j来解析xml文件,方便快捷,那这里要引入dom4j的依赖,为了减少getset的方法,这里也引入lombok的依赖,不了解lombok的小伙伴可以bing下。

<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>
<!--lombok--> <dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <version>1.18.2</version>
   <scope>provided</scope>
</dependency>

然后准备xml文件Province.xml,放到resource/xml 目录下:

<?xml version="1.0" encoding="utf-8"?> <provinceNameAndCode>
    <code name="province" description="省份">
        <parameter  id="01" name="北京"/>
        <parameter  id="02" name="天津"/>
        <parameter  id="03" name="河北省"/>
        <parameter  id="04" name="山西省"/>
        <parameter  id="05" name="内蒙古"/>
        <parameter  id="06" name="辽宁省"/>
        <parameter  id="07" name="吉林省"/>
        <parameter  id="08" name="黑龙江"/>
        <parameter  id="09" name="上海"/>
        <parameter  id="10" name="江苏省"/>
        <parameter  id="11" name="浙江省"/>
        <parameter  id="12" name="安徽省"/>
        <parameter  id="13" name="福建省"/>
        <parameter  id="14" name="江西省"/>
        <parameter  id="15" name="山东省"/>
        <parameter  id="16" name="河南省"/>
        <parameter  id="17" name="湖北省"/>
        <parameter  id="18" name="湖南省"/>
        <parameter  id="19" name="广东省"/>
        <parameter  id="20" name="广西省"/>
        <parameter  id="21" name="海南省"/>
        <parameter  id="22" name="重庆"/>
        <parameter  id="23" name="四川省"/>
        <parameter  id="24" name="贵州省"/>
        <parameter  id="25" name="云南省"/>
        <parameter  id="26" name="西藏"/>
        <parameter  id="27" name="陕西省"/>
        <parameter  id="28" name="甘肃省"/>
        <parameter  id="29" name="青海省"/>
        <parameter  id="30" name="宁夏省"/>
        <parameter  id="31" name="新疆"/>
        <parameter  id="32" name="澳门"/>
        <parameter  id="33" name="香港"/>
        <parameter  id="34" name="台湾"/>
    </code>
</provinceNameAndCode>

然后就是我们使用双重加锁来实现的单例类,读取xml文件信息放到集合里面:

package com.zhrb.testDemo.singletonDemoUse;

import com.zhrb.entity.Province;
import org.dom4j.*;
import org.dom4j.io.*;

import java.io.File;

import java.util.*;

/**
 * @ClassName SelectProvinceUtil
 * @Description TODO
  * @Author zhrb
 * @Date 2019/9/24 10:22
 * @Version
  */ 
  public class SelectProvinceUtil {
    //1、私有静态变量
  private static volatile SelectProvinceUtil selectProvinceUtil;
    private Map<String,List<Province>> selectMap;//业务对象
  private Document document;

    //2、私有构造方法
  private SelectProvinceUtil(String filePath){
        List<Province> listProvince = new ArrayList<Province>();
        selectMap = new HashMap<String, List<Province>>();
        try {
            //载入文件
  SAXReader saxReader = new SAXReader();
            document = saxReader.read(new File(filePath));
            //获取根节点
  Element root = document.getRootElement();
            Iterator<Element> eleBrands = root.elementIterator();
            //读取节点
  while(eleBrands.hasNext()){
                Element brand = (Element)eleBrands.next();
                String key = brand.attributeValue("name");
                String description = brand.attributeValue("description");
                if(key.equals("province")){
                    Iterator<Element> eleTypes = brand.elementIterator();
                    while(eleTypes.hasNext()){
                        Element element = (Element)eleTypes.next();
                        Province province = new Province(element.attributeValue("id"), element.attributeValue("name"));
                        listProvince.add(province);
                    }
                    //把取到的值放进Maps
  key = key +","+ description;
                    selectMap.put(key, listProvince);
                }
            }
        } catch (DocumentException e) {
            e.printStackTrace();
        }
    }

    //3、公有静态方法,获取实例对象
  public static synchronized SelectProvinceUtil getInstance(String filePath){
        if(null == selectProvinceUtil){
            selectProvinceUtil = new SelectProvinceUtil(filePath);
        }
        return selectProvinceUtil;
    }

    //4、公有业务方法
  public Map<String, List<Province>> getSelectMap(){
        return selectMap;
    }
}

接下来就是测试类:

package com.zhrb.testDemo.singletonDemoUse;

import com.zhrb.entity.Province;

import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @ClassName TestSelectProvinceUtil
 * @Description TODO
  * @Author zhrb
 * @Date 2019/9/24 10:23
 * @Version
  */ 
  public class TestSelectProvinceUtil {
    public static void main(String[] args) {
        //xml文件绝对路径
  String filePath = new File("src/main/resources/xml/Province").getAbsolutePath();

        Map<String, List<Province>> map = SelectProvinceUtil.getInstance(filePath).getSelectMap();

        //遍历输出
  Set<String> keys = map.keySet();
        String name = keys.toArray()[0].toString().split(",")[0];
        String description = keys.toArray()[0].toString().split(",")[1];
        for(String key : keys){
            System.out.println("节点名称:"+ name + "\t" + "节点描述:" + description);
            List<Province> list = (List<Province>)map.get(key);
            for(Province p:list){
                System.out.println("\t"+p.getId()+"\t"+p.getName());
            }
        }
    }
}

测试打印结果如下:

节点名称:province	节点描述:省份
	01	北京
	02	天津
	03	河北省
	04	山西省
	05	内蒙古
	06	辽宁省
	07	吉林省
	08	黑龙江
	09	上海
	10	江苏省
	11	浙江省
	12	安徽省
	13	福建省
	14	江西省
	15	山东省
	16	河南省
	17	湖北省
	18	湖南省
	19	广东省
	20	广西省
	21	海南省
	22	重庆
	23	四川省
	24	贵州省
	25	云南省
	26	西藏
	27	陕西省
	28	甘肃省
	29	青海省
	30	宁夏省
	31	新疆
	32	澳门
	33	香港
	34	台湾

Process finished with exit code 0

简单吧,剔除上面的业务代码,其实核心的就是双重检验单例模式,如下:

package com.zhrb.testDemo;

import com.zhrb.entity.Student;

/**
 * @ClassName DoubleCheck
 * @Description TODO
  * @Author zhrb
 * @Date 2019/9/23 16:20
 * @Version
  */ 
  public class DoubleCheck {
    //1、私有静态变量
  private static volatile DoubleCheck doubleCheck = null;
    //业务对象
  private Student student;
    //2、私有构造方法
  private DoubleCheck(){
        student = new Student();
    }
    //3、公有静态方法,获取实例对象
 //加上同步锁 synchronized,线程安全  public static synchronized DoubleCheck getDoubleCheck(DoubleCheck doubleCheck){
        if (doubleCheck == null){
            synchronized (DoubleCheck.class){
                if (doubleCheck == null){
                    doubleCheck = new DoubleCheck();
                }
            }
        }
        return doubleCheck;
    }
    //4、公有业务方法
  public String getStudentName(){
        return student.getName();
    }
}

敲黑板,上面的单例模式有知识点,是什么呢?

1.volatile关键字有啥用?

2.synchronized (Object.class)和synchronized (this)的区别是什么?

那么我们一一来说。

首先说volatile关键字作用。

volatile关键字作用

简单的说是内存可见性(Memory Visibility):所有线程都能看到共享内存的最新状态。

我们知道,对于Java变量的读写操作,Java通过几种原子操作完成工作内存主内存的交互:

  1. lock:作用于主内存,把变量标识为线程独占状态。
  2. unlock:作用于主内存,解除独占状态。
  3. read:作用主内存,把一个变量的值从主内存传输到线程的工作内存
  4. load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中
  5. use:作用工作内存,把工作内存当中的一个变量值传给执行引擎
  6. assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
  7. store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
  8. write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。

那如开始时所说,volatile可以保持内存可见性,那volatile如何保持内存可见性?

volatile的特殊规则就是:

所以,使用volatile变量能够保证:

也就是说,volatile关键字修饰的变量看到的随时是自己的最新值。线程1中对变量v的最新修改,对线程2是可见的。

笼统及总结性的来说:

使用volatile修饰的对象,当每次修改对象,都会讲本次改变刷新到主内存中,而当其他线程去读取这个对象的时候,都是从主内存中去拿值。

所以经常说volatile是多线程并发保证数据准确性的有效手段。

但是 volatile只有内存可见这一点特殊的吗?

NO,还包含指令重排,这就是理论上的东西了,笔者这里就不赘述了,可以参考这两位大佬的文章,写的还是挺好的 : https://www.cnblogs.com/shan1393/p/8999683.htm http://swiftlet.net/archives/3321

那说完了volatile之后,再说另一个知识点。

synchronized (Object.class)和synchronized (this)的区别是什么

笔者就直接说结论了:

这里推荐一篇写的挺细致的文章: # synchronized(this)、synchronized(class)与synchronized(Object)的区别

有兴趣的小伙伴可以去看下,写的挺详细的。

那在之后如果你去面试,面试官再让你手写个单例模式,然后介绍下里面的知识点,是不是很惬意,简直就是就怕你不问我,不然我怎么展示这么骚气的一面~

,博主的微信公众号
程序员小圈圈’开始持续更新了哟~~
识别二维码或者直接搜索名字 ‘程序员小圈圈’ 即可关注本公众号哟~~
不只是有技术哟~~ 还有很多的学习娱乐资源免费下载哦~~ 还可以学下教育知识以及消遣娱乐哟~~ 求关注哟~~ '