一個單例引發的學案

歡迎關注微信公衆號:程序員小圈圈
原文首發於: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的特殊規則就是:

  • read、load、use動作必須連續出現。
  • assign、store、write動作必須連續出現。

所以,使用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)的區別是什麼

筆者就直接說結論了:

  • 1.synchronized(.class)只要是訪問這個類的方法,就會同步,不管用這個類創建了幾個對象!一般單列模式常用
  • 2.synchronized(this) 指的是對象本身同步,一般在定義對象的方法時可以用,當只有訪問同一對象,纔會同步,和synchronized(Object x)功能類似。
    1. synchronized(Object x),通過對象同步,注意必須是同一個對象一般在多線程中訪問同一個對象時,在run方法中用到。

這裏推薦一篇寫的挺細緻的文章:
# synchronized(this)、synchronized(class)與synchronized(Object)的區別

有興趣的小夥伴可以去看下,寫的挺詳細的。

那在之後如果你去面試,面試官再讓你手寫個單例模式,然後介紹下里面的知識點,是不是很愜意,簡直就是就怕你不問我,不然我怎麼展示這麼騷氣的一面~

,博主的微信公衆號
程序員小圈圈’開始持續更新了喲~~
識別二維碼或者直接搜索名字 ‘程序員小圈圈’ 即可關注本公衆號喲~~
不只是有技術喲~~
還有很多的學習娛樂資源免費下載哦~~
還可以學下教育知識以及消遣娛樂喲~~
求關注喲~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章