分類
發燒車訊

Tesla 股價大跌 穆斯克荷包失血 28 億美元

電動車大廠特斯拉(Tesla) 股價重創,不僅投資人損失慘重,公司創辦人穆斯克(Elon Musk)財富更是大失血。根據經濟日報報導,Tesla 股價 2014 年 9 月至今大跌 34%,穆斯克財富也蒸發 28 億美元。   穆斯克表示,Tesla 在中國的銷售低迷,使曾經超搶手的 Tesla 股票價格 14 日大跌 11.56 美元,跌幅約 6%,若和去年 9 月每股 291.42 美元的高價相比,Tesla 股票在這段期間崩跌掉 3 分之 1 的價值。   穆斯克則擁有逾 2,800 萬股的 Tesla 股票,相當流通股總數的 4 分之 1 左右,這意味股價大跌讓穆斯克 28 億美元的財富煙消雲散。不過,雖然上述損失數字很可觀,但穆斯克本身可能不太意外,他在去年 9 月曾警告說,Tesla 的股價「有點太高」,當時投資人不予理會,Tesla 股價還接著上漲至每股 281.19 美元。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

分類
發燒車訊

常見的索引模型淺析

  索引的出現是為了提高數據庫查詢的效率,就像書的目錄一樣。常見的索引模型有哈希表、有序數組、B+樹。

  • 自適應哈希索引(AHI)

  哈希表是一種常見的數據結構,即通過哈希算法計算出一個数字在表中的位置,並將数字存入該表。哈希索引就是通過哈希表來實現的,一般情況下查找時間複雜度為O(1)。InnoDB會監控對錶上各索引頁的查詢,會自動根據訪問的頻率和模式為某些熱點頁建立哈希索引,所以又叫自適應哈希索引,訪問模式一樣指查詢的條件一樣。

  比如我們維護一張身份證信息和用戶姓名的表,需要根據身份證號查詢姓名,哈希索引大概是這樣的:

  哈希索引適合只有等值查詢的場景,例如select * from T where index_col = ‘##’。哈希索引是無序的,如果需要區間查詢,那就要把所有數據掃描一遍。

  • 有序數組索引

  有序數組在等值查詢和區間查詢場景中效率都很高,同樣用上面的表舉例,索引大概是這樣的:

  要查詢某條數據或者區間的時候,使用二分法時間複雜度為O(logN)。但如果需要在中間更新數據時,那麼就要移動後面所有的數據。有序數組索引只適用於靜態存儲引擎,比如保存2019年度學校所有學生信息。

  • B+樹索引

  B+樹是為磁盤或其他直接存取輔助設備設計的一種平衡查找樹。下面是一顆高度為2的B+樹,所有記錄都在恭弘=叶 恭弘子結點上順序存放,恭弘=叶 恭弘子結點通過指針相連。

  B+樹索引就是B+樹在數據庫中的實現,由於B+索引在數據庫中具有高扇出性,在數據庫中B+樹的高度一般為2~4層。查找某一鍵值的行記錄時最多只需要2~4次IO。以InnoDB的一個整数字段索引為例,這顆B+樹大概是1200叉樹,這裏N叉樹的N取決於數據塊的大小。高度為4的時候就可以存1200的3次方個值,大概為17億。考慮到樹根的數據塊總是在內存中,一個10億行的表上一個整数字段的索引,查找一個值最多只需要訪問3次磁盤。

  在InnoDB存儲引擎中,表是根據主鍵順序存放的。根據恭弘=叶 恭弘子結點內容,B+樹索引又分為聚簇索引和輔助索引。

    • 聚簇索引:按照每張表的主鍵構造一顆B+樹,恭弘=叶 恭弘子結點的key是主鍵值, value是該行的其他字段值,聚簇索引的恭弘=叶 恭弘子結點也稱為數據頁。
    • 輔助索引:恭弘=叶 恭弘子結點的內容是主鍵的值。

我們用一個例子來說明上面的概念,創建一張表,在字段k上有索引:

create table T(
id int primary key, 
k int not null, 
name varchar(16),
index (k))engine=InnoDB;

  表中R1~R5的(ID,k)值分別為(100,1)、(200,2)、(300,3)、(500,5)和(600,6),兩顆B+樹如下,可以明顯看到這兩個顆樹的區別。

  使用普通索引查詢時,例如 select * from T where k = 2,需要先搜索k索引樹,得到ID的值為200;再到ID索引搜索一次。這個過程就叫做回表,非主鍵索引查詢需要多搜索一棵樹。

  參考資料:《MySQL實戰45講》

       《MySQL技術內幕:InnoDB存儲引擎》第二版

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

分類
發燒車訊

4. union-find算法

  算法的主題思想:

  1.優秀的算法因為能夠解決實際問題而變得更為重要;

  2.高效算法的代碼也可以很簡單;

  3.理解某個實現的性能特點是一個挑戰;

  4.在解決同一個問題的多種算法之間進行選擇時,科學方法是一種重要的工具;

  5.迭代式改進能夠讓算法的效率越來越高效;

 

   1. 動態連通性

  動態連接:輸入是一對整數對的序列,其中每個整數代表某種類型的對象(或觸點),我們將整數對p q 解釋為意味着p連接到q。我們假設“連接到”是等價關係:

  對稱性:如果p連接到q,則q 連接到p。

  傳遞性:如果p連接到q且q 連接到r,則p連接到r。
  自反性:p與p連接。
  等價關係將對象劃分為多個等價類 或連接的組件。等價類稱為連通分量或分量。
  我們的目標是編寫一個程序,以從序列中過濾掉多餘的對:當程序從輸入中讀取整數對 p q時,只有在該對點不等價的情況下,才應將對寫入到輸出中,並且將p連接到q。如果等價,則程序應忽略整數對pq 並繼續讀取下對。

  

 

  動態連通性問題的應用:

    1.網絡

    2.變量名等價性

    3.數學集合

      在更高的抽象層次上,可以將輸入的所有整數看做屬於不同的數學集合。

   2. 定義問題

  設計算法的第一個任務就是精確地定義問題。

  算法解決的問題越大,它完成任務所需的時間和空間可能越多。我們不可能預先知道這其間的量化關係,通常只會在發現解決問題很困難,或是代價巨大,或是發現算法所提供的信息比原問題所需要的更加有用時修改問題。例如,連通性問題只要求我們的程序能夠判斷出給定的整數對是否相連,但並沒有要求給出兩者之間的通路上的所有連接。這樣的要求更難,並會得出另一組不同的算法。

  為了定義和說明問題,先設計一份API  來封裝基本操作: 初始化,連接兩個觸點,查找某個觸點的分量 ,判斷兩個觸點是否屬於同一分量,分量的數量:

    /// <summary>
    /// 動態連通API
    /// </summary>
    public interface IUnionFind
    {
        /// <summary>
        /// 連接
        /// </summary>
        /// <param name="p"></param>
        /// <param name="q"></param>
        void Union(int p, int q);

        /// <summary>
        /// 查找觸點 p 的分量標識符
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        int Find(int p);

        /// <summary>
        /// 判斷兩個觸點是否處於同一分量
        /// </summary>
        /// <param name="p"></param>
        /// <param name="q"></param>
        /// <returns></returns>
        bool Connected(int p, int q);

        /// <summary>
        /// 連通分量的數量
        /// </summary>
        /// <returns></returns>
        int Count();
    }

  為解決動態連通性問題設計算法的任務轉化為實現這份API:

    1. 定義一種數據結構表示已知的連接;

    2. 基於此數據結構高效的實現API的方法;

  數據結構的性質會直接影響算法的效率。這裏,觸點為索引,觸點和連接分量都是用 int 值表示,將會使用分量中某個觸點的值作為分量的標識符。所以,一開始,每個觸點都是只含有自己的分量,分量標識符為觸點的值。由此,可以初步實現一部分方法:

 

    public class FirstUnionFind:IUnionFind
    {
        private int[] id;//* 分量id 以觸點作為索引
        private int count;//分量數量

        public FirstUnionFind(int n)
        {
            count = n;
            id = new int[n];
            for (var i = 0; i < n; i++)
            {
                id[i] = i; // 第一個 i 作為觸點,第二個 i 作為觸點的值
            }
        }

        public int Count()
        {
            return count;
        }

        public bool Connected(int p, int q)
        {
            return Find(p) == Find(q);
        }

        public int Find(int p)
        {
            
        }

        public void Union(int p, int q)
        {
            
        }
    }

 

  Union-find 的成本模型 是數組的訪問次數(無論讀寫)。

   3. quick-find算法實現

  quick-find 算法是保證當且僅當 id[p] 等於 id[q] 時,p 和 q 是連通的。也就是說,在同一個連通分量中的所有觸點在 id[ ] 中的值全部相等。

  所以 Find 方法只需返回 id[q],Union 方法需要先判斷 Find(p)  是否等於 Find(q) ,若相等直接返回;若不相等,需要將 q 所在的連通分量中所有觸點的 id [ ] 值全部更新為 id[p]。

    public class QuickFindUF: IUnionFind
    {
        private int[] id;//* 分量id 以觸點作為索引
        private int count;//分量數量

        public QuickFindUF(int n)
        {
            count = n;
            id = new int[n];
            for (var i = 0; i < n; i++)
            {
                id[i] = i; // 第一個 i 作為觸點,第二個 i 作為觸點的值
            }
        }

        public int Count()
        {
            return count;
        }

        public bool Connected(int p, int q)
        {
            return Find(p) == Find(q);
        }

        public int Find(int p)
        {
            return id[p];
        }

        public void Union(int p, int q)
        {
            var pID = Find(p);
            var qID = Find(q);

            if (pID == qID)
                return;

            for (var i = 0; i < id.Length; i++)
            {
                if (id[i] == qID)
                    id[i] = pID;
            }
            count--; //連通分量減少
        }

        public void Show()
        {
            for(var i = 0;i<id.Length;i++)
                Console.WriteLine("索引:"+i+",值:"+ id[i] );
            Console.WriteLine("連通分量數量:"+count);
        }
    }

  

  算法分析

  Find() 方法只需訪問一次數組,所以速度很快。但是對於處理大型問題,每對輸入 Union() 方法都需要掃描整個數組。

  每一次歸併兩個分量的 Union() 方法訪問數組的次數在 N+3 到 2N+1 之間。由代碼可知,兩次 Find 操作訪問兩次數組,掃描數組會訪問N次,改變其中一個分量中所有觸點的值需要訪問 1 到 N – 1 次(最好情況是該分量中只有一個觸點,最壞情況是該分量中有 N – 1個觸點),2+N+N-1。

  如果使用quick-find 算法來解決動態連通性問題並且最後只得到一個連通分量,至少需要調用 N-1 次Union() 方法,那麼至少需要 (N+3)(N-1) ~ N^2 次訪問數組,是平方級別的。

 

   4. quick-union算法實現

  quick-union 算法重點提高 union 方法的速度,它也是基於相同的數據結構 — 已觸點為索引的 id[ ] 數組,但是 id[ ] 的值是同一分量中另一觸點的索引(名稱),也可能是自己(根觸點)——這種聯繫成為鏈接。

  在實現 Find() 方法時,從給定觸點,鏈接到另一個觸點,知道到達根觸點,即鏈接指向自己。同時修改 Union() 方法,分別找到 p q 的根觸點,將其中一個根觸點鏈接到根觸點。

public class QuickUnionUF : IUnionFind
    {
        private int[] id;
        private int count;

        public QuickUnionUF(int n)
        {
            count = n;
            id = new int[n];
            for (var i = 0; i < n; i++)
            {
                id[i] = i; // 第一個 i 作為觸點,第二個 i 作為觸點的值
            }
        }

        public int Count()
        {
            return count;
        }

        public bool Connected(int p, int q)
        {
            return Find(p) == Find(q);
        }

        public int Find(int p)
        {
            while (p != id[p])
                p = id[p];
            return p;
        }

        public void Union(int p, int q)
        {
            var pRoot = Find(p);
            var qRoot = Find(q);

            if (pRoot == qRoot)
                return;

            id[pRoot] =qRoot;
count
--; //連通分量減少 } public void Show() { for (var i = 0; i < id.Length; i++) Console.WriteLine("索引:" + i + ",值:" + id[i]); Console.WriteLine("連通分量數量:" + count); } }

  

  森林表示

  id[ ] 數組用父鏈接的形式表示一片森林,用節點表示觸點。無論從任何觸點所對應的節點隨着鏈接查找,最後都將到達含有該節點的根節點。初始化數組之後,每個節點的鏈接都指向自己。

 

  算法分析

  定義:一棵樹的大小是它的節點的數量。樹中一個節點的深度是它到根節點的路徑上鏈接數。樹的高度是它的所有節點中的最大深度。

  quick-union 算法比 quick-find 算法更快,因為它對每對輸入不需要遍歷整個數組。

  分析quick-union 算法的成本比 quick-find 算法的成本要困難,因為quick-union 算法依賴於輸入的特點。在最好的情況下,find() 方法只需訪問一次數組就可以得到一個觸點的分量表示;在最壞情況下,需要 2i+1 次數組訪問(i 時觸點的深度)。由此得出,該算法解決動態連通性問題,在最佳情況下的運行時間是線性級別,最壞情況下的輸入是平方級別。解決了 quick-find 算法中 union() 方法總是線性級別,解決動態連通性問題總是平方級別。

  quick-union 算法中 find() 方法訪問數組的次數為 1(到達根節點只需訪問一次) 加上 給定觸點所對應節點的深度的兩倍(while 循環,一次讀,一次寫)。union() 訪問兩次 find() ,如果兩個觸點不在同一分量還需加一次寫數組。

   假設輸入的整數對是有序的 0-1, 0-2,0-3 等,N-1 對之後N個觸點將全部處於相同的集合之中,且得到的樹的高度為 N-1。由上可知,對於整數對 0-i , find() 訪問數組的次數為 2i + 1,因此,處理 N 對整數對所需的所有訪問數組的總次數為 3+5+7+ ……+(2N+1) ~ n^2

  

  

  5.加權 quick-union 算法實現

  簡單改動就可以避免 quick-union算法 出現最壞情況。quick-union算法 union 方法是隨意將一棵樹連接到另一棵樹,改為總是將小樹連接到大樹,這需要記錄每一棵樹的大小,稱為加權quick-union算法。

  代碼:

    public class WeightedQuickUnionUF: IUnionFind
    {
        int[] sz;//以觸點為索引的 各個根節點對應的分量樹大小
        private int[] id;
        private int count;

        public WeightedQuickUnionUF(int n)
        {
            count = n;
            id = new int[n];
            sz = new int[n];
            for (var i = 0; i < n; i++)
            {
                id[i] = i; // 第一個 i 作為觸點,第二個 i 作為觸點的值
                sz[i] = 1;
            }
        }

        public int Count()
        {
            return count;
        }

        public bool Connected(int p, int q)
        {
            return Find(p) == Find(q);
        }

        public int Find(int p)
        {
            while (p != id[p])
                p = id[p];
            return p;
        }

        public void Union(int p, int q)
        {
            var pRoot = Find(p);
            var qRoot = Find(q);

            if (pRoot == qRoot)
                return;

            if (sz[pRoot] < sz[qRoot])
            {
                id[pRoot] = qRoot;
            }
            else
            {
                id[qRoot] = pRoot;
            }
                
            count--; //連通分量減少
        }

        public void Show()
        {
            for (var i = 0; i < id.Length; i++)
                Console.WriteLine("索引:" + i + ",值:" + id[i]);
            Console.WriteLine("連通分量數量:" + count);
        }
    }

 

  算法分析

  加權 quicj-union 算法最壞的情況:

  

  這種情況,將要被歸併的樹的大小總是相等的(且總是 2 的 冥),都含有 2^n 個節點,高度都正好是 n 。當歸併兩個含有 2^n 個節點的樹時,得到的樹含有 2 ^ n+1 個節點,高度增加到 n+1 。

  節點大小: 1  2  4  8  2^k = N

  高       度: 0  1  2  3  k

  k = logN

  所以加權 quick-union 算法可以保證對數級別的性能。

  對於 N 個觸點,加權 quick-union 算法構造的森林中的任意節點的深度最多為logN。

  對於加權 quick-union 算法 和 N 個觸點,在最壞情況下 find,connected 和 union 方法的成本的增長量級為 logN。

  對於動態連通性問題,加權 quick-union 算法 是三種算法中唯一可以用於解決大型問題的算法。加權 quick-union 算法 處理 N 個觸點和 M 條連接時最多訪問數組 c M logN 次,其中 c 為常數。

  

  三個算法處理一百萬個觸點運行時間對比:

  

  

  三個算法性能特點:

 

  6.最優算法 – 路徑壓縮

  在檢查節點的同時將它們直接連接到根節點。

  實現:為 find 方法添加一個循環,將在路徑上的所有節點都直接鏈接到根節點。完全扁平化的樹。

 

  

  研究各種基礎問題的基本步驟:

  1. 完整而詳細地定義問題,找出解決問題所必須的基本抽象操作並定義一份API。

  2. 簡潔地實現一種初級算法,給出一個精心組織的開發用例並使用實際數據作為輸入。

  3. 當實現所能解決的問題的最大規模達不到期望時決定改進還是放棄。

  4. 逐步改進實現,通過經驗性分析和數學分析驗證改進后的效果。

  5. 用更高層次的抽象表示數據結構或算法來設計更高級的改進版本。

  6. 如果可能盡量為最壞情況下的性能提供保證,但在處理普通數據時也要有良好的性能。

  7.在適當的時候將更細緻的深入研究留給有經驗的研究者並解決下一個問題。

 

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

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

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

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

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

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

※回頭車貨運收費標準

分類
發燒車訊

IOT設備SmartConfig實現

一般情況下,IOT設備(針對wifi設備)在智能化過程中需要連接到家庭路由。但在此之前,需要將wifi信息(通常是ssid和password,即名字和密碼)發給設備,這一步驟被稱為配網。移動設備如Android、iOS等扮演發送wifi信息的角色,簡單來說就是移動應用要與IOT設備建立通信,進而交換數據。針對配網這一步驟,市面上一般有兩種做法:

  • AP連接方式:IOT設備發出AP(Access Point,可理解為路由器,可發出wifi)信息;移動設備STA(Station,可以連接wifi)連接到IOT設備AP,接着就可以發送wifi(家庭路由的wifi)信息給設備了。另外,也可互換角色,及移動設備釋放熱點,IOT設備進行連接。
  • SmartConfig(一鍵配置)方式:不需要建立連接,移動設備將wifi信息(需提前獲取)寫入數據包,組播循環發出此數據包;IOT設備處於監聽所有網絡的模式,接收到UDP包后解析出wifi信息拿去連網。

可以發現,SmartConfig不需建立連接,步驟較少,實現起來也較容易,並且用戶也無需進行過多的操作。本文的IOT設備基於ESP32開發板,解釋原理及實現如何通過Android APP發出UDP包實現SmartConfig。知識點:計算機網絡、UDP、組播、DatagramSocket

一、網絡知識回顧

計算機網絡分層結構如下:

  • 應用層:體系中的最高層。任務是通過應用程序間的交互來完成特定網絡應用。不同的網絡應用對應不同的協議:如HTTPDNSSMTP。其交互的數據單元稱為報文。
  • 運輸層:複雜向兩台主機中進程直接的通信提供通用的數據傳輸服務,使用端口作為向上傳遞的進程標識,主要有TCP和UDP。
  • 網絡層:負責為分組交換網絡上的不同主機提供通信服務,使用IP協議。
  • 網絡接口層:包括數據鏈路層和物理層,傳輸單位分別是幀和比特。

1. IP協議

IP(Internet Protocol)協議是網絡層的主要協議。其版本有IPv4(32位)、IPv6(128位)。與IP協議配套使用的還有地址解析協議(ARP)、網際控制報文協議(ICMP,重要應用即常見的PING,測試連通性)、網際組管理協議(IGMP)。

IP數據報格式,由首部和數據部分兩部分組成:

IP地址分類如下:

1.1 兩級IP地址

IP地址是每一台主機唯一的標識符,由網絡號和主機號組成。A、B、C三類均為單播地址(一對一),D類為多播地址(一對多)。

1.2 三級IP地址

在兩級中新增了子網號字段,也稱為劃分子網。其方法是從主機號借用若干位作為子網號。

子網掩碼:是一個網絡或子網的重要屬性,可通過子網掩碼計算出目的主機所處於哪一個子網。若沒有劃分子網,則使用默認子網掩碼(A類255.0.0.0、B類255.255.0.0、C類255.255.255.0)

1.3 無分類編址

無分類編址(CIDR)也稱為構造超網,使用網絡前綴+主機號規則,並使用斜線標明前綴位數,如:

128.15.34.77/20
1.4 IP多播

多播又稱為組播,提供一對多的通信,大大節約網絡資源。IP數據報中不能寫入某一個IP地址,需寫入多播組的標識符(需要接收的主機與此標識符關聯)。D類地址即為多播組的標識符,所以多播地址(D類)只能作為目的地址。分為本局域網上的硬件多播、互聯網多播兩種。

多播使用到的協議

  • IGMP(網際組管理協議):讓連接在本地局域網上的多播路由器(能夠運行多播協議的路由器)知道本局域網上是否有主機(進程)參加或退出了某個多播組。
  • 多播路由選擇協議:用於多播路由器之間的協同工作,以便讓多播數據報以最小的代價傳輸。

2. UDP協議

運輸層向上面的應用層提供通信服務,通信的端點並不是主機而是主機中進程,使用協議端口號標識進程(如HTTP為80)。UDP協議是運輸層中重要的兩個協議之一。

  • UDP是無連接的
  • UDP使用盡最大努力交付,不保證可靠交付
  • UDP是面向報文的
  • UDP沒有擁塞控制
  • UDP支持一對一,一對多,多對一和多對多
  • UDP首部開銷小

二、Java中的UDP

1. Socket

socket是在應用層和傳輸層之間的一個抽象層,它把TCP/IP層複雜的操作抽象為幾個簡單的接口供應用層調用已實現進程在網絡中通信。簡單來說,socket是一種接口,對傳輸層(TCP/UPD協議)進行了的封裝。

socket通信

  • TCP socket:需建立連接,TCP三次握手,基於流的通信(InputStrea和OutputStream)
  • UDP socket:無需建立連接,基於報文的通信。可以組播的形式發出報文,適合本場景中的配網步驟。

2. Java中的socket

2.1 類解釋

Java為Socket編程封裝了幾個重要的類(均為客戶端-服務端模式):

Socket
實現了一個客戶端socket,作為兩台機器通信的終端,默認採用TCP。connect()方法請求socket連接、getXXXStream()方法獲取輸入/出流、close()關閉流。

ServerSocket
實現了一個服務器的socket,等待客戶端的連接請求。bind()方法綁定一個IP地址和端口、accept()方法監聽並返回一個Socket對象(會阻塞)、close()關閉一個socket

SocketAddress # InetSocketAddress
前者是一個抽象類,提供了一個socket地址,不關心傳輸層協議;後者繼承自前者,表示帶有IP地址和端口號的socket地址。

DatagramSocket
實現了一個發送和接收數據報的socket,使用UDP。send()方法發送一個數據報(DatagramPacket)、receive()方法接收一個數據報(一直阻塞接至收到數據報或超時)、close()方法關閉一個socket。

DatagramPacket
使用DatagramSocket時的數據報載體。

2.2 UDP實例

SmartConfig採用UDP實現,所以在前述知識的基礎下,先編寫一個例子熟悉java udp的使用,首先建立服務端的代碼:

public class UDPServer {
    /**
     * 設置緩衝區的長度
     */
    private static final int BUFFER_SIZE = 255;
    /**
     * 指定端口,客戶端需保持一致
     */
    private static final int PORT = 8089;

    public static void main(String[] args) {
        DatagramSocket datagramSocket = null;
        try {
            datagramSocket = new DatagramSocket(PORT);
            DatagramPacket datagramPacket = new DatagramPacket(new byte[BUFFER_SIZE], BUFFER_SIZE);
            while (true) {
                // 接收數據報,處於阻塞狀態
                datagramSocket.receive(datagramPacket);
                System.out.println("Receive data from client:" + new String(datagramPacket.getData()));
                // 服務器端發出響應信息
                byte[] responseData = "Server response".getBytes();
                datagramPacket.setData(responseData);
                datagramSocket.send(datagramPacket);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (datagramSocket != null) {
                datagramSocket.close();
            }
        }
    }
}

客戶端發出數據報:

public class UDPClient {
    /**
     * 指定端口,與服務端保持一致
     */
    private static final int PORT = 8089;
    /**
     * 超時重發時間
     */
    private static final int TIME_OUT = 2000;
    /**
     * 最大重試次數
     */
    private static final int MAX_RETRY = 3;

    public static void main(String[] args) throws IOException {
        try {
            byte[] sendMsg = "Client msg".getBytes();
            // 創建數據報
            DatagramSocket socket = new DatagramSocket();
            // 設置阻塞超時時間
            socket.setSoTimeout(TIME_OUT);
            // 創建server主機的ip地址(此處使用了本機地址)
            InetAddress inetAddress = InetAddress.getByName("192.168.xxx.xxx");
            // 發送和接收的數據報文
            DatagramPacket sendPacket = new DatagramPacket(sendMsg, sendMsg.length, inetAddress, PORT);
            DatagramPacket receivePacket = new DatagramPacket(new byte[sendMsg.length], sendMsg.length);
            // 數據報文可能丟失,設置重試計數器
            int tryTimes = 0;
            boolean receiveResponse = false;
            // 將數據報文發送出去
            socket.send(sendPacket);
            while (!receiveResponse && (tryTimes < MAX_RETRY)) {
                try {
                    // 阻塞接收數據報文
                    socket.receive(receivePacket);
                    // 檢查返回的數據報文
                    if (!receivePacket.getAddress().equals(inetAddress)) {
                        throw new IOException("Unknown server's data");
                    }
                    receiveResponse = true;
                } catch (InterruptedIOException e) {
                    // 重試
                    tryTimes++;
                    System.out.println("TimeOut, try " + (MAX_RETRY - tryTimes) + " times");
                }
            }
            if (receiveResponse) {
                System.out.println("Receive from server:" + new String(receivePacket.getData()));
            } else {
                System.out.println("No data!");
            }
            socket.close();
        } catch (SocketException e) {
            e.printStackTrace();
        }
    }
}

運行結果:

* 發現客戶端收到的數據被截斷了,這是因為沒有重置接收包的長度,在服務端datagramPacket.setLength()可解決。

三、SmartConfig

根據前面的socket相關應用,基本想到如何實現一鍵配置。在實際應用中,原理一樣,只是增加了組播(這一點需要和IOT設備端共同確定,數據的格式也需協定)。在實現中,需要針對不同IP組播地址發出循環的UDP報文,增加設備端接收到的可能性;同時APP也要開啟服務端程序監聽發出數據報的響應,以此更新UI或進行下一步的數據通信。相關核心代碼如下:

// 對每一個組播地址循環發出報文 
while (!mIsInterrupt && System.currentTimeMillis() - currentTime < mParameter
         .getTimeoutGuideCodeMillisecond()) {
     mSocketClient.sendData(gcBytes2,
             mParameter.getTargetHostname(),
             mParameter.getTargetPort(),
             mParameter.getIntervalGuideCodeMillisecond());
     // 跳出條件,發出UDP報文達到一定時間
     if (System.currentTimeMillis() - startTime > mParameter.getWaitUdpSendingMillisecond()) {
         break;
     }
}

組播地址設置:

public String getTargetHostname() {
    if (mBroadcast) {
        return "255.255.255.255";
    } else {
        int count = __getNextDatagramCount();
        return "234." + (count + 1) + "." + count + "." + count;
    }
}

完整代碼省略(利益相關,代碼匿了^_^),基本思路很簡單。最終的實現是IOT設備收到UDP發出的wifi信息,並以此成功連接wifi,連接服務器,進而綁定賬號。

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

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

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

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

分類
發燒車訊

mybatis緩存之一級緩存(二)

這篇文章介紹下mybatis的一級緩存的生命周期

一級緩存的產生

一級緩存的產生,並不是看mappper的xml文件的select方法,看下面的例子

mapper.xml

    <select id="getById" resultType="entity.TempEntity">
       select * from  temp where id = #{id}
    </select>

test

    @Test
    public  void testSelectAsUpdate() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = build.openSession();
        sqlSession.update("dao.Temp03Dao.getById", 1);
        sqlSession.update("dao.Temp03Dao.getById", 1);
    }

執行結果

2020-06-26 17:33:27,899 DEBUG [dao.Temp03Dao.getById] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 17:33:27,922 DEBUG [dao.Temp03Dao.getById] - ==> Parameters: 1(Integer)
2020-06-26 17:33:27,923 DEBUG [dao.Temp03Dao.getById] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 17:33:27,923 DEBUG [dao.Temp03Dao.getById] - ==> Parameters: 1(Integer)

我們可以看到執行了2次查詢。說明並沒有產生緩存。說明和sqlsession調用的方法是有關係的

只有調用上圖中的方法才會產生一級緩存

一級緩存的銷毀

1.關閉session

這個是根據debug看到的一級緩存的最終結構。下面是整個依賴的類圖

test

 @Test
    public  void test() throws IOException, NoSuchFieldException, IllegalAccessException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = build.openSession();
        TempEntity tempEntity1 = sqlSession.selectOne("dao.Temp03Dao.getById", 1);
        logger.info(tempEntity1);

        Field executorField = sqlSession.getClass().getDeclaredField("executor");
        executorField.setAccessible(true);
        CachingExecutor  cachingExecutor = (CachingExecutor) executorField.get(sqlSession);

        Field declaredField = cachingExecutor.getClass().getDeclaredField("delegate");
        declaredField.setAccessible(true);
        SimpleExecutor simpleExecutor  = (SimpleExecutor) declaredField.get(cachingExecutor);

        Field localCacheField = simpleExecutor.getClass().getSuperclass().getDeclaredField("localCache");
        localCacheField.setAccessible(true);
        PerpetualCache perpetualCache = (PerpetualCache) localCacheField.get(simpleExecutor);

        Field cacheField = perpetualCache.getClass().getDeclaredField("cache");
        cacheField.setAccessible(true);
        Map<Object,Object> map= (Map<Object, Object>) cacheField.get(perpetualCache);
        logger.info("緩存關閉前");
        for (Map.Entry<Object,Object> objectObjectEntry:map.entrySet()){
            logger.info(objectObjectEntry.getKey() + "===" + objectObjectEntry.getValue());
        }
        sqlSession.close();
        logger.info("緩存關閉后");

        for (Map.Entry<Object,Object> objectObjectEntry:map.entrySet()){
            logger.info(objectObjectEntry.getKey() + "=" + objectObjectEntry.getValue());
        }
    }

運行結果

2020-06-26 17:38:52,777 DEBUG [dao.Temp03Dao.getById] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 17:38:52,801 DEBUG [dao.Temp03Dao.getById] - ==> Parameters: 1(Integer)
2020-06-26 17:38:52,824 DEBUG [dao.Temp03Dao.getById] - <==      Total: 1
2020-06-26 17:38:52,824 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 17:38:52,825 INFO [TempTest] - 緩存關閉前
2020-06-26 17:38:52,826 INFO [TempTest] - -1654591322:461730790:dao.Temp03Dao.getById:0:2147483647:select * from  temp where id = ?:1:dev===[TempEntity{id=1, value1='11111', value2='aaaaa'}]
2020-06-26 17:38:52,827 INFO [TempTest] - 緩存關閉后

可以看到session關閉后,緩存就不存在了

2.Commit提交

test

    @Test
    public  void testCommit() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = build.openSession();
        TempEntity tempEntity1 = sqlSession.selectOne("dao.Temp03Dao.getById", 1);
        logger.info(tempEntity1);
        sqlSession.commit();
        TempEntity tempEntity2 = sqlSession.selectOne("dao.Temp03Dao.getById", 1);
        logger.info(tempEntity2);
        logger.info(tempEntity1 == tempEntity2);

    }

運行結果

2020-06-26 17:40:40,821 DEBUG [dao.Temp03Dao.getById] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 17:40:40,846 DEBUG [dao.Temp03Dao.getById] - ==> Parameters: 1(Integer)
2020-06-26 17:40:40,862 DEBUG [dao.Temp03Dao.getById] - <==      Total: 1
2020-06-26 17:40:40,862 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 17:40:40,863 DEBUG [dao.Temp03Dao.getById] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 17:40:40,863 DEBUG [dao.Temp03Dao.getById] - ==> Parameters: 1(Integer)
2020-06-26 17:40:40,864 DEBUG [dao.Temp03Dao.getById] - <==      Total: 1
2020-06-26 17:40:40,864 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 17:40:40,864 INFO [TempTest] - false

說明sqlSession.commit時會清空緩存

3.Rollback

test

    @Test
    public  void testRollback() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = build.openSession();
        TempEntity tempEntity1 = sqlSession.selectOne("dao.Temp03Dao.getById", 1);
        logger.info(tempEntity1);
        sqlSession.rollback();
        TempEntity tempEntity2 = sqlSession.selectOne("dao.Temp03Dao.getById", 1);
        logger.info(tempEntity2);
        logger.info(tempEntity1 == tempEntity2);

    }

執行結果

2020-06-26 17:42:23,793 DEBUG [dao.Temp03Dao.getById] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 17:42:23,833 DEBUG [dao.Temp03Dao.getById] - ==> Parameters: 1(Integer)
2020-06-26 17:42:23,843 DEBUG [dao.Temp03Dao.getById] - <==      Total: 1
2020-06-26 17:42:23,843 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 17:42:23,844 DEBUG [dao.Temp03Dao.getById] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 17:42:23,844 DEBUG [dao.Temp03Dao.getById] - ==> Parameters: 1(Integer)
2020-06-26 17:42:23,845 DEBUG [dao.Temp03Dao.getById] - <==      Total: 1
2020-06-26 17:42:23,845 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 17:42:23,845 INFO [TempTest] - false

sqlSession.rollback()也會清空緩存

4.update更新

這裡是在第一次查詢后,緊接着進行update操作。這裏與表無關。就是操作其它表,也會清空緩存。

test

    @Test
    public  void testForUpdate() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = build.openSession();
        TempEntity tempEntity1 = sqlSession.selectOne("dao.Temp03Dao.getById", 1);
        logger.info(tempEntity1);
        sqlSession.update("dao.Temp03Dao.updateById", 1);
        TempEntity tempEntity2 = sqlSession.selectOne("dao.Temp03Dao.getById", 1);
        logger.info(tempEntity2);
        logger.info(tempEntity1 == tempEntity2);

    }

運行結果

2020-06-26 17:45:43,997 DEBUG [dao.Temp03Dao.getById] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 17:45:44,034 DEBUG [dao.Temp03Dao.getById] - ==> Parameters: 1(Integer)
2020-06-26 17:45:44,048 DEBUG [dao.Temp03Dao.getById] - <==      Total: 1
2020-06-26 17:45:44,049 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 17:45:44,049 DEBUG [dao.Temp03Dao.updateById] - ==>  Preparing: update temp set value1 = 'ffffff' where id = ? 
2020-06-26 17:45:44,049 DEBUG [dao.Temp03Dao.updateById] - ==> Parameters: 1(Integer)
2020-06-26 17:45:44,050 DEBUG [dao.Temp03Dao.updateById] - <==    Updates: 1
2020-06-26 17:45:44,051 DEBUG [dao.Temp03Dao.getById] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 17:45:44,051 DEBUG [dao.Temp03Dao.getById] - ==> Parameters: 1(Integer)
2020-06-26 17:45:44,052 DEBUG [dao.Temp03Dao.getById] - <==      Total: 1
2020-06-26 17:45:44,053 INFO [TempTest] - TempEntity{id=1, value1='ffffff', value2='aaaaa'}
2020-06-26 17:45:44,053 INFO [TempTest] - false

這裏還是在一個session會話中。記得之前有人給我說只要在一個session會話中,執行update不會清空緩存。這裏的代碼就證明了

5.clearCache 主動清除

test

    @Test
    public  void testClearCatch() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = build.openSession();
        TempEntity tempEntity1 = sqlSession.selectOne("dao.Temp03Dao.getById", 1);
        logger.info(tempEntity1);
        sqlSession.clearCache();
        TempEntity tempEntity2 = sqlSession.selectOne("dao.Temp03Dao.getById", 1);
        logger.info(tempEntity2);
        logger.info(tempEntity1 == tempEntity2);

    }

運行結果

2020-06-26 17:48:42,085 DEBUG [dao.Temp03Dao.getById] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 17:48:42,110 DEBUG [dao.Temp03Dao.getById] - ==> Parameters: 1(Integer)
2020-06-26 17:48:42,124 DEBUG [dao.Temp03Dao.getById] - <==      Total: 1
2020-06-26 17:48:42,124 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 17:48:42,125 DEBUG [dao.Temp03Dao.getById] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 17:48:42,125 DEBUG [dao.Temp03Dao.getById] - ==> Parameters: 1(Integer)
2020-06-26 17:48:42,126 DEBUG [dao.Temp03Dao.getById] - <==      Total: 1
2020-06-26 17:48:42,126 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 17:48:42,126 INFO [TempTest] - false

一級緩存 臟讀問題

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

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

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

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

分類
發燒車訊

十、深度優先 && 廣度優先

原文地址

一、什麼是“搜索”算法?

  • 算法是作用於具體數據結構之上的,深度優先搜索算法和廣度優先搜索算法都是基於“圖”這種數據結構的。
  • 因為圖這種數據結構的表達能力很強,大部分涉及搜索的場景都可以抽象成“圖”。
  • 圖上的搜索算法,最直接的理解就是,在圖中找出從一個頂點出發,到另一個頂點的路徑。
  • 具體方法有很多,兩種最簡單、最“暴力”的方法為深度優先、廣度優先搜索,還有A、 IDA等啟髮式搜索算法。
  • 圖有兩種主要存儲方法,鄰接表和鄰接矩陣。
  • 以無向圖,採用鄰接表存儲為例:
public class Graph {
    // 頂點的個數
    private int v;
    // 每個頂點後面有個鏈表
    private LinkedList<Integer>[] adj;

    public Graph(int v) {
        this.v = v;
        adj = new LinkedList[v];
        for (int i = 0; i < v; i++) {
            adj[i] = new LinkedList<>();
        }
    }

    /**
     * 添加邊
     * @param s 頂點
     * @param t 頂點
     */
    public void addEdge(int s,int t){
        // 無向圖一條邊存兩次(聯想微信好友)
        adj[s].add(t);
        adj[t].add(s);
    }
}

二、廣度優先搜索(BFS)

  • 廣度優先搜索(Breadth-First-Search),簡稱為 BFS。
  • 它是一種“地毯式”層層推進的搜索策略,即先查找離起始頂點最近的,然後是次近的,依次往外搜索

2.1、實現過程

/**
 * 圖的廣度優先搜索,搜索一條從 s 到 t 的路徑。
 * 這樣求得的路徑就是從 s 到 t 的最短路徑。
 *
 * @param s 起始頂點
 * @param t 終止頂點
 */
public void bfs(int s, int t) {
    if (s == t) {
        return;
    }
    // visited 記錄已經被訪問的頂點,避免頂點被重複訪問。如果頂點 q 被訪問,那相應的visited[q]會被設置為true。
    boolean[] visited = new boolean[v];
    visited[s] = true;
    // queue 是一個隊列,用來存儲已經被訪問、但相連的頂點還沒有被訪問的頂點。因為廣度優先搜索是逐層訪問的,只有把第k層的頂點都訪問完成之後,才能訪問第k+1層的頂點。
    // 當訪問到第k層的頂點的時候,需要把第k層的頂點記錄下來,稍後才能通過第k層的頂點來找第k+1層的頂點。
    // 所以,用這個隊列來實現記錄的功能。
    Queue<Integer> queue = new LinkedList<>();
    queue.add(s);
    // prev 用來記錄搜索路徑。當從頂點s開始,廣度優先搜索到頂點t后,prev數組中存儲的就是搜索的路徑。
    // 不過,這個路徑是反向存儲的。prev[w]存儲的是,頂點w是從哪個前驅頂點遍歷過來的。
    // 比如,通過頂點2的鄰接表訪問到頂點3,那prev[3]就等於2。為了正向打印出路徑,需要遞歸地來打印,就是print()函數的實現方式。
    int[] prev = Arrays.stream(new int[v]).map(f -> -1).toArray();

    while (queue.size() != 0) {
        int w = queue.poll();
        LinkedList<Integer> wLinked = adj[w]; // 表示:鄰接表存儲時頂點為w,所對應的鏈表
        for (int i = 0; i < wLinked.size(); ++i) {
            int q = wLinked.get(i);
            // 判斷頂點 q 是否被訪問
            if (!visited[q]) {
                // 未被訪問
                prev[q] = w;
                if (q == t) {
                    print(prev, s, t);
                    return;
                }
                visited[q] = true;
                queue.add(q);
            }
        }
    }
}

// 遞歸打印s->t的路徑
private void print(int[] prev, int s, int t) {
    if (prev[t] != -1 && t != s) {
        print(prev, s, prev[t]);
    }
    System.out.print(t + " ");
}

原理如下:

2.2、複雜度分析

  • 最壞情況下,終止頂點 t 離起始頂點 s 很遠,需要遍歷完整個圖才能找到。
  • 這個時候,每個頂點都要進出一遍隊列,每個邊也都會被訪問一次,所以,廣度優先搜索的時間複雜度是 O(V+E)
  • 其中,V 表示頂點的個數,E 表示邊的個數。
  • 對於一個連通圖來說,也就是說一個圖中的所有頂點都是連通的,E肯定要大於等於 V-1,所以,廣度優先搜索的時間複雜度也可以簡寫為 O(E)。
  • 廣度優先搜索的空間消耗主要在幾個輔助變量 visited 數組、queue 隊列、prev 數組上。
  • 這三個存儲空間的大小都不會超過頂點的個數,所以空間複雜度是 O(V)

三、深度優先搜索(DFS)

  • 深度優先搜索(Depth-First-Search),簡稱DFS。
  • 最直觀的例子就是“走迷宮,假設站在迷宮的某個岔路口,然後想找到出口。
  • 隨意選擇一個岔路口來走,走着走着發現走不通的時候,就回退到上一個岔路口,重新選擇一條路繼續走,直到最終找到出口。這種走法就是一種深度優先搜索策略。
  • 如下圖所示,在圖中應用深度優先搜索,來找某個頂點到另一個頂點的路徑。
  • 搜索的起始頂點是 s,終止頂點是 t,在圖中尋找一條從頂點 s 到頂點 t 的路徑。
  • 用深度遞歸算法,把整個搜索的路徑標記出來了。實線箭頭表示遍歷,虛線箭頭表示回退。
  • 從圖中可以看出,深度優先搜索找出來的路徑,並不是頂點 s 到頂點 t 的最短路徑。

3.1、實現過程

// 全局變量或者類成員變量,標記是否找到終點 t
boolean found = false;

/**
 * 深度優先搜索
 *
 * @param s 起始頂點
 * @param t 終止頂點
 */
public void dfs(int s, int t) {
    found = false;
    // 標記頂點是否被訪問
    boolean[] visited = new boolean[v];
    // prev 用來記錄搜索路徑,prev[w] = a 表示 w 頂點的上一級節點為 a
    int[] prev = Arrays.stream(new int[v])
            .map(f -> -1).toArray();

    recurDfs(s, t, visited, prev);
    print(prev, s, t);
}

private void recurDfs(int w, int t, boolean[] visited, int[] prev) {
    if (found == true) {
        return;
    }
    visited[w] = true;
    if (w == t) {
        found = true;
        return;
    }
    LinkedList<Integer> wLinked = adj[w];
    for (int i = 0; i < wLinked.size(); ++i) {
        int q = wLinked.get(i);
        if (!visited[q]) {
            prev[q] = w;
            recurDfs(q, t, visited, prev);
        }
    }
}

3.2、複雜度分析

  • 深度搜索中每條邊最多會被訪問兩次,一次是遍歷,一次是回退。
  • 所以,深度優先搜索算法的時間複雜度是 O(E), E 表示邊的個數。
  • 深度優先搜索算法的消耗內存主要是 visited、 prev 數組和遞歸調用棧。
  • visited、 prev 數組的大小跟頂點的個數V成正比,遞歸調用棧的最大深度不會超過頂點的個數,所以總的空間複雜度就是 O(V)

四,兩者對比

  • 廣度優先搜索和深度優先搜索是圖上的兩種最常用、最基本的搜索算法,比起其他高級的搜索算法,比如A、 IDA等,要簡單粗暴,沒有什麼優化,所以,也被
    叫作暴力搜索算法。
  • 所以,這兩種搜索算法僅適用於狀態空間不大,也就是說圖不大的搜索。
  • 廣度優先搜索,通俗的理解就是,地毯式層層推進,從起始頂點開始,依次往外遍歷。
  • 廣度優先搜索需要藉助隊列來實現,遍歷得到的路徑就是,起始頂點到終止頂點的最短路徑。
  • 深度優先搜索用的是回溯思想,非常適合用遞歸實現。換種說法,深度優先搜索是藉助棧來實現的。
  • 在執行效率方面,深度優先和廣度優先搜索的時間複雜度都是 O(E),空間複雜度是 O(V)。

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

【其他文章推薦】

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

※超省錢租車方案

分類
發燒車訊

springboot的jar為何能獨立運行

歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos
內容:所有原創文章分類匯總及配套源碼,涉及Java、Docker、Kubernetes、DevOPS等;

能獨立運行的jar文件

在開發springboot應用時,通過java -jar命令啟動應用是常用的方式,今天就來一起了解這個簡單操作背後的技術;

開發demo

開發一個springboot應用作為本次研究的對象,對應的版本信息如下:

  • JDK:1.8.0_211
  • springboot:2.3.1.RELEASE
  • maven:3.6.0

接下來開發springboot應用,這個應用異常簡單:

  1. springboot應用名為springbootstarterdemo,pom.xml文件內容:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.bolingcavalry</groupId>
    <artifactId>springbootstarterdemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springbootstarterdemo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. 只有一個java類,裏面有個http接口:
package com.bolingcavalry.springbootstarterdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

@SpringBootApplication
@RestController
public class SpringbootstarterdemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootstarterdemoApplication.class, args);
    }
    @RequestMapping(value = "/hello")
    public String hello(){
        return "hello " + new Date();
    }
}
  1. 編碼完成,在pom.xml所在目錄執行命令
mvn clean package -U -DskipTests
  1. 構建成功后,在target目錄下得到文件springbootstarterdemo-0.0.1-SNAPSHOT.jar
  2. 就是這個springbootstarterdemo-0.0.1-SNAPSHOT.jar,此時執行java -jar springbootstarterdemo-0.0.1-SNAPSHOT.jar就能啟動應用,如下圖:

接下來就用這個springbootstarterdemo-0.0.1-SNAPSHOT.jar來分析jar文件能夠獨立啟動的原因;

java -jar做了什麼

  • 先要弄清楚java -jar命令做了什麼,在oracle官網找到了該命令的描述:

    If the -jar option is specified, its argument is the name of the JAR file containing class and resource files for the application. The startup class must be indicated by the Main-Class manifest header in its source code.

  • 再次秀出我蹩腳的英文翻譯:

  1. 使用-jar參數時,後面的參數是的jar文件名(本例中是springbootstarterdemo-0.0.1-SNAPSHOT.jar);
  2. 該jar文件中包含的是class和資源文件;
  3. 在manifest文件中有Main-Class的定義;
  4. Main-Class的源碼中指定了整個應用的啟動類;(in its source code)
  • 小結一下:
    java -jar會去找jar中的manifest文件,在那裡面找到真正的啟動類;

探查springbootstarterdemo-0.0.1-SNAPSHOT.jar

  1. springbootstarterdemo-0.0.1-SNAPSHOT.jar是前面的springboot工程的構建結果,是個壓縮包,用常見的壓縮工具就能解壓,我這裏的環境是MacBook Pro,用unzip即可解壓;

  2. 解壓後有很多內容,我們先關注manifest相關的,下圖紅框中就是manifest文件:

  3. 打開上圖紅框中的文件,內容如下:

Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: springbootstarterdemo
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.bolingcavalry.springbootstarterdemo.Springbootstarter
 demoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.1.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher
  1. 在上述內容可見Main-Class的值org.springframework.boot.loader.JarLauncher,這個和前面的java官方文檔對應上了,正是這個JarLauncher類的代碼中指定了真正的啟動類;

疑惑出現

  1. 在MANIFEST.MF文件中有這麼一行內容:
Start-Class: com.bolingcavalry.springbootstarterdemo.Springbootstarter
 demoApplication
  1. 前面的java官方文檔中,只提到過Main-Class ,並沒有提到Start-Class
  2. Start-Class的值是SpringbootstarterdemoApplication,這是我們的java代碼中的唯一類,也只真正的應用啟動類;
  3. 所以問題就來了:理論上看,執行java -jar命令時JarLauncher類會被執行,但實際上是SpringbootstarterdemoApplication被執行了,這其中發生了什麼呢?

猜測

動手之前先猜一下,個人覺得原因應該如下:

  1. java -jar命令會啟動JarLauncher;
  2. Start-Class是給JarLauncher用的;
  3. JarLauncher根據Start-Class找到了SpringbootstarterdemoApplication,然後執行它;

分析JarLauncher

  1. 先下載SpringBoot源碼,我下載的是2.3.1版本,地址:https://github.com/spring-projects/spring-boot/releases/tag/v2.3.1.RELEASE

  2. JarLauncher所在的工程是spring-boot-loader,先弄明白JarLauncher的繼承關係,如下圖,可見JarLauncher繼承自ExecutableArchiveLauncher,而ExecutableArchiveLauncher的父類Launcher位於最頂層,是個抽象類:

  3. java -jar執行的是JarLauncher的main方法,如下,會實例化一個JarLauncher對象,然後執行其launch方法,並且將所有入參都帶入:

public static void main(String[] args) throws Exception {
	new JarLauncher().launch(args);
}
  1. 上面的launch方法在父類Launcher中:
protected void launch(String[] args) throws Exception {
    // 將jar解壓后運行的方式叫做exploded mode
    // 如果是exploded mode,就不能支持通過URL加載jar
    // 如果不是exploded mode,就可以通過URL加載jar
	if (!isExploded()) {
	    // 如果允許通過URL加載jar,就在此註冊對應的處理類
		JarFile.registerUrlProtocolHandler();
	}
	// 創建classLoader
	ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
	// jarmode是創建docker鏡像時用到的參數,使用該參數是為了生成帶有多個layer信息的鏡像
	// 這裏暫時不關注jarmode
	String jarMode = System.getProperty("jarmode");
	//如果沒有jarmode參數,launchClass的值就來自getMainClass()返回
	String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
	launch(args, launchClass, classLoader);
}
  1. 可見要重點關注的是getMainClass()方法,在看這個方法之前,我們先去關注一個重要的成員變量archive,是JarLauncher的父類ExecutableArchiveLauncher的archive,如下可見,該變量又來自方法createArchive:
public ExecutableArchiveLauncher() {
		try {
			this.archive = createArchive();
			this.classPathIndex = getClassPathIndex(this.archive);
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}
  1. 方法來自Launcher.createArchive,如下所示,可見成員變量archive實際上是個JarFileArchive對象:
protected final Archive createArchive() throws Exception {
		ProtectionDomain protectionDomain = getClass().getProtectionDomain();
		CodeSource codeSource = protectionDomain.getCodeSource();
		URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
		String path = (location != null) ? location.getSchemeSpecificPart() : null;
		if (path == null) {
			throw new IllegalStateException("Unable to determine code source archive");
		}
		File root = new File(path);
		if (!root.exists()) {
			throw new IllegalStateException("Unable to determine code source archive from " + root);
		}
		return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
	}
  1. 現在回到getMainClass()方法,可見his.archive.getManifest方法返回的是META-INF/MANIFEST.MF文件的內容,然後getValue(START_CLASS_ATTRIBUTE)方法實際上就是從META-INF/MANIFEST.MF中取得了Start-Class的屬性:
@Override
	protected String getMainClass() throws Exception {
	    // 對應的是JarFileArchive.getManifest方法,
	    // 進去后發現對應的就是JarFile.getManifest方法,
	    // JarFile.getManifest對應的就是META-INF/MANIFEST.MF文件的內容
		Manifest manifest = this.archive.getManifest();
		String mainClass = null;
		if (manifest != null) {
		    // 對應的是META-INF/MANIFEST.MF文件中的Start-Class的屬性
			mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
		}
		if (mainClass == null) {
			throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
		}
		return mainClass;
	}
  1. 從上述分析可知:getMainClass()方法返回的是META-INF/MANIFEST.MF中取得了Start-Class的屬性com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication,再次回到launch方法中,可見最終運行的代碼是launch(args, launchClass, classLoader),它的launchClass參數就是com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication:
protected void launch(String[] args) throws Exception {
		if (!isExploded()) {
			JarFile.registerUrlProtocolHandler();
		}
		ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
		String jarMode = System.getProperty("jarmode");
		// 這裏的launchClass等於"com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication"
		String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
		// 這裏就是啟動SpringbootstarterdemoApplication的地方
		launch(args, launchClass, classLoader);
	}
  1. 展開launch(args, launchClass, classLoader),最終查到了MainMethodRunner類:
public class MainMethodRunner {

	private final String mainClassName;

	private final String[] args;

	/**
	 * Create a new {@link MainMethodRunner} instance.
	 * @param mainClass the main class
	 * @param args incoming arguments
	 */
	public MainMethodRunner(String mainClass, String[] args) {
	    // mainClassName被賦值為"com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication"
		this.mainClassName = mainClass;
		this.args = (args != null) ? args.clone() : null;
	}

	public void run() throws Exception {
	    // 得到SpringbootstarterdemoApplication的Class對象
		Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
		// 得到SpringbootstarterdemoApplication的main方法對象
		Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
		mainMethod.setAccessible(true);
		// 通過反射執行main方法
		mainMethod.invoke(null, new Object[] { this.args });
	}
}

終於,真相大白了;

小結

最後盡可能簡短做個小結,先看jar是如何產生的,如下圖,maven插件生成的jar文件中,有常見的class、jar,也有符合java規範的MANIFEST.MF文件,並且,還在MANIFEST.MF文件中額外生成了名為Start-Class的配置,這裏面是我們編寫的應用啟動類SpringbootstarterdemoApplication

啟動類是JarLauncher,它是如何與MANIFEST.MF文件關聯的呢?從下圖可以看出,最終是通過JarFile類的成員變量manifestSupplier關聯上的:

再來看看關鍵代碼的執行情況,如下圖:

至此,SpringBoot的jar獨立運行的基本原理已經清楚,探究的過程中,除了熟悉關鍵代碼流程,還對jar中的文件有了更多了解,如果您正在學習SpringBoot,希望本文能給您一些參考;

官方文檔

  1. 最後附上SpringBoot官方文檔,可以看到Start-Class描述信息:

  2. 上述文檔明確提到:Start-Class定義的是實際的啟動類,此時的您應該對一切都瞭然於胸,產生本該如此的感慨;

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

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

分類
發燒車訊

中興攜手國家電網組合資公司 建成都無線汽車充電站

中興子公司中興新能源汽車與國家電網四川省電力公司達成協議,將在成都組建合資公司。此合資公司在傳統集中式充電模式的基礎上分佈式建設無線汽車充電站,第一批支援無線充電的社區巴士同日會在成都正式投入運營。   除社區巴士外,中興還將逐步推廣支持無線充電的大型公車專案,而房車的無線充電技術亦已成熟,用戶如有條件和需要,可對房車和停車廠進行改造,即可享有無線充電功能。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

分類
發燒車訊

新能源汽車入北京 改為備案制

在中國的全球新能源汽車大會上,北京市科委新能源與新材料處處長許心超透露,北京市新能源汽車準入方式有所調整,由先前的新能源汽車目錄方式改為備案方式,車型只要備案即可在北京銷售、上牌。   先前規定,按照《北京市示範應用新能源小客車生企業及品目錄》,北京市共有 7 款新能源汽車可在備案之後直接銷售、上牌。而這次許心超表示,按照中央的要求,準入方式也在改變,主要採取備案制,只要備案了就可銷售,政府則加強事後監管。
這意味著所有中國純電動汽車都可以進京銷售、上牌,享受財政補貼,而不再像以前必須要先進入嚴格的北京市新能源汽車目錄。   另一方面,為了讓純電動汽車的使用者迅速找到充電樁,相關部門也在進行改進,一是和百度合作,標注所有充電樁的電子地圖將很快上線;二是印製北京充電樁地圖;三是辦充電體驗周活動,在 2 月 2 日至 6 日,將分 2 次聯合所有北京的汽車企業,做充電實際體驗。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

分類
發燒車訊

巴西浮油事件3個月 污染擴至座頭鯨保育區

摘錄自2019年11月3日中央通訊社巴西報導

巴西東北巴伊亞州(Bahia)外海阿布羅略斯(Abrolhos)群島四周,有著巴西最豐富的海洋多樣性。群島中一座國家公園有著罕見珊瑚群、海鳥以及座頭鯨繁殖地。巴西海軍3日通報,困擾巴西海岸3個月的浮油污染已飄到座頭鯨保育區,嚴重污染生物多樣性豐富的群島地區,背後的環境及經濟破壞目前還無法量化。海軍在聲明中表示,海軍船艦清理的油污有部分在海洋中,有部分則在海灘上。

這些油污3個月前開始出現在巴西東北部外海,巴西著名2000公里海岸線上的200多個海灘都遭油漬污染。船員及志工已在海灘上清出數噸重的原油。巴西官員說,目前還無法量化油漬造成的環境及經濟破壞。

巴西政府2日點名一艘掛有希臘旗幟的油輪布普林納號(Bouboulina)是主要嫌疑犯,說這艘油輪當時從委內瑞拉載石油前往新加坡。巴西國家太空署(Inpe)2日表示,海中仍可能有油污在洋流推動下,南下至巴西東南的聖靈州(Espiritu Santo)及里約熱內盧州(Rio De Janeiro)。

清理污染的志工。照片來源: Kleber from Burgos / WWF-Brasil

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心