深入理解JVM(③)虛擬機性能監控、故障處理工具

前言

JDK的bin目錄中有一系列的小工具,除了java.exe、javac.exe這兩個編譯和運行Java程序外,還有打包、部署、簽名、調試、監控、運維等各種場景都會用到這些小工具。

這些工具根據軟件可用性和授權的不同,可以把它們劃分為三類:

  • 商業授權工具: 主要是JMC(Java Mission Control)及它要使用到的JFR(Java Flight Recorder),JMC在個人開發環境中使用是免費的,但是在商業環境中使用它則是付費的。
  • 正式支持工具: 這一類工具屬於被長期支持的工具,不同平台、不同版本的JDK之間,這類工具可能會略有差異,但是不會出現某一個工具突然消失的情況。
  • 實驗性工具: 這一類工具在它們的使用說明中被聲明為“沒有技術支持,並且是實驗性質的”(Unsupported and Experimental)產品,日後可能會轉載,也可能會在某個JDK版本中國無聲無息地消失。

jps:虛擬機進程狀態工具

JDK的一些小工具都參考了UNIX的命名方式,jps(JVM Process Status Tool)是其中的典型。
功能也是和UNIX的ps的命令類似:
可以列出正在運行的虛擬機進程,並显示虛擬機執行主類(Main Class,main()函數所在的類)名稱以及這些進程的本地虛擬機唯一ID(LVMID,Local Virtual Machine Identifier)。
jps命令格式:

jps [ options ]  [ hostid ]

jps工具主要選項:

jstat:虛擬機統計信息監視工具

jstat( JVM Statistics Monitoring Tool )是用戶監視虛擬機各種運行狀態信息的命令行工具。可以显示本地虛擬機進程中 類加載、內存、垃圾收集、即時編譯等運行時數據,這個命令是在服務器是哪個運行期定位虛擬機性能問題的常用工具。
jstat 命令格式為:

jstat [ option  vmid [ interval [ s | ms ] [ count ] ] ]

參數interval 和 count 代表查詢間隔和次數,如果省略這2個參數,說明只查詢一次假設需要每250毫秒查詢一次進程 1440 垃圾收集狀況,一共查詢20次,那命令應當是:

jstat -gc 1440 250 20 

option 代表用戶希望查詢的虛擬機信息,主要分三類:
類加載、垃圾收集、運行期間編譯狀況。
jstat工具主要選項

jinfo:Java配置信息工具

jinfo(Configuration Info for Java)的作用是實時查看和調整虛擬機各項參數。使用jps命令的-v參數可以查看虛擬機啟動時显示指定的參數列表,但如果想知道未被显示指定的參數的系統默認值,除了去找資料外,就只能使用jinfo的-flag選項進行查詢了。jinfo還可以使用-sysprops選項把虛擬機進程的

System.getProperties()

的內容打出來。
jinfo 命令格式:

jinfo [ option ] pid

jmap:Java內存映像工具

jmap (Memory Map for Java)命令用於生成堆轉儲快照(一般稱為heapdump 或 dump文件)。
jmap的作用並不僅僅是為了獲取堆轉儲快照,它還可以查詢finalize執行隊列、Java堆和方法區的詳細信息,如空間使用率、當前用的是哪種收集器等。
jmap 命令格式:

jmap [ option ] vmid

jmap工具主要選項

jhat:虛擬機堆轉儲快照分析工具

JDK提供jhat(JVM Heap Analysis Tool)命令與jmap搭配使用,來分析jmap生成的堆轉儲快照。jhat內置了一個微型的HTTP/Web服務器,生成堆轉儲快照的分析結果后,可以在瀏覽器中查看。但是一般在實際工作中,都不會直接使用jhat命令來分析堆轉儲快照文件,一是因為分析工作耗時而且極為耗費資源,一般不會直接在服務器上使用,而是在其他機器上進行分析。二是jhat的分析功能比較簡陋,不如VisualVM,以及一些專業的分析工具例如:Eclipse Memory Analyzer、IBM HeapAnalyzer。

jstack:Java堆棧跟蹤工具

jstack(Stack Trace for Java)命令用於生成虛擬機當前時刻的線程快照(一般稱為threaddump或者javacore文件)。
線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的目的通常是定位線程出現長時間停頓的原因,如線程死鎖、死循環、請求外部資源導致長時間掛起等,都是導致線程長時間停頓的常見原因。
jstack命令格式:

jstack [ option ] vmid 

線程出現停頓時通過jstack來查看各個線程的調用堆棧,就可以獲知沒有響應的線程到底在後頭做些什麼事情,或者等待着什麼資源。
jstack工具主要選項

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

野花如何用多樣性抗氣候變遷 基因研究說分明

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

Newtonsoft 六個超簡單又實用的特性,值得一試 【下篇】

一:講故事

上一篇介紹的 6 個特性從園子里的反饋來看效果不錯,那這一篇就再帶來 6 個特性同大家一起欣賞。

二:特性分析

1. 像弱類型語言一樣解析 json

大家都知道弱類型的語言有很多,如: nodejs,python,php,它們有一個的地方就是處理json,不需要像 強類型語言 那樣還要給它配一個類,什麼意思呢? 就拿下面的 json 說事。


{
  "DisplayName": "新一代算法模型",
  "CustomerType": 1,
  "Report": {
    "TotalCustomerCount": 1000,
    "TotalTradeCount": 50
  },
  "CustomerIDHash": [1,2,3,4,5]
}

這個 json 如果想灌到 C# 中處理,你就得給它定義一個適配的類,就如 初篇 的客戶算法模型類,所以這裏就有了一個需求,能不能不定義類也可以自由解析上面這串 json 呢??? 哈哈,當然是可以的, 反序列化成 Dictionary 即可,就拿提取 Report.TotalCustomerCountCustomerIDHash 這兩個字段演示一下。


        static void Main(string[] args)
        {
            var json = @"{
                           'DisplayName': '新一代算法模型',
                           'CustomerType': 1,
                           'Report': {
                             'TotalCustomerCount': 1000,
                             'TotalTradeCount': 50
                           },
                           'CustomerIDHash': [1,2,3,4,5]
                         }";

            var dict = JsonConvert.DeserializeObject<Dictionary<object, object>>(json);

            var report = dict["Report"] as JObject;
            var totalCustomerCount = report["TotalCustomerCount"];

            Console.WriteLine($"totalCustomerCount={totalCustomerCount}");

            var arr = dict["CustomerIDHash"] as JArray;
            var list = arr.Select(m => m.Value<int>()).ToList();

            Console.WriteLine($"list={string.Join(",", list)}");
        }

2. 如何讓json中的枚舉保持更易讀的字符串型

這句話是什麼意思呢? 默認情況下, SerializeObject 會將 Model 中的 Enum 變成數值型,大家都知道數值型語義性是非常差的,如下代碼所示:


    static void Main(string[] args)
    {
        var model = new ThreadModel() { ThreadStateEnum = System.Threading.ThreadState.Running };

        var json = JsonConvert.SerializeObject(model);

        Console.WriteLine(json);
    }

    class ThreadModel
    {
        public System.Threading.ThreadState ThreadStateEnum { get; set; }
    }

對吧,確實語義特別差,那能不能直接生成 Running 這種字符串形式呢? 當然可以了。。。改造如下:


  var json = JsonConvert.SerializeObject(model, new StringEnumConverter());

這裏可能就有人鑽牛角尖了,能不能部分指定讓枚舉生成 string,其他的生成 int ,沒關係,這也難不倒我,哪裡使用就用 JsonConverter 標記哪裡。。。


        static void Main(string[] args)
        {
            var model = new ThreadModel()
            {
                ThreadStateEnum = System.Threading.ThreadState.Running,
                TaskStatusEnum = TaskStatus.RanToCompletion
            };

            var json = JsonConvert.SerializeObject(model);

            Console.WriteLine(json);
        }

        class ThreadModel
        {
            public System.Threading.ThreadState ThreadStateEnum { get; set; }

            [JsonConverter(typeof(StringEnumConverter))]
            public TaskStatus TaskStatusEnum { get; set; }
        }        

3. 格式化 json 中的時間類型

在 model 轉化成 json 的過程中,總少不了 時間類型,為了讓時間類型 可讀性更高,通常會 格式化為 YYYY年/MM月/dd日 ,那如何實現呢? 很簡單撒,在 JsonConvert 中也是一個 枚舉 幫你搞定。。。


        static void Main(string[] args)
        {
            var json = JsonConvert.SerializeObject(new Order()
            {
                OrderTitle = "女裝大佬",
                Created = DateTime.Now
            }, new JsonSerializerSettings
            {
                DateFormatString = "yyyy年/MM月/dd日",
            });

            Console.WriteLine(json);
        }
        public class Order
        {
            public string OrderTitle { get; set; }
            public DateTime Created { get; set; }
        }   

對了,我記得很早的時候,C# 自帶了一個 JavaScriptSerializer, 也是用來進行 model 轉 json的,但是它會將 datetime 轉成 時間戳,而不是時間字符串形式,如果你因為特殊原因想通過 JsonConvert 將時間生成時間戳的話,也是可以的, 用 DateFormatHandling.MicrosoftDateFormat 枚舉指定一下即可,如下:

4. 對一些常用設置進行全局化

在之前所有演示的特性技巧中都是在 JsonConvert 上指定的,也就是說 100 個 JsonConvert 我就要指定 100 次,那有沒有類似一次指定,整個進程通用呢? 這麼強大的 Newtonsoft 早就支持啦, 就拿上面的 Order 舉例:


        JsonConvert.DefaultSettings = () =>
        {
            var settings = new JsonSerializerSettings
            {
                Formatting = Formatting.Indented
            };
            return settings;
        };

        var order = new Order() { OrderTitle = "女裝大佬", Created = DateTime.Now };

        var json1 = JsonConvert.SerializeObject(order);
        var json2 = JsonConvert.SerializeObject(order);

        Console.WriteLine(json1);
        Console.WriteLine(json2);

可以看到,Formatting.Indented 對兩串 json 都生效了。

5. 如何保證 json 到 model 的嚴謹性 及提取 json 未知字段

有時候我們有這樣的需求,一旦 json 中出現 model 未知的字段,有兩種選擇: 要麼報錯,要麼提取出未知字段,在 Newtonsoft 中默認的情況是忽略,場景大家可以自己找哈。

  • 未知字段報錯

        static void Main(string[] args)
        {
            var json = "{'OrderTitle':'女裝大佬', 'Created':'2020/6/23','Memo':'訂單備註'}";

            var order = JsonConvert.DeserializeObject<Order>(json, new JsonSerializerSettings
            {
                MissingMemberHandling = MissingMemberHandling.Error
            });

            Console.WriteLine(order);
        }

        public class Order
        {
            public string OrderTitle { get; set; }
            public DateTime Created { get; set; }
            public override string ToString()
            {
                return $"OrderTitle={OrderTitle}, Created={Created}";
            }
        }        

  • 提取未知字段

我依稀的記得 WCF 在這種場景下也是使用一個 ExtenstionDataObject 來存儲客戶端傳過來的未知字段,有可能是客戶端的 model 已更新,server端還是舊版本,通常在 json 序列化中也會遇到這種情況,這裏只要使用 JsonExtensionData 特性就可以幫你搞定,在 OnDeserialized 這種AOP方法中進行攔截,如下代碼:


    static void Main(string[] args)
    {
        var json = "{'OrderTitle':'女裝大佬', 'Created':'2020/6/23','Memo':'訂單備註'}";

        var order = JsonConvert.DeserializeObject<Order>(json);

        Console.WriteLine(order);
    }

    public class Order
    {
        public string OrderTitle { get; set; }

        public DateTime Created { get; set; }

        [JsonExtensionData]
        private IDictionary<string, JToken> _additionalData;

        public Order()
        {
            _additionalData = new Dictionary<string, JToken>();
        }

        [OnDeserialized]
        private void OnDeserialized(StreamingContext context)
        {
            var dict = _additionalData;
        }

        public override string ToString()
        {
            return $"OrderTitle={OrderTitle}, Created={Created}";
        }
    }        

6. 開啟 JsonConvert 詳細日誌功能

有時候在查閱源碼的時候開啟日誌功能更加有利於理解源碼的內部運作,所以這也是一個非常實用的功能,看看如何配置吧。


        static void Main(string[] args)
        {
            var json = "{'OrderTitle':'女裝大佬', 'Created':'2020/6/23','Memo':'訂單備註'}";

            MemoryTraceWriter traceWriter = new MemoryTraceWriter();

            var account = JsonConvert.DeserializeObject<Order>(json, new JsonSerializerSettings
            {
                TraceWriter = traceWriter
            });

            Console.WriteLine(traceWriter.ToString());
        }

        public class Order
        {
            public string OrderTitle { get; set; }

            public DateTime Created { get; set; }

            public override string ToString()
            {
                return $"OrderTitle={OrderTitle}, Created={Created}";
            }
        }

三:總結

嘿嘿,這篇 6 個特性就算說完了, 結合上一篇一共 12 個特性,是不是非常簡單且實用,後面準備給大家帶來一些源碼解讀吧! 希望本篇對您有幫助,謝謝!

如您有更多問題與我互動,掃描下方進來吧~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

CPU明明8個核,網卡為啥拚命折騰一號核?

中斷機制

我是CPU一號車間的阿Q,我又來了!

我們日常的工作就是不斷執行代碼指令,不過這看似簡單的工作背後其實也並不輕鬆。

咱不能悶着頭啥也不管一個勁的只管執行代碼,還得和連接在主板上的其他單位打交道。經常保持聯繫的有鍵盤、鼠標、磁盤,哦對,還有網卡,這傢伙最近把我惹到了,待會再說這事兒。

原以為內存那傢伙已經夠慢的了,沒想到跟上面這幾位通個信比他更慢,咱CPU工廠的時間一刻值千金,不能幹等着,耽誤工夫。後來廠里一合計,想了個叫中斷的辦法。

在我們車間裝了個大燈,這些單位想聯繫我們辦事兒,就先給我們發一个中斷信號,大燈就會自動亮起。我們平時工作執行代碼指令的時候,每執行一條指令就會瞅一眼看看大燈有沒有亮起來。一旦發現燈亮了,就把手頭的工作先放一邊,去處理一下。

我們記性很差的,等會處理了完了還得回來接着原來的活繼續干,為了等會回來還能接的起來,走之前得把當前執行的這個線程的各個寄存器的值,執行到哪裡了等等這些信息都保存在這個線程的棧里去。

不過有時候我們在執行非常重要的事情的時候,就不想被他們打斷。於是我們又在車間里那個eflags寄存器中設置了一個標記,如果是1我們才允許被打斷,如果是0那就算天王老子找我們也不管了。

哦不對,還有一種不可以屏蔽的中斷NMI,走得是綠色通道。不過我可不期望有這種事情發生,因為一般都沒有好事,不是電源斷電就是溫度過高,或者總線出了錯誤等這之類嚴重的事情。

8259A PIC

還有一個問題,找我們辦事兒的單位有很多,我們得要區分開來,到底是誰來消息了,而且要是他們一起來找,按什麼樣優先級順序處理,也是一件頭疼的事情。

為此,廠里單獨組建了一個全資的子公司來負責這事兒,他就是可編程中斷控制器PIC,外號8259A,其他單位想聯繫我們都得通過這個PIC,我們只需要和PIC進行對接就可以了。

我們給辦事單位都分配了一個編號,叫做中斷向量。我們還準備了一個表格叫中斷描述符表IDT,表格里記錄了很多信息,其中就有處理這个中斷號對應的函數地址。我們找PIC拿到編號后就執行處理函數就OK了。

這個表格有點大,足足有256項,咱CPU車間空間有限,放不下,就把它放在內存那傢伙那裡了,為了能快速找到這個表,專門添置了一個叫idtr的寄存器指向這個表格。

其實除了中斷,我們在執行指令的時候如果遇到了異常情況,也會去這個表裡執行異常處理函數,最常見的比如遇到了除數是0,內存地址錯誤等等情況。

這種情況下,我們必須主動放下手裡的活,去處理異常,所以我們也說異常是同步的,而中斷不知道什麼時候發生,所以是異步的。

APIC

8259A乾的挺不錯的,不過後來咱們廠擴大規模,從單核CPU變成了多核,他就有點應付不過來了。

終於有一天,廠里召開會議,把8259A給撤了,成立了一個新的全資子公司叫高級可編程中斷控制器APIC,名字就多了個高級兩個字,乾的活還是一樣的。

不過你還別說,這兩個字還真不是吹噓,比8259A不知道高到哪裡去了。

這個APIC的新公司一上台,就成立了兩個部門,一個叫I/O APIC,負責接待那些要找我們辦事兒的單位,一個叫Local APIC,以外包的形式入駐到我CPU的各個車間工作,因為就挨着我們辦公,所以取名叫Local。

I/O APIC收到中斷信號以後,根據自己的策略就分發到對應的Local APIC,咱們八個車間就可以專心處理了,為我們省了不少事兒。

不僅如此,通過這個外包團隊,我們八個車間還能向彼此發起中斷請求,我們把這個叫做處理器間中斷Inter-Processor Interrupt,簡稱IPI

中斷親和性

每當網絡中有數據包到來,網卡那傢伙就發送一个中斷消息過來,告訴我們去處理。

不過最近不知道怎麼回事,網絡數據量激增。咱們廠里明明有8個車間,他非得一個勁的只給我們發消息,搞得我們手頭的工作老是被打斷,忙得不可開交。

終於,我忍不住了,去找網卡那傢伙理論了一番。不過他告訴我,這也不能怪他,分發給誰處理,那是APIC在負責。

想想也是,回頭我就去了APIC那裡,要求他們分攤一點給別的車間處理。

APIC表示這他們做不了主,得讓廠里來決定。

沒過幾天,廠里開了個會,參會的有各車間代表、APIC負責人,還請了操作系統那邊的相關代表過來。

會上,大家為了此事爭執不休。

二號車間虎子:“阿Q,誰叫你們一號車間是Bootstrap Processor,你們就多辛苦一點嘛”

三號車間代表:“你這話說的不合適,大家是一個Team,要互相幫助!要不這樣,既然有這麼多單位要聯繫我們,咱就分下工,比如一號車間負責網卡,二號負責磁盤,我們三號負責鍵盤,以此類推”

五號車間代表:“你想的倒是挺美哦,鍵盤一天能發多少中斷,網卡一天要發多少中斷,你凈挑輕鬆的干。這樣吧,咱就用隨機分發進行負載均衡你們覺得怎麼樣?”

八號車間代表:“隨機個啥啊,多麻煩,依我看吶咱8個車間就輪流來唄”

這時,領導問操作系統代表有沒有什麼建議。

這代表站起身來,推了推眼鏡說到:“幾位有沒有聽過線程的CPU親和性?”

大家都搖了搖頭,問到:“這是個什麼意思?”

“就是有些線程想綁定在你們之中的某一個核上面執行,不希望一會兒在這個核執行,一會兒在那個核執行”

我接過他的話:“好像是有這麼回事兒,之前有遇到過,有個線程一直被分配到我們一號車間,不過我們對這個不用關心吧,執行誰不是幹活啊,對我們都一個樣”

代表搖了搖頭,“唉,這可不一樣!你們每個核的一二級緩存都是自己在管理,要是換到別的核,這緩存多半就沒用了,又得重新來建立,這換來換去的豈不是瞎耽誤功夫嘛!對於一般的線程他們倒是不關心,但是有些線程執行大量的內存訪問和運算處理,又對性能要求很高的話,那就很在意這個問題了”

我們幾個都恍然大悟,紛紛點頭。

虎子起身問到:“那你們是如何實現這個親和性的呢?這跟我們今天的會議又有什麼關係呢?”

代表繼續回答說到:“我先回答你的第一個問題。線程調度是我們操作系統完成的工作,我們提供了API接口,線程通過調用這些接口表明自己的親和性意願,我們在調度的時候就能按照他們的意願把線程分配給你們來執行。”

代表喝了一口水接着說到:“我再回答你的第二個問題。既然線程可以有親和性,那中斷也可以按照這個思路來分發啊!APIC默認有一套分發策略,但是也提供親和性的設置,可以指定誰哪些核來處理,這樣不用把規矩定死,靈活可變,豈不更好?”

剛說完,會議室門口突然出現一年輕少年,揮手將操作系統代表喚了出去。

接下來,我們詳細討論了這種方案的可行性,最後大家一致決定,就照這麼辦,我們一起提出了一個叫中斷親和性的東西,操作系統那邊提供一個可配置的入口smp_affinity,可以通過設置各處理器核的掩碼來決定中斷交由誰來處理,APIC回去負責落地支持。

有了這套方案,再遇到網絡高峰期,咱們一號車間的壓力就有辦法緩解了。

我們剛剛達成一致,操作系統代表返回會議室,神色凝重的說到:“不好意思各位,操作系統那邊有點事情需要趕回去處理一下,先走一步了”

未完待續······

彩蛋

隨着網卡的一聲中斷,一個新的數據包來到了這片土地。

帝國網絡部新來的年輕人顯然沒有意識到危險的到來······

預知後事如何,請關注後續精彩······

往期TOP5文章

真慘!連各大編程語言都擺起地攤了!

因為一個跨域請求,我差點丟了飯碗

完了!CPU一味求快出事兒了!

哈希表哪家強?幾大編程語言吵起來了!

一個HTTP數據包的奇幻之旅

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

7000 字說清楚 HashMap,面試點都在裏面了

我是風箏,公眾號「古時的風箏」,一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!
文章會收錄在 JavaNewBee 中,更有 Java 後端知識圖譜,從小白到大牛要走的路都在裏面。

這是上篇文章 有趣的條漫版 HashMap,25歲大爺都能看懂 的文字版。有不少同學說條漫版的比較有意思,簡單易懂,但是畢竟圖片畫不了那麼詳細,只能從大面而上理解。

真正的了解細節,還得看這一篇。其實是這篇先寫完,然後畫了不少圖片,所以就寫了一篇圖片版的。本篇 7000 多字,建議三連呦。

在 Java 中,最常用的數據類型是 8 中基本類型以及他們的包裝類型以及字符串類型,其次應該就是 ArrayListHashMap了吧。HashMap存的是鍵值對類型的數據,其存儲和獲取的速度快、性能高,是非常好用的一個數據結構,每一個 Java 開發者都肯定用過它。

而且 HashMap的設計巧妙,其結構和原理也經常被拿去當做面試題。其中有很多巧妙的算法和設計,比如 Hash 算法、拉鏈法、紅黑樹設計等,值得每一個開發者借鑒學習。

想了老半天,怎麼才能簡單易懂的把 HashMap說明白呢,那就從我理解它的思路和過程去說吧。要理解一個事物最好的方式就是先了解整體結構,再去追究細節。所以,我們先從結構談起。

先從結構說起

拿我自身的一個體會來說吧,風箏我作為一個專業路痴,對於迷路這件事兒絕不含糊,雖然在北京混跡多年,但是只在中關村能分清南北,其他地方,哪怕是我每天住的小區、每天工作的公司也分不太清方向,回家只能認一條路,要是打車換條路回家,也得迷糊一陣,這麼說吧,在小區前面能回家,小區後面找不到家。去個新地方,得盯着地圖看半天。這時,我就在想啊,要是我能在城市上空俯瞰下面的街道,那我就再也不怕找不到回家的路了。這不就是三體里的降維打擊嗎,站在高維的立場,理解低維的事物,那就簡單多了。

理解數據結構也是一個道理,大多數時候,我們都是停留在會用的層面上,理解一些原理也只是支離破碎的,困在數據機構的迷宮裡跌跌撞撞,迫切的需要一張地圖或者一架直升機。

先來看一下整個 Map家族的集成關係圖,一看東西還不少,但其他的可能都沒怎麼用過,只有 HashMap最熟悉。

以下描述可能不夠專業,只為簡單的描述 HashMap的結構,請結合下圖進行理解。

HashMap主體上就是一個數組結構,每一個索引位置英文叫做一個 bin,我們這裏先管它叫做桶,比如你定義一個長度為 8 的 HashMap,那就可以說這是一個由 8 個桶組成的數組。當我們像數組中插入數據的時候,大多數時候存的都是一個一個 Node 類型的元素,Node 是 HashMap中定義的靜態內部類。

當插入數據(也就是調用 put 方法)的時候,並不是按順序一個一個向後存儲的,HashMap中定義了一套專門的索引選擇算法,叫做散列計算,但散列計算存在一種情況,叫哈希碰撞,也就是兩個不一樣的 key 散列計算出來的 hash 值是一致的,這種情況怎麼辦呢,採用拉鏈法進行擴展,比如圖中藍色的鏈表部分,這樣一來,具有相同 hash 值的不同 key 即可以落到相同的桶中,又保證不會覆蓋之前的內容。

但隨着插入的元素越來越多,發生碰撞的概率就越大,某個桶中的鏈表就會越來越長,直到達到一個閾值,HashMap就受不了了,為了提升性能,會將超過閾值的鏈錶轉換形態,轉換成紅黑樹的結構,這個閾值是 8 。也就是單個桶內的鏈表節點數大於 8 ,就會將鏈表變身為紅黑樹。

以上概括性的描述就是 HashMap的整體結構,也是我們進一步研究細節的藍圖。我們將從中抽取出幾個關鍵點一一解釋,從整體到細節,降維打擊 HashMap

接下來就是說明為什麼會設計成這樣的結構以及從單純數組到桶內鏈表產生,接着把鏈錶轉換成紅黑樹的詳細過程。

認清幾個關鍵概念

存儲容器

因為HashMap內部是用一個數組來保存內容的,數組定義如下:

transient Node<K,V>[] table;

Node 類型

table 是一個 Node類型的數組,Node是其中定義的靜態內部類,主要包括 hash、key、value 和 next 的屬性。比如之後我們使用 put 方法像其中加鍵值對的時候,就會轉換成 Node 類型。

static class Node<K,V> implements Map.Entry<K,V> {
  final int hash;
  final K key;
  V value;
  Node<K,V> next;
}

TreeNode

前面說了,當桶內鏈表到達 8 的時候,會將鏈錶轉換成紅黑樹,就是 TreeNode類型,它也是 HashMap中定義的靜態內部類。

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
  TreeNode<K,V> parent;  // red-black tree links
  TreeNode<K,V> left;
  TreeNode<K,V> right;
  TreeNode<K,V> prev;    // needed to unlink next upon deletion
  boolean red;
}

容量和默認容量

容量就是 table 數組的長度,也就是我們所說的桶的個數。其定義如下

int threshold;

默認是 16,如果我們在初始化的時候沒有指定大小,那就是 16。當然我們也可以自己指定初始大小,而 HashMap 要求初始大小必須是 2 的 冪次方。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

元素個數

容量是指定了桶的個數,而 size 是說 HashMap中實際存了多少個鍵值對。

transient int size;

最大容量

table 的長度也是有限制的,不能無限大,HashMap規定最大長度為 2 的30次方。

static final int MAXIMUM_CAPACITY = 1 << 30;

負載因子

這是一個係數,它和 threshold 結合起作用,默認是 0.75。一般情況下不要改。

final float loadFactor;

擴容閾值

閾值 = 容量 x 負載因子,假設當前 HashMap的容量是 16,負載因子是默認值 0.75,那麼當 size 到達 16 x 0.75= 12 的時候,就會觸發擴容。

初始化 HashMap

使用 HashMap肯定要初始化吧,很多情況下都是用無參構造方法創建。

Map<String,String> map = new HashMap<>();

這種情況下所有屬性都是默認值,比如容量是 16,負載因子是 0.75。

另外推薦的一種初始化方式,就是給定一個默認容量,比如指定默認容量是 32。

Map<String,String> map = new HashMap<>(32);

但是 HashMap 要求初始大小必須是 2 的 n 次方,但是又不能要求每個開發人員指定初始容量的時候都按要求來,比如我們指定初始大小為為 7、18 這種會怎麼樣呢?

沒關係,HashMap中有個方法專門負責將傳過來的參數值轉換為最接近、且大於等於指定參數的 2 的 n 次方的值,比如指定大小為 7 的話,最後實際的容量就是 8 ,如果指定大小為 18的話,那最後實際的容量就是 32 。

public HashMap(int initialCapacity, float loadFactor) {
  if (initialCapacity < 0)
    throw new IllegalArgumentException("Illegal initial capacity: " +
                                       initialCapacity);
  if (initialCapacity > MAXIMUM_CAPACITY)
    initialCapacity = MAXIMUM_CAPACITY;
  if (loadFactor <= 0 || Float.isNaN(loadFactor))
    throw new IllegalArgumentException("Illegal load factor: " +
                                       loadFactor);
  this.loadFactor = loadFactor;
  this.threshold = tableSizeFor(initialCapacity);
}

執行這個轉換動作的就是 tableSizeFor方法,經過轉換后,將最終的結果賦值給 threshold變量,也就是初始容量,也就是本篇中所說的桶個數。

static final int tableSizeFor(int cap) {
  int n = cap - 1;
  n |= n >>> 1;
  n |= n >>> 2;
  n |= n >>> 4;
  n |= n >>> 8;
  n |= n >>> 16;
  return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

tableSizeFor這個方法就有意思了,先把初始參數減 1,然後連着做或等於無符號右移操作,最後算出一個接近的 2 的冪次方,下圖演示了初始參數為 18 時的一系列操作,最後得出的初始大小為 32。

這個算法很有意思了,比如你給的初始大小是 63,那得到的結果就是 64,如果初始大小給定 65 ,那得到的結果就是 128,總是能得出不小於給定初始大小,並且最接近的2的n次方的最終值。

從 put 方法解密核心原理

put方法是增加鍵值對最常用的方法,也是最複雜的過程,增加鍵值對的過程涉及了 HashMap最核心的原理,主要包括以下幾點:

  1. 什麼情況下會擴容,擴容的規則是什麼?
  2. 插入鍵值對的時候如何確定索引,HashMap可不是按順序插入的,那樣不就真成了數組了嗎。
  3. 如何確保 key 的唯一性?
  4. 發生哈希碰撞怎麼處理?
  5. 拉鏈法是什麼?
  6. 單桶內的鏈表如何轉變成紅黑樹?

以下是 put 方法的源碼,我在其中做了註釋。


public V put(K key, V value) {
  return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
  HashMap.Node<K,V>[] tab; // 聲明 Node 數組 tab
  HashMap.Node<K,V> p;    // 聲明一個 Node 變量 p
  int n, i;
  /**
  * table 定義 transient Node<K,V>[] table; 用來存儲 Node 節點
  * 如果 當前table為空,則調用resize() 方法分配數組空間
  */
  if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  // n 總是為 2 的冪次方,(n-1) & hash 可確定 tab.length (也就是table數組長度)內的索引
  // 然後 創建一個 Node 節點賦給當前索引
  if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
  else {
    //如果當前索引位置已經有值了,怎麼辦
    // 拉鏈法出場
    HashMap.Node<K,V> e;
    K k;
    // 判斷 key 值唯一性
    // p 是當前待插入索引處的值
    // 哈希值一致並且(當前位置的 key == 待插入的key(注意 == 符號),或者key 不為null 並且 key.equals(k))
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k)))) //如果當前節點只有一個元素,且和待插入key一樣 則覆蓋
      // 將 p(當前索引)節點臨時賦予 e
      e = p;
    else if (p instanceof HashMap.TreeNode) // 如果當前索引節點是一顆樹節點
      //插入節點樹中 並返回
      e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
      // 當前索引節點即不是只有一個節點,也不是一顆樹,說明是一個鏈表
      for (int binCount = 0; ; ++binCount) {
        if ((e = p.next) == null) { //找到沒有 next 的節點,也就是最後一個
          // 創建一個 node 賦給 p.next
          p.next = newNode(hash, key, value, null);
          // 如果當前位置+1之後大於 TREEIFY_THRESHOLD 則要進行樹化
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            //執行樹化操作
            treeifyBin(tab, hash);
          break;
        }
        //如果又發生key衝突則停止 後續這個節點會被相同的key覆蓋
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
          break;
        p = e;
      }
    }
    if (e != null) { // existing mapping for key
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
        e.value = value;
      afterNodeAccess(e);
      return oldValue;
    }
  }
  ++modCount;
  // 當實際長度大於 threshold 時 resize
  if (++size > threshold)
    resize();
  afterNodeInsertion(evict);
  return null;
}

首次初始化數組和擴容

在執行 put方法時,第一步要檢查 table 數組是否為空或者長度是否為 0,如果是這樣的,說明這是首次插入鍵值對,需要執行 table 數組初始化操作。

另外,隨之鍵值對添加的越來越多,HashMap的 size 越來越大,注意 size 前面說了,是實際的鍵值對數量,那麼 size 到了多少就要擴容了呢,並不是等 size 和 threshold(容量)一樣大了才擴容,而是到了閾值就開始擴容,閾值上面也說了,是容量 x 負載因子

為什麼放在一起說呢,因為首次初始化和擴容都是用的同一個方法,叫做 resize()。以下是我註釋的 resize()方法。

final HashMap.Node<K,V>[] resize() {
  // 保存 table 副本,接下來 copy 到新數組用
  HashMap.Node<K,V>[] oldTab = table;
  // 當前 table 的容量,是 length 而不是 size
  int oldCap = (oldTab == null) ? 0 : oldTab.length;
  // 當前桶大小
  int oldThr = threshold;

  int newCap, newThr = 0;
  if (oldCap > 0) { //如果當前容量大於 0,也就是非第一次初始化的情況(擴容場景下)
    if (oldCap >= MAXIMUM_CAPACITY) { //不能超過最大允許容量
      threshold = Integer.MAX_VALUE;
      return oldTab;
    }
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY) // 雙倍擴容
      newThr = oldThr << 1; // double threshold
  }
  else if (oldThr > 0) // 初始化的場景(給定默認容量),比如 new HashMap(32)
    newCap = oldThr; //將容量設置為 threshold 的值
  else {               // 無參數初始化場景,new HashMap()
    // 容量設置為 DEFAULT_INITIAL_CAPACITY
    newCap = DEFAULT_INITIAL_CAPACITY;
    // 閾值 超過閾值會觸發擴容
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  }
  if (newThr == 0) { //給定默認容量的初始化情況
    float ft = (float)newCap * loadFactor;
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
              (int)ft : Integer.MAX_VALUE);
  }
  // 保存新的閾值
  threshold = newThr;
  // 創建新的擴容后數組,然後將舊的元素複製過去
  @SuppressWarnings({"rawtypes","unchecked"})
  HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
  table = newTab;
  if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
      HashMap.Node<K,V> e;
      //遍歷 獲得得到元素 賦給 e
      if ((e = oldTab[j]) != null) { //如果當前桶不為空
        oldTab[j] = null; // 置空回收
        if (e.next == null) //節點 next為空的話 重新尋找落點 
          newTab[e.hash & (newCap - 1)] = e;
        else if (e instanceof HashMap.TreeNode) //如果是樹節點
          //紅黑樹節點單獨處理
          ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
        else { // 保持原順序
          HashMap.Node<K,V> loHead = null, loTail = null;
          HashMap.Node<K,V> hiHead = null, hiTail = null;
          HashMap.Node<K,V> next;
          do {
            next = e.next;
            if ((e.hash & oldCap) == 0) {
              if (loTail == null)
                loHead = e;
              else
                loTail.next = e;
              loTail = e;
            }
            else {
              if (hiTail == null)
                hiHead = e;
              else
                hiTail.next = e;
              hiTail = e;
            }
          } while ((e = next) != null);
          if (loTail != null) {
            loTail.next = null;
            newTab[j] = loHead;
          }
          if (hiTail != null) {
            hiTail.next = null;
            newTab[j + oldCap] = hiHead;
          }
        }
      }
    }
  }
  return newTab;
}

首次初始化

put方法中線先檢查 table 數組是否為空,如果為空就初始化。

if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;

首次初始化分為無參初始化和有參初始化兩種情況,前面在講 HashMap初始化的時候說了,無參情況默認就是 16,也就是 table 的長度為 16。有參初始化的時候,首先使用 tableSizeFor()方法確定實際容量,最後 new 一個 Node 數組出來。

HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];

其中 newCap就是容量,默認16或者自定義的。

而這個過程中還有很重要的一步,就是維護擴容閾值

擴容

put方法中,判斷當 size(實際鍵值對個數)到達 threshold (閾值)時,觸發擴容操作。

// 當實際長度大於 threshold 時 resize
if (++size > threshold)
    resize();

HashMap遵循兩倍擴容規則,每次擴容之後的大小是擴容前的兩倍。另外,說到底,底層的存儲還是一個數組,Java 中沒有真正的動態數組這一說,數組初始化的時候是多大,那它就一直是這麼大,那擴容是怎麼來的呢,答案就是創建一個新數組,然後將老數組的數據拷貝過去。

拷貝的時候可能會有如下幾種情況:

  1. 如果節點 next 屬性為空,說明這是一個最正常的節點,不是桶內鏈表,也不是紅黑樹,這樣的節點會重新計算索引位置,然後插入。
  2. 如果是一顆紅黑樹,則使用 split方法處理,原理就是將紅黑樹拆分成兩個 TreeNode 鏈表,然後判斷每個鏈表的長度是否小於等於 6,如果是就將 TreeNode 轉換成桶內鏈表,否則再轉換成紅黑樹。
  3. 如果是桶內鏈表,則將鏈表拷貝到新數組,保證鏈表的順序不變。

確定插入點

當我們調用 put方法時,第一步是對 key 進行 hash 計算,計算這個值是為了之後尋找落點,也就是究竟要插入到 table 數組的哪個桶中。

hash 算法是這樣的,拿到 key 的 hashCode,將 hashCode 做一次16位右位移,然後將右移的結果和 hashCode 做異或運算,這段代碼叫做「擾動函數」,之所以不直接拿 hashCode 是為了增加隨機性,減少哈希碰撞次數。

/**
* 用來計算 key 的 hash 值
**/
static final int hash(Object key) {
  int h;
  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

拿到這個 hash 值之後,會進行這樣的運算 i = (n - 1) & hash,其中 i就是最終計算出來的索引位置。

有兩個場景用到了這個索引計算公式,第一個場景就是 put方法插入鍵值對的時候。第二個場景是在 resize 擴容的時候,new 出來新數組之後,將已經存在的節點移動到新數組的時候,如果節點不是鏈表,也不是紅黑樹,而是一個普通的 Node 節點,會重新計算,找到在新數組中的索引位置。

接着看圖,還是圖說的清楚。

HashMap 要求容量必須是 2 的 n 次方,2的 n 次方的二進製表示大家肯定都很清楚,2的6次方,就是從右向左 6 個 0,然後第 7 位是 1,下圖展示了 2 的 6 次方的二進製表示。

然後這個 n-1的操作就厲害了,減一之後,後面之前二進製表示中 1 後面的 0 全都變成了 1,1 所在的位變為 0。比如 64-1 變為 63,其二進製表示是下面這樣的。

下圖中,前面 4 行分別列出了當 map 的容量為 8、16、32、64的時候,假設容量為 n,則對應的 n-1 的二進製表示是下面這樣的,尾部一片紅,都是 1 ,能預感到將要有什麼騷操作。

沒錯,將這樣的二進製表示代入這個公式 (n - 1) & hash中,最終就能確定待插入的索引位了。接着看圖最下面的三行,演示了假設當前 HashMap的容量為 64 ,而待插入的一個 key 經過 hash 計算后得到的結果是 99 時,代入公式計算 index 的值,也就是 (64-1)& 99,最終的計算結果是 35,也就是這個 key 會落到 table[35] 這個位置。

為什麼 HashMap一定要保證容量是 2 的冪次方呢,通過二進製表示可以看出,如果有多位是 1 ,那與 hash 值進行與運算的時候,更能保證最後散列的結果均勻,這樣很大程度上由 hash 的值來決定。

如何確保 key 的唯一性

HashMap中不允許存在相同的 key 的,那怎麼保證 key 的唯一性呢,判斷的代碼如下。

if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))

首先通過 hash 算法算出的值必須相等,算出的結果是 int,所以可以用 == 符號判斷。只是這個條件可不行,要知道哈希碰撞是什麼意思,有可能兩個不一樣的 key 最後產生的 hash 值是相同的。

並且待插入的 key == 當前索引已存在的 key,或者 待插入的 key.equals(當前索引已存在的key),注意== 和 equals 是或的關係。== 符號意味着這是同一個對象, equals 用來確定兩個對象內容相同。

如果 key 是基本數據類型,比如 int,那相同的值肯定是相等的,並且產生的 hashCode 也是一致的。

String 類型算是最常用的 key 類型了,我們都知道相同的字符串產生的 hashCode 也是一樣的,並且字符串可以用 equals 判斷相等。

但是如果用引用類型當做 key 呢,比如我定義了一個 MoonKey 作為 key 值類型

public class MoonKey {

    private String keyTile;

    public String getKeyTile() {
        return keyTile;
    }

    public void setKeyTile(String keyTile) {
        this.keyTile = keyTile;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        MoonKey moonKey = (MoonKey) o;
        return Objects.equals(keyTile, moonKey.keyTile);
    }
}

然後用下面的代碼進行兩次添加,你說 size 的長度是 1 還是 2 呢?

Map<MoonKey, String> m = new HashMap<>();
MoonKey moonKey = new MoonKey();
moonKey.setKeyTile("1");
MoonKey moonKey1 = new MoonKey();
moonKey1.setKeyTile("1");
m.put(moonKey, "1");
m.put(moonKey1, "2");
System.out.println(hash(moonKey));
System.out.println(hash(moonKey1));
System.out.println(m.size());

答案是 2 ,為什麼呢,因為 MoonKey 沒有重寫 hashCode 方法,導致 moonkey 和 moonKey1 的 hash 值不可能一樣,當不重寫 hashCode 方法時,默認繼承自 Object的 hashCode 方法,而每個 Object對象的 hash 值都是獨一無二的。

划重點,正確的做法應該是加上 hashCode的重寫。

@Override
public int hashCode() {
  return Objects.hash(keyTile);
}

這也是為什麼要求重寫 equals 方法的同時,也必須重寫 hashCode方法的原因之一。 如果兩個對象通過調用equals方法是相等的,那麼這兩個對象調用hashCode方法必須返回相同的整數。有了這個基礎才能保證 HashMap或者HashSet的 key 唯一。

發生哈希碰撞怎麼辦

前面剛說了相等的對象產生的 hashCode 也要相等,但是不相等的對象使用 hash方法計算之後也有可能產生相同的值,這就叫做哈希碰撞。雖然通過算法已經很大程度上避免碰撞的發生,但是卻無法避免。

產生碰撞之後,自然得出的在 table 數組的索引(也就是桶)也是一樣的,這時,怎麼辦呢,一個桶里怎麼放多個鍵值對?

拉鏈法

文章剛開頭就提到了,HashMap可不是簡單的數組而已。當碰撞發生就坦然接收。有一種方法叫做拉鏈法,不是衣服上那種拉鏈。而是,當碰撞發生了,就在當前桶上拉一條鏈表出來,這樣解釋就合理了。

前面介紹關鍵概念的時候提到了 Node類型,裏面有個屬性叫做 next,它就是為了這種鏈表設計的,如下圖所示。node1、node2、node3都落在了同一個桶中,這時候就得用鏈表的方式處理了,node1.next = node2,node2.next = node3,這樣將鏈表串起來。而 node3.next = null,則說明這是鏈表的尾巴。

當有新元素準備插入到鏈表的時候,採用的是尾插法,而不是頭插法了,JDK 1.7 的版本採用的是頭插法,但是頭插法有個問題,就是在兩個線程執行 resize() 擴容的時候,很可能造成環形鏈表,導致 get 方法出現死循環。

鏈錶轉換成樹

鏈表不是碰撞處理的終極結構,終極結構是紅黑樹,當鏈表長度到達 8 之後,再有新元素進來,那就要開始由鏈表到紅黑樹的轉換了。方法 treeifyBin是完成這個過程的。

使用紅黑樹是出於性能方面的考慮,紅黑樹的查找速度要優於鏈表。那為什麼不是一開始就直接生成紅黑樹,而是鏈表長度大於 8 之後才升級成樹呢?

首先來說,哈希碰撞的概率還是很小的,大部分情況下都是一個桶裝一個 Node,即便發生碰撞,都碰撞到一個桶的概率那就更是少之又少了,所以鏈表長度很少有機會能到 8 ,如果鏈表長度到 8 了,那說明當前 HashMap中的元素數量已經非常大了,那這時候用紅黑樹來提高性能是可取的。而反過來,如果 HashMap總的元素很少,即便用紅黑樹對性能的提升也不大,況且紅黑樹對空間的使用要比鏈表大很多。

get 方法

T value = map.get(key);

例如通過上面的語句通過 key 獲取 value 值,是我們最常用到的方法了。

看圖理解,當調用 get方法后,第一步還是要確定索引位置,也就是我們所說的桶的位置,方法和 put方法時一樣,都是先使用 hash這個 擾動函數 確定 hash 值,然後用 (n-1) & hash獲取索引。這不廢話嗎,當然得和 put的時候一樣了,不一樣還怎麼找到正確的位置。

確定桶的位置后,會出現三種情況:

單節點類型: 也就是這個桶內只有一個鍵值對,這也在 HashMap中存在最多的類型,只要不發生哈希碰撞都是這種類型。其實 HashMap最理想的情況就是這樣,全都是這種類型就完美了。

鏈表類型: 如果發現 get 的 key 所在的是一個鏈表結構,就需要遍歷鏈表,知道找到 key 相等的 Node。

紅黑樹類型: 當鏈表長度超過 8 就轉變成紅黑樹,如果發現找到的桶是一顆紅黑樹,就使用紅黑樹專有的快速查找法查找。

另外,Map.containsKey方法其實用的就是 get方法。

remove 方法

removeputget方法類似,都是先求出 key 的 hash 值,然後 (n-1) & hash獲取索引位置,之後根據節點的類型採取不同的措施。

單節點類型: 直接將當前桶元素替換為被刪除 node.next ,其實就是 null。

鏈表類型: 如果是鏈表類型,就將被刪除 node 的前一個節點的 next 屬性設置為 node.next。

紅黑樹類型: 如果是一棵紅黑樹,就調用紅黑樹節點刪除法,這裏,如果節點數在 2~6之間,就將樹結構簡化為鏈表結構。

非線程安全

HashMap沒有做併發控制,如果想在多線程高併發環境下使用,請用 ConcurrentHashMap。同一時刻如果有多個線程同時執行 put 操作,如果計算出來的索引(桶)位置是相同的,那會造成前一個 key 被后一個 key 覆蓋。

比如下圖線程 A 和 線程 B 同時執行 put 操作,很巧的是計算出的索引都是 2,而此時,線程A 和 線程B都判斷出索引為 2 的桶是空的,然後就是插入值了,線程A先 put 進去了 key1 = 1的鍵值對,但是,緊接着線程B 又 put 進去了 key2 = 2,線程A 表示痛哭流涕,白忙活一場。最後索引為2的桶內的值是 key2=2,也就是線程A的存進去的值被覆蓋了。

總結

前面沒說,HashMap搞的這麼複雜不是白搞的,它的最大優點就是快,尤其是 get數據,是 O(1)級別的,直接定位索引位置。

HashMap不是單純的數組結構,當發生哈希碰撞時,會採用拉鏈法生成鏈表,當鏈表大於 8 的時候會轉換成紅黑樹,紅黑樹可以很大程度上提高性能。

HashMap容量必須是 2 的 n 次方,這樣設計是為了保證尋找索引的散列計算更加均勻,計算索引的公式為 (n - 1) & hash

HashMap在鍵值對數量達到擴容閾值「容量 x 負載因子」的時候進行擴容,每次擴容為之前的兩倍。擴容的過程中會對單節點類型元素進行重新計算索引位置,如果是紅黑樹節點則使用 split方法重新考量,是否將紅黑樹變為鏈表。

壯士且慢,先給點個贊吧,總是被白嫖,身體吃不消!

我是風箏,公眾號「古時的風箏」。一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!你可選擇現在就關注我,或者看看歷史文章再關注也不遲。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

用樹的高度來建築  弗班社區找回古城區的感覺

環境資訊中心特約記者 陳文姿報導

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

宣示2050年零碳排 全球87大企業領頭邁向1.5℃目標

環境資訊中心綜合外電;姜唯 編譯;彭瑞祥 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

氣候暖化 瑞士居民為消失冰川辦葬禮

摘錄自2019年9月23日公視報導

瑞士居民為阿爾卑斯山的冰川舉行了葬禮,受氣候變遷影響,這座冰川從2006年消融速度加快,現在已經消失了90%面積。

大約250個瑞士居民,22日穿著黑衣,披著黑頭紗爬了約兩小時的路程,登上海拔約2700公尺的皮措爾山山頂,為這座即將消失的冰川舉行葬禮。

瑞士蘇黎世聯邦理工學院冰川專家赫斯表示,「照目前情況來看,我們還有約4個足球場大小的冰川,但過去兩年冰川消融的速度迅速增加。」

皮措爾冰川位在瑞士境內的阿爾卑斯山,自從2006年以來,已經失去了將近90%面積,現在只剩下約兩萬6000平方公尺,不到四個足球場大小,科學家認為,冰川消融如此快速是受到氣候變遷影響,如果再不控制溫室氣體排放,這座冰川將會在2030年前完全消失。

 

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

專家解讀:利用Angular項目與數據庫融合實例

摘要:面對如何在現有的低版本的框架服務上,運行新版本的前端服務問題,華為雲前端推出了一種融合方案,該方案能讓獨立的Angular項目整體運行在低版本的框架服務上,通過各種適配手段,讓Angular項目也能獲取到外層框架服務的資源。

華為雲前端服務前期採用AngularJs作為框架技術棧,技術較為老舊,性能較差,在華為雲快速發展的今天,顯然不能滿足要求。因此我們必須要升級前端技術棧,使用Angular2+來承載我們的前端服務。GeminiDB作為新服務,也是數據庫乃至華為雲未來的重點服務,作為前端部分,必須在技術上使用最前沿的框架,以最大地提高用戶體驗。

但是技術棧的升級不是一蹴而就的,尤其是在華為雲,所有的雲服務必須在框架服務的底座上運行,而框架服務承載了所有的雲服務,如果要進行技術棧升級,必然是一個緩慢的過程。GeminiDB作為華為雲服務里的一員,也不可能脫離框架服務而存在。因此存在一個問題,就是如何在現有的低版本的框架服務上,運行新版本的前端服務。

為了解決以上問題,華為雲前端推出了一種融合方案,該方案能讓獨立的Angular項目整體運行在低版本的框架服務上,通過各種適配手段,讓Angular項目也能獲取到外層框架服務的資源。

底層項目

底層項目使用webpack打包,打包后通過在index.html里引入businessAll.js文件,以該文件為入口啟動整個框架服務。

<script type="text/javascript" src="businessAll.js"></script>

在底層框架服務啟動后,再渲染出具體雲服務內容。

<div class="service-content-view" ui-view ng-animate="{enter:'fade-enter'}"></div>

Angular項目

Angular項目支持獨立運行,有單獨的index.html,也有單獨的main.ts入口。但是如果希望Angular項目運行在底層框架服務上,就必須把Angular項目看作是一個獨立的模塊,把項目整體引入到底層項目中。因此,我們可以預先把Angular項目編譯好,放到底層項目的一個目錄下。在運行底層項目時,在index.html里將Angular項目引進來,獨立運行。

<link rel="stylesheet" type="text/css" href="{底層項目中Angular項目的路徑}/styles.css" />
<script type="text/javascript" src="{底層項目中Angular項目的路徑}/runtime.js"></script>
<script type="text/javascript" src="{底層項目中Angular項目的路徑}/polyfills.js"></script>
<script type="text/javascript" src="{底層項目中Angular項目的路徑}/main.js"></script>

項目融合

底層項目和Angular項目均能獨立,但是要讓兩者融合起來,會遇到以下幾個問題:

1.底層項目中如何渲染出Angular項目。

2.Angular項目依賴底層項目的資源,如何保證Angular項目在底層項目運行起來后再運行。

3.如何解決底層項目和Angular項目的路由衝突問題。

渲染Angular項目

底層項目分為兩部分,一部分是底層框架服務,另一部分是具體雲服務。現在我們要做的是把老的雲服務項目替換成新的Angular項目,因此我們可以直接在渲染老的雲服務的地方替換成新的Angular項目的渲染容器。

<div class="service-content-view" ui-view ng-animate="{enter:'fade-enter'}"></div>
<app-root></app-root>

底層框架服務對頁面渲染上做了一些體驗上的優化,因此必須保留原模板中的ui-view,使底層項目正常運行起來,實際上老的雲服務項目的渲染內容已經轉發到新的Angular項目上面。

Angular項目對底層項目的依賴

底層框架服務給雲服務提供了很多公共變量與服務,這些變量和服務是各個雲服務必須要使用的,否則雲服務將不能正常運作。

啟動順序問題

對於Angular項目來說,要使用底層框架服務提供的內容,首先要求Angular項目在底層項目運行起來之後再運行。這裏採用Augular中的APP_INITIALIZER令牌來解決這個問題。APP_INITIALIZER是一個函數,在程序初始化的時候被調用。這裡在根模塊的providers中以factory的形式來配置。

import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";

import { AppInitService } from './services/app-init.service';
import { AppComponent } from "./app.component";

@NgModule({
 declarations: [AppComponent],
 imports: [BrowserModule],
 providers: [
     AppInitService,
    {
         provide: APP_INITIALIZER,
         useFactory: initializeApp,
         deps: [AppInitService],
         multi: true
    }
],
 bootstrap: [AppComponent]
})
export class AppModule {}

export function initializeApp(appInitService: AppInitService) {
   return (): Promise<any> => {
       return appInitService.Init();
  };
}

在appInitService里,先獲取到底層框架的資源,再進行Angular項目的初始化。

import { Injectable } from '@angular/core';

@Injectable()
export class AppInitService {
   constructor() {}

   Init() {
       return new Promise<void>((resolve, reject) => {
           // 獲取到底層框架服務的資源
           resolve();
      });
  }
}

資源依賴問題

底層項目使用的是AngularJs,Angular項目獲取底層框架服務提供的資源不能通過Angular的方式引入,因此需要藉助AngularJS的注入器獲取在底層框架中註冊的服務組件:

static get(inject: string): any {
return (window as any).angular.element('html').injector().get(inject);}
如,要獲取 $rootScope:
 rootScope = (window as any).angular.element('html').injector().get(‘$rootScope’);

路由衝突問題

Angular項目本身有自己的路由,但是Angular項目是運行在底層框架之上的,Angular項目的路由將會被底層框架所攔截。因此,我們也需要在底層框架的項目中配置相同的路由,以免Angular項目中的有效路由被底層框架識導向為404。

Angular項目路由:

{
   path: '',
   redirectTo: 'ng2app1',
   pathMatch: 'full'
},
{
   path: 'ng2app1',
   loadChildren: './ng2app1/ng2app1.module#Ng2app1Module',
},
{
   path: 'ng2app2',
   loadChildren: './ng2app2/ng2app2.module#Ng2app2Module',
}
 
底層框架路由:
var configArr = [
  {
       name: 'ng2app1',
       url: '/ng2app1'
  },
  {
       name: 'ng2app2',
       url: '/ng2app2'
  }
];

另外,由於底層項目使用的是hash路由,Angular項目中也要做相應的配置,默認是使用的是PathLocationStrategy,需要切換到hash模式。

import { LocationStrategy, HashLocationStrategy } from '@angular/common';

...
providers: [
  {
       provide: LocationStrategy,
       useClass: HashLocationStrategy
  }
]

總結

以上方案是在底層框架升級周期長的前提下的一個臨時方案,實際上還是存在着不少的問題。比如底層框架對於老的雲服務容器是有統一管理的,老的雲服務容器會針對不同的場景能夠自適應,而融合方案中的Angular項目則不能;每次啟動整個項目時,必須要預先編譯好裏面的Angular項目,再去啟動外層的底層框架,開發效率比較低。因此,後續GeminiDB服務應該在底層框架升級后,儘快適應到新的底層框架體系中,提高服務的可用性和穩定性。

 

點擊關注,第一時間了解華為雲新鮮技術~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

探索ADC的原理(自製3位并行比較型ADC)

摘要

      本文通過列舉歷史中出現的產品,梳理了模數轉換器在20世紀30年代~~20世紀80年代末的發展歷史。接下來,簡要介紹模數轉換器的原理、技術指標、分類和未來發展方向。最後,提供了一種自製3位FLASH型ADC的方法(該方法經過了作者的測試且價格在20元以下)。

 

參考文獻

    涉及到的數據手冊(eyg7)

    Flash ADC_Chapter 13 – Digital-Analog Conversion

    ZepToBars

    《Analog-Digital Conversion》 Chapter I Walt Kester

    《数字电子技術》第六版 康華光

    数字电子技術 西南石油大學課程中心

 

ADC的歷史

     世界上記載的第一個”純电子“的A/D轉換器於1939年被亞歷克·哈利·里夫斯(Alec Harley Reeves)發明,該設計的採樣率為6KSPS,分辨率為5位。

亞歷克·哈利·里夫斯設計的A/D轉換器原理圖,《Analog-Digital Conversion》 Chapter I Walt Kester

     1947年,鍺晶體管於貝爾實驗室誕生。

     1946年,ENIAC問世,現代数字計算機的鼻祖,為A/D的蓬勃發展做鋪墊。

     1948年,貝爾實驗室發明了5位、8KSPS的逐次逼近型A/D轉換器。

     得力於电子束編碼管技術,在1960年左右出現了12MSPS、9位的編碼器(A/D)。

 电子束編碼管原理圖,《Analog-Digital Conversion》 Chapter I Walt Kester

     1954年,硅晶體管於德州儀器誕生。

     1954年,伯納德·M·戈登(Bernard M. Gordon)發明了11位、50KSPS的基於真空管的A/D,這被認為是世界上第一個商業化的A/D轉換器。“Datrac”功率500W,售價8000~~9000美元。

伯納德·M·戈登發明的 “Datrac”,《Analog-Digital Conversion》 Chapter I Walt Kester

     1958/1959,集成電路問世,德州儀器(1958),仙童半導體(1959)。

     1963~1965年,為了給美國軍方的雷達提供高速A/D,貝爾實驗室的John M. Eubanks和Robert C. Bedingfield研發了8位、10MSPS的A/D,其功率為150W、售價10000美元。

 John M. Eubanks和Robert C. Bedingfield研發的A/D,《Analog-Digital Conversion》 Chapter I Walt Kester

      1969年,Pastoriza公司利用分立元件製造了12位、10us、2.3W的逐次逼近型A/D樣機—-“ADC-12U”,售價800美元。

 “ADC-12U”原型機,《Analog-Digital Conversion》 Chapter I Walt Kester

     1978年,Paul Brokaw設計了第一個完整的單芯片ADC,型號為AD571,使用了雙極型工藝,參數為:10位、25us、SAR結構。同年,誕生了最具重要意義的SAR ADC–AD574。這時的A/D可以說開始走向現代。

AD571,源

AD571框圖,《Analog-Digital Conversion》 Chapter I Walt Kester

     1988年,Crystal Semiconductor推出了世界上第一個單芯片商業化的ε-Δ ADC–CSZ5316,參數:16位、20KSPS,可以用於語音處理。

     接下來的歷史中,各廠商不斷改進ADC的性能、推出更多不同用途的ADC。總而言之,就是讓ADC進入千家萬戶。

 

ADC的原理

     ADC(Analog to Digital Converter)是一類將模擬信號(連續信號)轉換為数字信號(離散信號)的器件,按原理可分為:并行比較型A/D轉換器(FLASH ADC)、逐次比較型A/D轉換器(SAR ADC)和雙積分式A/D轉換器(Double Integral ADC)。

     模擬信號,下圖中的ui(t)是一個輸入的模擬電壓信號,可以想象成從一個麥克風輸出的音頻信號。

     数字信號,現代計算機能夠處理的信號,表現為下圖中的“n位数字量輸出”。

     香農-奈奎斯特(Shannon & Nyquist)採樣定理規定,使恢復出的信號不失真的條件:採樣頻率大於原始信號頻率的兩倍,即 Fs >= 2Fi

      一個連續的電壓信號ui(t)通過一個由方波CPs控制的開關S之後施加到電容C上,由於電容兩端的電壓不會突變,可知在S斷開時C將維持ui(t)在開關斷開瞬間的電壓一段時間,直到開關S再次打開。這樣,一個模擬的電壓信號就轉換成了採樣展寬信號us(t),其中CPs的頻率就是採樣頻率Fs。然後,由ADC的数字編碼電路將採樣展寬信號us(t)轉換成n位的数字量dn-1 : d0並輸出。

     通過上述步驟,一個連續的電壓信號就轉換成了n位的数字量,而實現該過程的器件叫做模擬-数字轉換器(ADC)。

AD轉換的一般原理,”数字电子技術”  SWPU

TLC5540I,8位、40MSPS、CMOS工藝的并行比較型A/D轉換器的版圖,https://zeptobars.com/,license: CC BY 3.0,未修改

 

 ADC的主要性能指標

     分辨率:ADC能分辨的最小電壓,通常用位數表示,例如:8位。一個n=8位的ADC,參考電壓為5V,則其能分辨的最小電壓為 5 / 2^n = 19.53mV

     轉換時間:ADC從控制信號到來開始,到輸出端得到穩定的数字信號所經歷的時間。

     轉換精度:ADC輸出的数字量所表示的模擬值與實際輸入的模擬量之間的偏差。

 

ADC的分類

     并行比較型A/D轉換器:這是本文嘗試構建的ADC,其由電阻分壓器、電壓比較器(運算放大器)、D觸發器和優先級編碼器構成。其原理簡單,將在後文介紹。

                            優點:1.轉換時間最短,其轉換周期為通過比較器、觸發器和優先級編碼器的時間總和(見下式),這個數值通常很小。

T轉 = T比 + T寄 + T編

                            缺點:1.造價高昂,隨着分辨位數的提高,所需的元件幾乎按幾何級數增長,如:一個n位的并行比較型ADC,需要2^n – 1個比較器和2^n – 1個觸發器,假如n=12,那麼一共需要8190個比較器和觸發器!

                                    2.對集成電路的工藝要求很高。

                    常見的型號:AD9012,TTL工藝,分辨率為8位,採樣率為100MSPS,模擬輸入電壓範圍 -Vs~~+0.5V(Vs為芯片供電電壓)。

                                     AD9002,ECL工藝(射極耦合邏輯),分辨率為8位,採樣率為150MSPS,模擬輸入電壓範圍 -Vs~~+0.5V(Vs為芯片供電電壓)。

                                     AD9020,TTL工藝,分辨率為10位,採樣率為60MSPS,雙極性模擬輸入(+-1.75V)。

3位并行比較型A/D轉換器原理圖,《数字电子技術》第六版 康華光

AD9012原理圖,Analog Devices 

AD9002原理圖,Analog Devices  

AD9020原理圖,Analog Devices 

1107PV2,蘇聯,8位、20MSPS,典型的并行比較型A/D轉換器的版圖https://zeptobars.com/,license: CC BY 3.0,未修改

1107PV2,蘇聯,8位、20MSPS,典型的并行比較型A/D轉換器的比較器的版圖https://zeptobars.com/,license: CC BY 3.0,未修改

 

     逐次比較型A/D轉換器:原理像天平,對輸入的模擬電壓信號與不同權值的電壓做多次比較,使得轉換所得的数字量在數值上不斷逼近輸入的模擬量。通常由控制邏輯電路、數據寄存器、移位寄存器、D/A轉換器(Digital Analog Converter)和電壓比較器構成。

                            優點:1.轉換速度快。其轉換周期等於 分辨率 * 時鐘周期(見下式),如一個8位的逐次比較型A/D轉換器,時鐘周期為10us,則其轉換周期為80us。

T轉 = n * Tclk  (n為分辨率)

                   常見的型號:1.ADC0808/ADC0809,8位逐次比較型A/D轉換器,轉換時間100us,輸入電壓範圍0~~5V,可接入8個模擬量輸入。

                                    2.ADC0803/ADC0804,8位逐次比較型A/D轉換器,在1MHz的時鐘頻率下,轉換時間在66~~73us之間,支持一對差分模擬電壓輸入。

逐次比較型A/D轉換器原理圖 ,《数字电子技術》第六版 康華光

 

 ADC0808/ADC0809原理圖,National Semiconductor

ADC0803/ADC0804原理圖,Philips Semiconductors

 

     雙積分式A/D轉換器:一種間接的A/D轉換器,其分別對輸入電壓和參考電壓進行兩次積分,將輸入電壓平均值變換成與之成正比的時間間隔,然後利用時鐘脈衝和計數器測出此時間間隔,進而在輸出端得到與模擬量相應的数字量。通常由積分器(運算放大器及相應的外部電路)、過零比較器(運算放大器)、時鐘脈衝控制門和計數器等構成。

                         優點:1.抗工頻干擾能力強。通過對輸入電壓的平均值進行變換來實現抗干擾。 

                         缺點:1.轉換速度最慢。

                常見的型號:TLC7135,4.5位雙積分式A/D轉換器,CMOS工藝,差分電壓輸入。

雙積分式A/D轉換器原理圖,《数字电子技術》第六版 康華光

TLC7135数字部分原理圖,Texas Instruments

 

ADC的未來發展方向

     ADC在未來會變得性能更強、價格更低、功耗更低、通用性和專業性更強。

     性能:從歷史上看,對ADC性能的改進主要集中在改進架構改善製造工藝兩個方面。ADC有很多架構,典型的包括:FLASH、SAR和雙積分;其他的有:流水線等。在集成電路發展的過程中,出現了許多的工藝:雙極性、ECL、CMOS、CB、BiCMOS、GaAs……這些工藝可以幫助改進ADC的性能。

     價格:隨着集成電路工藝的不斷成熟,價格變低只是時間問題。

     功耗:得力於集成電路工藝的改善,如:使用更低線寬的IC的功耗會低於高線寬的IC。功耗同時也取決於ADC架構。

 

元件清單(” * “為可選)

————————————————————時鐘發生器部分——————————————————————–

  NE555           *1

  *DIP-8芯片座  *1

  8位撥碼開關    *1

  *3pin排針       *1

  3.9K電阻        *1

  68K電阻         *1

  10uF無極電容   *1

  1uF無極電容     *1

  100nF無極電容 *1

  10nF無極電容   *2

  1nF無極電容     *1

  100pF無極電容 *1

  10pF無極電容   *1

  1pF無極電容     *1

所有元件合照(時鐘發生器部分)

———————————————————————————————————————————————–

————————————————————數模轉換器部分——————————————————————–

  MCP6004        *1(可以使用LM324替換)

  *DIP-14芯片座 *1

  CD4042B *1

  CD4532B *1

  *DIP-16芯片座 *2

  2K可調電阻器   *1

  330R電阻   *3

  390R電阻   *1

  1K電阻       *5

  LED-G       *3

  *Pin-3排母 *1

  *Pin-2排針 *1

所有元件合照(數模轉換器部分,不含Pin-2排針)

———————————————————————————————————————————————–

 

電路原理

 

總原理圖(1)

 

總原理圖(2)

————————————————————時鐘發生器部分——————————————————————–

     555定時器工作在多諧振蕩器模式,通過撥碼開關選擇不同的電容來產生不同頻率的方波。

 

基於555定時器的時鐘發生器原理圖

————————————————————————————————————————————————

————————————————————數模轉換器部分——————————————————————–

     比較器:左側的電阻分壓網絡為右側的四個比較器的反相輸入端提供階梯狀的參考電壓(4V、3V、2V、1V),可調電阻模擬輸入到四個比較器同相端的模擬電壓(0~~5V)。比較器通過比較同相輸入端與反相輸入端電壓的大小,輸出0V(Vp<Vn)或5V(Vp>Vn)給後面的D鎖存器。

 

運放的特性圖

     四路D鎖存器:在時鐘的每一個上升沿,將四個運放輸出的電壓(比較結果)存儲起來並交給後面的編碼器。

     優先級編碼器:對來自鎖存器的四個比較結果進行編碼,並輸出給計算機處理(如果有計算機的話)。

———————————————————————————————————————————————–

 

集成電路簡介

     MCP6004:微芯公司生產的低功耗1MHz帶寬的4路運算放大器,本項目的運算放大器均工作在飽和區。

 

MCP6004實物圖

MCP6004引腳定義

     CD4042B:CMOS四路D鎖存器,在本項目中使用上升沿觸發,時鐘由555定時器提供,用於保存MCP6004輸出的4位數據。

 

CD4042B實物圖

CD4042B引腳定義

CD4042B真值表

     CD4532B: CMOS的8位優先級編碼器,用於對CD4042B鎖存的數據進行編碼。

 

CD4532B實物圖

CD4532B引腳定義

CD4532B真值表

 

測試

 ————————————————————時鐘發生器部分——————————————————————–

     此555時鐘發生電路,實際測試可以產生1Hz、10Hz、100Hz、1KHz、10KHz、100KHz、0.4MHz、0.7MHz的方波信號。實測中,產生的0.4MHz和0.7MHz與設計的1MHz、10MHz存在較大的誤差,可能是電容的問題(這兩個頻率對應所使用的是貼片電容)。

時鐘發生器(正面)

時鐘發生器(反面)

實測產生的最大頻率的波形(Vcc=5V下,Vpp=4.7V)

————————————————————————————————————————————————

————————————————————數模轉換器部分——————————————————————–

     在時鐘為400KHz下,此并行比較型A/D可以正常工作;使用700KHz的時鐘會導致轉換故障。

     主要參數:A/D分辨率為3位(嚴格來說只有2位,可以在不改變架構的情況下通過增加4個比較器拓增至3位)

                   採樣率為400KSPS~~700KSPS。

     功耗:20mA@5V = 100mW (包含時鐘發生器部分)

 

當輸入電壓為2.5V時,輸出的情況(可以看出LED指示“101”,正好是對“1100”編碼的結果)

 

正面(1)

 

正面(2)

 

反面

———————————————————————————————————————————————–

 

聲明

     此教程未經DLHC允許,禁止轉載。所有引用均註明了出處。DLHC保留所有權利。

     由於本人學識有限且整理較為倉促,如有錯誤或不妥,請指正。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案