【故障公告】docker swarm 集群問題造成新版博客後台故障

非常抱歉,今天下午 16:55~17:05 左右,由於 docker swarm 集群的突發不穩定問題造成(目前處於灰度發布階段)無法正常使用,由此給您帶來麻煩,請您諒解。

出故障期時,新版博客後台的2個容器都無法正常啟動。

AME                NODE                DESIRED STATE       CURRENT STATE 
i_web.1             prod-swarm-w3       Running             Assigned 5 minutes ago                       
i_web.2             prod-swarm-w4       Running             Assigned 2 hours ago       

發現問題后,我們進行了刪除 stack 並重新部署的操作。

docker stack rm i
./deploy-production.sh 2.0.6
NAME                NODE                DESIRED STATE       CURRENT STATE
i_web.1             prod-swarm-w3       Running             Assigned 42 seconds ago                       
i_web.2             prod-swarm-w7       Running             Starting 42 seconds ago

重新部署后發現 prod-swarm-w7  節點上的容器可以正常啟動,而 prod-swarm-w3 節點上的容器問題依舊,由此確認是 prod-swarm-w3 節點出了問題,於是立即卸載該節點。

docker node update --availability drain prod-swarm-w3

卸載后,新版博客後台很快恢復了正常。

我們已經決定用 k8s 取代 docker swarm ,但目前 k8s 集群還沒部署好,在這即將與 docker swarm 說 88 的時刻,又被 docker swarm 坑了一次,都怪我們當時貪圖省事,選對了集裝箱(docker 容器)卻上錯了船(docker swarm),我們會深刻吸取這次上錯船的教訓。

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

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

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

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

坑~夏令時冬令時引發的時間換算問題

 

起因

最近接觸到一些國外的項目,由於國內外有時差這個東西,對於某些基礎數據存到數據庫的時候需要記錄時間,為了方便,這裏採用了時間戳(int或者timestamp)記錄。由於時間戳全球都是一樣的,需要的時候根據時區進行轉換就能夠拿到當地的時間。

嗯~ o(* ̄▽ ̄*)o,這樣看起來確實沒什麼毛病。眾所周知,一天有24小時,換算成秒就是:24*60*60=86400秒。

然而,我在某次使用 MySql 的 FROM_UNIXTIME 發現一個問題,兩個時間相差86400秒,但是格式化之後卻不是相差一天!!!

假設北京時間2019年11月25日 12:00:00,對應的時間戳是:1574654400,照理說這個時間戳加上一天86400秒,理論上就是北京時間2019年11月26日 12:00:00,事實上確實如此,國內的話這麼算確實沒什麼問題,但是如果是國外時區的話,那可能會出問題。

由於國外部分國家有夏令時和冬令時之分(具體下面會細說),直接加上86400秒可能會有問題。

感興趣的可以拿1572764400(太平洋時間2019-11-03 00:00:00,單位:秒)這個時間戳驗證下

拿代碼演示下:

PHP:

<?php

echo "PST時區的時間\n";
date_default_timezone_set('PST8PDT');
echo date('Y-m-d H:i:s',1572764400);
echo "\n";
echo date('Y-m-d H:i:s',1572764400+86400);
echo "\n";

//換個時區
echo "換成上海時區看看\n";
date_default_timezone_set('Asia/Shanghai');
echo date('Y-m-d H:i:s',1572764400);
echo "\n";
echo date('Y-m-d H:i:s',1572764400+86400);
echo "\n";

運行結果:

PST時區的時間
2019-11-03 00:00:00
2019-11-03 23:00:00
換成上海時區看看
2019-11-03 15:00:00
2019-11-04 15:00:00

明明是同一個時間戳,都是加上86400(一天),為什麼在上海這個時區是第二天,而在PST(美國太平洋時區)只加了23小時?神不神奇!意不意外!

為了弄清楚這個問題,首先得先了解下什麼是夏令時,什麼是冬令時

 

夏令時

夏令時,表示為了節約能源,人為規定時間的意思。也叫夏時制,夏時令(Daylight Saving Time:DST),又稱“日光節約時制”和“夏令時間”,在這一制度實行期間所採用的統一時間稱為“夏令時間”。

一般在天亮早的夏季人為將時間調快一小時,可以使人早起早睡,減少照明量,以充分利用光照資源,從而節約照明用電。各個採納夏時制的國家具體規定不同。目前全世界有近110個國家每年要實行夏令時。[1]

 

冬令時

有夏令時就會有冬令時。高緯度和中緯度的許多國家在夏季到來前,把時針撥快一小時,新的時間就是夏令時,到下半季秋季來臨前,再把時針撥回一小時,即形成冬令時。 [2] 

 

夏令時和冬令時的影響

拿美國來說,美國各個地區的時間都不同,不像中國一樣統一使用北京時間,美國一般以三月份第二個周日凌晨兩點當成夏季的開始,十一月份第一個周日的凌晨兩點當成冬季的開始。

所以在每年的三月份第二個周日凌晨兩點過後,時間就會往前調快一個小時;同理,十一月份第一個周日把這一個小時調回來。

你也可以理解成美國那邊,一年裡面有一天只有23小時(夏天開始那一天),有一天有25小時(冬天開始那一天),其他時間每天都是24小時。

所以你會發現,夏天的時候,中國的北京時間(東八區)與美國太平洋時區(西八區)的時差是15小時,而到了冬天卻變成16小時

 

解決方案

回到開頭那個問題,如果我們想直接算第二天,直接加上86400(一天)可能在其他國家就會有我上面那個夏令時和冬令時時間換算的問題,要如何避免呢?首先能夠確定的是,直接加上86400是不可取的,如果加上一天能否行得通

PHP:

<?php

echo "PST時區的時間\n";
date_default_timezone_set('PST8PDT');
echo date('Y-m-d H:i:s',1572764400);
echo "\n";
echo date('Y-m-d H:i:s',1572764400+86400);
echo "\n";

echo "--------------------------\n";
echo date('Y-m-d H:i:s',1572764400);
echo "\n";
echo date('Y-m-d H:i:s',strtotime('+1 day',1572764400));
echo "\n";

運行結果:

PST時區的時間
2019-11-03 00:00:00
2019-11-03 23:00:00
--------------------------
2019-11-03 00:00:00
2019-11-04 00:00:00

可以看出,不直接加上86400,直接在日期上加上一天是完全沒問題的。

JavaScript:

var date = new Date(1572764400*1000);
date.setDate(date.getDate()+1);
var timestamp = Math.round(date.getTime()/1000);

注意:JS的時間戳是毫秒!!!

 

結論

在經濟全球化快速發展的今天,在軟件開發的過程中,盡量養成習慣,由於夏令時和冬令時不是固定的,開發在時間計算上應該慎用86400進行加減運算,時間計算請直接對日期進行加減,展示時間給用戶看的時候盡量結合當地時間,結合夏令時和冬令時計算出準確的當地時間,避免產生不必要的分歧。

 

參考:

[1]. 

[2]. 

 

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

※想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

※不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置,台北網頁設計,各種案例分享

Java虛擬機詳解(十)——類加載過程

  在上一篇文章中,我們詳細的介紹了Java,那麼這些Class文件是如何被加載到內存,由虛擬機來直接使用的呢?這就是本篇博客將要介紹的——類加載過程。

1、類的生命周期

  類從被加載到虛擬機內存開始,到卸載出內存為止,其聲明周期流程如下:

  

  上圖中紅色的5個部分(加載、驗證、準備、初始化、卸載)順序是確定的,也就是說,類的加載過程必須按照這種順序按部就班的開始。這裏的“開始”不是按部就班的“進行”或者“完成”,因為這些階段通常是互相交叉混合的進行的,通常會在一個階段執行過程中調用另一個階段。

2、加載

  “加載”階段是“類加載”生命周期的第一個階段。在加載階段,虛擬機要完成下面三件事:

  ①、通過一個類的全限定名來獲取定義此類的二進制字節流。

  ②、將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。

  ③、在Java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入口。

  PS:類的全限定名可以理解為這個類存放的絕對路徑。方法區是JDK1.7以前定義的運行時數據區,而在JDK1.8以後改為元數據區(Metaspace),主要用於存放被Java虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。詳情可以參考這邊該系列的第二篇文章——。

  另外,我們看第一點——通過類的權限定名來獲取定義此類的二進制流,這裏並沒有明確指明要從哪裡獲取以及怎樣獲取,也就是說並沒有明確規定一定要我們從一個 Class 文件中獲取。基於此,在Java的發展過程中,充滿創造力的開發人員在這個舞台上玩出了各種花樣:

  1、從 ZIP 包中讀取。這稱為後面的 JAR、EAR、WAR 格式的基礎。

  2、從網絡中獲取。比較典型的應用就是 Applet。

  3、運行時計算生成。這就是動態代理技術。

  4、由其它文件生成。比如 JSP 應用。

  5、從數據庫中讀取。

  加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區中,然後在Java堆中實例化一個 java.lang.Class 類的對象,這個對象將作為程序訪問方法區中這些類型數據的外部接口。

  注意,加載階段與連接階段的部分內容(如一部分字節碼文件的格式校驗)是交叉進行的,加載階段尚未完成,連接階段可能已經開始了。

3、驗證

  驗證是連接階段的第一步,作用是為了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

  我們說Java語言本身是相對安全,因為編譯器的存在,純粹的Java代碼要訪問數組邊界外的數據、跳轉到不存在的代碼行之類的,是要被編譯器拒絕的。但是前面我們也說過,Class 文件不一定非要從Java源碼編譯過來,可以使用任何途徑,包括你很牛逼,直接用十六進制編輯器來編寫 Class 文件。

  所以,如果虛擬機不檢查輸入的字節流,將會載入有害的字節流而導致系統崩潰。但是虛擬機規範對於檢查哪些方面,何時檢查,怎麼檢查都沒有明確的規定,不同的虛擬機實現方式可能都會有所不同,但是大致都會完成下面四個方面的檢查。

①、文件格式驗證

  校驗字節流是否符合Class文件格式的規範,並且能夠被當前版本的虛擬機處理。

  一、是否以魔數 0xCAFEBABE 開頭。

  二、主、次版本號是否是當前虛擬機處理範圍之內。

  三、常量池的常量中是否有不被支持的常量類型(檢查常量tag標誌)

  四、指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。

  五、CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的數據。

  六、Class 文件中各個部分及文件本身是否有被刪除的或附加的其他信息。

  以上是一部分校驗內容,當然遠不止這些。經過這些校驗后,字節流才會進入內存的方法區中存儲,接下來後面的三個階段校驗都是基於方法區的存儲結構進行的。

②、元數據驗證

  第二個階段主要是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範要求。

  一、這個類是否有父類(除了java.lang.Object 類之外,所有的類都應當有父類)。

  二、這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)。

  三、如果這個類不是抽象類,是否實現了其父類或接口之中要求實現的所有普通方法。

  四、類中的字段、方法是否與父類產生了矛盾(例如覆蓋了父類的final字段、或者出現不符合規則的重載)

③、字節碼驗證

  第三個階段字節碼驗證是整個驗證階段中最複雜的,主要是進行數據流和控制流分析。該階段將對類的方法進行分析,保證被校驗的方法在運行時不會做出危害虛擬機安全的行為。

  一、保證任意時刻操作數棧中的數據類型與指令代碼序列都能配合工作。例如不會出現在操作數棧中放置了一個 int 類型的數據,使用時卻按照 long 類型來加載到本地變量表中。

  二、保證跳轉指令不會跳轉到方法體以外的字節碼指令中。

  三、保證方法體中的類型轉換是有效的。比如把一個子類對象賦值給父類數據類型,這是安全的。但是把父類對象賦值給子類數據類型,甚至賦值給完全不相干的類型,這就是不合法的。

④、符號引用驗證

  符號引用驗證主要是對類自身以外(常量池中的各種符號引用)的信息進行匹配性的校驗,通常需要校驗如下內容:

  一、符號引用中通過字符串描述的全限定名是否能夠找到相應的類。

  二、在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段。

  三、符號引用中的類、字段和方法的訪問性(private、protected、public、default)是否可以被當前類訪問。

4、準備

  準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存是在方法區中進行分配。

  注意:

  一、上面說的是類變量,也就是被 static 修飾的變量,不包括實例變量。實例變量會在對象實例化時隨着對象一起分配在堆中。

  二、初始值,指的是一些數據類型的默認值。基本的數據類型初始值如下(引用類型的初始值為null):

  

 

   比如,定義 public static int value = 123 。那麼在準備階段過後,value 的值是 0 而不是 123,把 value 賦值為123 是在程序被編譯后,存放在類的構造器方法之中,是在初始化階段才會被執行。但是有一種特殊情況,通過final 修飾的屬性,比如 定義 public final static int value = 123,那麼在準備階段過後,value 就被賦值為123了。

5、解析

  解析階段是虛擬機將常量池中的符號引用替換為直接引用的過程。

  符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標不一定已經加載到內存中。

  直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現內存布局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那麼引用的目標必定已經在內存中存在。

  解析動作主要針對類或接口、字段、類方法、接口方法四類符號引用,分別對應於常量池的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANTS_InterfaceMethodref_info四種類型常量。

6、初始化

   初始化階段是類加載階段的最後一步,前面過程中,除第一個加載階段可以通過用戶自定義類加載器參与之外,其餘過程都是完全由虛擬機主導和控制。而到了初始化階段,則開始真正執行類中定義的Java程序代碼(或者說是字節碼)。

  在前面介紹的準備階段中,類變量已經被賦值過初始值了,而初始化階段,則根據程序員的編碼去初始化變量和資源。

  換句話來說,初始化階段是執行類構造器<clinit>() 方法的過程。

  ①、<clinit>() 方法 是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{})中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊中可以賦值,但是不能訪問。

  比如如下代碼會報錯:

  

 

   但是你把第 14 行代碼放到 static 靜態代碼塊的上面就不會報錯了。或者不改變代碼順序,將第 11 行代碼移除,也不會報錯。

  ②、<clinit>() 方法與類的構造函數(或者說是實例構造器<init>()方法)不同,它不需要显示的調用父類構造器,虛擬機會保證在子類的<init>()方法執行之前,父類的<init>()方法已經執行完畢。因此虛擬機中第一個被執行的<init>()方法的類肯定是 java.lang.Object。

  ③、由於父類的<clinit>() 方法先執行,所以父類中定義的靜態語句塊要優先於子類的變量賦值操作。

  ④、<clinit>() 方法對於接口來說並不是必須的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯器可以不為這個類生成<clinit>() 方法。

  ⑤、接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>() 方法。但接口與類不同的是,執行接口中的<clinit>() 方法不需要先執行父接口的<clinit>() 方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。

  ⑥、接口的實現類在初始化時也一樣不會執行接口的<clinit>() 方法。

  ⑦、虛擬機會保證一個類的<clinit>() 方法在多線程環境中被正確的加鎖和同步。如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>() 方法,其他的線程都需要阻塞等待,直到活動線程執行<clinit>() 方法完畢。如果在一個類的<clinit>() 方法中有很耗時的操作,那麼可能造成多個進程的阻塞。

  比如對於如下代碼:

package com.yb.carton.controller;

/**
 * Create by YSOcean
 */
public class ClassLoadInitTest {


    static class Hello{
        static {
            if(true){
                System.out.println(Thread.currentThread().getName() + "init");
                while(true){}
            }
        }
    }

    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"start");
            Hello h1 = new Hello();
            System.out.println(Thread.currentThread().getName()+"run over");
        }).start();


        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"start");
            Hello h2 = new Hello();
            System.out.println(Thread.currentThread().getName()+"run over");
        }).start();
    }

}

View Code

  運行結果如下:

  

 

   線程1搶到了執行<clinit>() 方法,但是該方法是一個死循環,線程2將一直阻塞等待。

  知道了類的初始化過程,那麼類的初始化何時被觸發呢?JVM大概規定了如下幾種情況:

  ①、當虛擬機啟動時,初始化用戶指定的類。

  ②、當遇到用以新建目標類實例的 new 指令時,初始化 new 指定的目標類。

  ③、當遇到調用靜態方法的指令時,初始化該靜態方法所在的類。

  ④、當遇到訪問靜態字段的指令時,初始化該靜態字段所在的類。

  ⑤、子類的初始化會觸發父類的初始化。

  ⑥、如果一個接口定義了 default 方法,那麼直接實現或間接實現該接口的類的初始化,會觸發該接口的初始化。

  ⑦、使用反射 API 對某個類進行反射調用時,會初始化這個類。

  ⑧、當初次調用 MethodHandle 實例時,初始化該 MethodHandle 指向的方法所在的類。

 

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

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

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

羞,Spring Bean 初始化/銷毀竟然有這麼多姿勢

文章來源:

一、前言

日常開發過程有時需要在應用啟動之後加載某些資源,或者在應用關閉之前釋放資源。Spring 框架提供相關功能,圍繞 Spring Bean 生命周期,可以在 Bean 創建過程初始化資源,以及銷毀 Bean 過程釋放資源。Spring 提供多種不同的方式初始化/銷毀 Bean,如果同時使用這幾種方式,Spring 如何處理這幾者之間的順序?

有沒有覺得標題很熟悉,沒錯標題模仿二哥 「@沉默王二」 文章。

二、姿勢剖析

首先我們先來回顧一下 Spring 初始化/銷毀 Bean 幾種方式,分別為:

  • init-method/destroy-method
  • InitializingBean/DisposableBean
  • @PostConstruct/@PreDestroy
  • ContextStartedEvent/ContextClosedEvent

PS: 其實還有一種方式,就是繼承 Spring Lifecycle 接口。不過這種方式比較繁瑣,這裏就不再分析。

2.1、init-method/destroy-method

這種方式在配置文件文件指定初始化/銷毀方法。XML 配置如下

<bean id="demoService" class="com.dubbo.example.provider.DemoServiceImpl"  destroy-method="close"  init-method="initMethod"/>

或者也可以使用註解方式配置:

@Configurable
public class AppConfig {

    @Bean(initMethod = "init", destroyMethod = "destroy")
    public HelloService hello() {
        return new HelloService();
    }
}

還記得剛開始接觸學習 Spring 框架,使用就是這種方式。

2.2、InitializingBean/DisposableBean

這種方式需要繼承 Spring 接口 InitializingBean/DisposableBean,其中 InitializingBean 用於初始化動作,而 DisposableBean 用於銷毀之前清理動作。使用方式如下:

@Service
public class HelloService implements InitializingBean, DisposableBean {
    
    @Override
    public void destroy() throws Exception {
        System.out.println("hello destroy...");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("hello init....");
    }
}

2.3、@PostConstruct/@PreDestroy

這種方式相對於上面兩種方式來說,使用方式最簡單,只需要在相應的方法上使用註解即可。使用方式如下:

@Service
public class HelloService {


    @PostConstruct
    public void init() {
        System.out.println("hello @PostConstruct");
    }

    @PreDestroy
    public void PreDestroy() {
        System.out.println("hello @PreDestroy");
    }
}

這裏踩過一個坑,如果使用 JDK9 之後版本 ,@PostConstruct/@PreDestroy 需要使用 maven 單獨引入 javax.annotation-api,否者註解不會生效。

2.4、ContextStartedEvent/ContextClosedEvent

這種方式使用 Spring 事件機制,日常業務開發比較少見,常用與框架集成中。Spring 啟動之後將會發送 ContextStartedEvent 事件,而關閉之前將會發送 ContextClosedEvent 事件。我們需要繼承 Spring ApplicationListener 才能監聽以上兩種事件。

@Service
public class HelloListener implements ApplicationListener {

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if(event instanceof ContextClosedEvent){
            System.out.println("hello ContextClosedEvent");
        }else if(event instanceof ContextStartedEvent){
            System.out.println("hello ContextStartedEvent");
        }

    }
}

也可以使用 @EventListener註解,使用方式如下:

public class HelloListenerV2 {
    
    @EventListener(value = {ContextClosedEvent.class, ContextStartedEvent.class})
    public void receiveEvents(ApplicationEvent event) {
        if (event instanceof ContextClosedEvent) {
            System.out.println("hello ContextClosedEvent");
        } else if (event instanceof ContextStartedEvent) {
            System.out.println("hello ContextStartedEvent");
        }
    }
}

PS:只有調用 ApplicationContext#start 才會發送 ContextStartedEvent。若不想這麼麻煩,可以監聽 ContextRefreshedEvent 事件代替。一旦 Spring 容器初始化完成,就會發送 ContextRefreshedEvent。

三、綜合使用

回顧完上面幾種方式,這裏我們綜合使用上面的四種方式,來看下 Spring 內部的處理順序。在看結果之前,各位讀者大人可以猜測下這幾種方式的執行順序。

public class HelloService implements InitializingBean, DisposableBean {


    @PostConstruct
    public void init() {
        System.out.println("hello @PostConstruct");
    }

    @PreDestroy
    public void PreDestroy() {
        System.out.println("hello @PreDestroy");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("bye DisposableBean...");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("hello InitializingBean....");
    }

    public void xmlinit(){
        System.out.println("hello xml-init...");
    }

    public void xmlDestory(){
        System.out.println("bye xmlDestory...");
    }

    @EventListener(value = {ContextClosedEvent.class, ContextStartedEvent.class})
    public void receiveEvents(ApplicationEvent event) {
        if (event instanceof ContextClosedEvent) {
            System.out.println("bye ContextClosedEvent");
        } else if (event instanceof ContextStartedEvent) {
            System.out.println("hello ContextStartedEvent");
        }
    }

}

xml 配置方式如下:

    <context:annotation-config />
    <context:component-scan base-package="com.dubbo.example.demo"/>
    
    <bean class="com.dubbo.example.demo.HelloService" init-method="xmlinit" destroy-method="xmlDestory"/>

應用啟動方法如下:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-provider.xml");
context.start();
context.close();

程序輸出結果如下所示:

最後採用圖示說明總結以上結果:

四、源碼解析

不知道各位讀者有沒有猜對這幾種方式的執行順序,下面我們就從源碼角度解析 Spring 內部處理的順序。

4.1、初始化過程

使用 ClassPathXmlApplicationContext 啟動 Spring 容器,將會調用 refresh 方法初始化容器。初始化過程將會創建 Bean 。最後當一切準備完畢,將會發送 ContextRefreshedEvent。當容器初始化完畢,調用 context.start() 就發送 ContextStartedEvent 事件。

refresh 方法源碼如下:

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
            //... 忽略無關代碼

            // 初始化所有非延遲初始化的 Bean
            finishBeanFactoryInitialization(beanFactory);

            // 發送 ContextRefreshedEvent
            finishRefresh();

            //... 忽略無關代碼
    }
}

一路跟蹤 finishBeanFactoryInitialization 源碼,直到 AbstractAutowireCapableBeanFactory#initializeBean,源碼如下:

protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
        // 調用 BeanPostProcessor#postProcessBeforeInitialization 方法
        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }

    try {
        // 初始化 Bean
        invokeInitMethods(beanName, wrappedBean, mbd);
    }
    catch (Throwable ex) {
        throw new BeanCreationException(
                (mbd != null ? mbd.getResourceDescription() : null),
                beanName, "Invocation of init method failed", ex);
    }
}

BeanPostProcessor 將會起着攔截器的作用,一旦 Bean 符合條件,將會執行一些處理。這裏帶有 @PostConstruct 註解的 Bean 都將會被 CommonAnnotationBeanPostProcessor 類攔截,內部將會觸發 @PostConstruct 標註的方法。

接着執行 invokeInitMethods ,方法如下:

protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd)
        throws Throwable {

    boolean isInitializingBean = (bean instanceof InitializingBean);
    if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
        // 省略無關代碼
        // 如果是 Bean 繼承 InitializingBean,將會執行  afterPropertiesSet 方法
        ((InitializingBean) bean).afterPropertiesSet();
    }

    if (mbd != null) {
        String initMethodName = mbd.getInitMethodName();
        if (initMethodName != null && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
                !mbd.isExternallyManagedInitMethod(initMethodName)) {
            // 執行 XML 定義 init-method
            invokeCustomInitMethod(beanName, bean, mbd);
        }
    }
}

如果 Bean 繼承 InitializingBean 接口,將會執行 afterPropertiesSet 方法,另外如果在 XML 中指定了 init-method ,也將會觸發。

上面源碼其實都是圍繞着 Bean 創建的過程,當所有 Bean 創建完成之後,調用 context#start 將會發送 ContextStartedEvent 。這裏源碼比較簡單,如下:

public void start() {
    getLifecycleProcessor().start();
    publishEvent(new ContextStartedEvent(this));
}

4.2、銷毀過程

調用 ClassPathXmlApplicationContext#close 方法將會關閉容器,具體邏輯將會在 doClose 方法執行。

doClose 這個方法首先發送 ContextClosedEvent,然再后開始銷毀 Bean。

靈魂拷問:如果我們顛倒上面兩者順序,結果會一樣嗎?

doClose 源碼如下:

protected void doClose() {
    if (this.active.get() && this.closed.compareAndSet(false, true)) {
        // 省略無關代碼

        try {
            // Publish shutdown event.
            publishEvent(new ContextClosedEvent(this));
        }
        catch (Throwable ex) {
            logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
        }


        // 銷毀 Bean
        destroyBeans();

        // 省略無關代碼
    }
}

destroyBeans 最終將會執行 DisposableBeanAdapter#destroy,@PreDestroy、DisposableBean、destroy-method 三者定義的方法都將會在內部被執行。

首先執行 DestructionAwareBeanPostProcessor#postProcessBeforeDestruction,這裏方法類似與上面 BeanPostProcessor。

@PreDestroy 註解將會被 CommonAnnotationBeanPostProcessor 攔截,這裏類同時也繼承了 DestructionAwareBeanPostProcessor。

最後如果 Bean 為 DisposableBean 的子類,將會執行 destroy 方法,如果在 xml 定義了 destroy-method 方法,該方法也會被執行。

public void destroy() {
    if (!CollectionUtils.isEmpty(this.beanPostProcessors)) {
        for (DestructionAwareBeanPostProcessor processor : this.beanPostProcessors) {
            processor.postProcessBeforeDestruction(this.bean, this.beanName);
        }
    }

    if (this.invokeDisposableBean) {
        // 省略無關代碼
        // 如果 Bean 繼承 DisposableBean,執行 destroy 方法
        ((DisposableBean) bean).destroy();
        
    }

    if (this.destroyMethod != null) {
        // 執行 xml 指定的  destroy-method 方法
        invokeCustomDestroyMethod(this.destroyMethod);
    }
    else if (this.destroyMethodName != null) {
        Method methodToCall = determineDestroyMethod();
        if (methodToCall != null) {
            invokeCustomDestroyMethod(methodToCall);
        }
    }
}

五、總結

init-method/destroy-method 這種方式需要使用 XML 配置文件或單獨註解配置類,相對來說比較繁瑣。而InitializingBean/DisposableBean 這種方式需要單獨繼承 Spring 的接口實現相關方法。@PostConstruct/@PreDestroy 這種註解方式使用方式簡單,代碼清晰,比較推薦使用這種方式。

另外 ContextStartedEvent/ContextClosedEvent 這種方式比較適合在一些集成框架使用,比如 Dubbo 2.6.X 優雅停機就是用改機制。

六、Spring 歷史文章推薦

歡迎關注我的公眾號:程序通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:

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

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

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

設計模式之代理模式

什麼是代理模式

代理模式就是為一個對象提供一個代理對象,由這個代理對象控制對該對象的訪問。

理解代理模式,可以對照生活中的一些具體例子,比如房產中介、二手車交易市場、經紀人等。

為什麼要用代理模式

通過使用代理模式,我們避免了直接訪問目標對象時可能帶來的一些問題,比如:遠程調用,需要使用遠程代理來幫我們處理一些網絡傳輸相關的細節邏輯;可能需要基於某種權限控制對目標資源的訪問,可以使用保護代理等。

總的來說,通過是用代理模式,我們可以控制對目標對象的訪問,可以在真實方法被調用前或調用后,通過代理對象加入額外的處理邏輯。

代理模式分類

代理模式分為靜態代理和動態代理。動態代理根據實現不同又可細分為JDK動態代理和cglib動態代理。

靜態代理是由程序員創建或工具生成代理類的源碼,再編譯代理類。所謂靜態也就是在程序運行前就已經存在代理類的字節碼文件,代理類和委託類的關係在運行前就確定了。

動態代理是在實現階段不用關心代理類,而在運行時動態生成代理類的。

靜態代理

以房哥買房子為例,用代碼實現靜態代理。

1、首先建立一個Seller接口

public interface Seller {
    void sell();
}

2、創建實現類,房哥,有一個方法,就是買房子

public class FangGe implements Seller{
    @Override
    public void sell() {
        System.out.println("房哥要出手一套四合院");
    }
}

3、買房子需要找到買家,達成交易后還要辦理過戶等其他手續,房哥只想賣房收錢就完了。因此,需要找一個代理來幫房哥處理這些雜事。

我們創建一個代理類FangGeProxy,代理類也需要實現Seller接口,行為上要保持和FangGe一樣,都是要賣房子。同時該代理類還需要持有房哥的引用。

public class FangGeProxy implements Seller{
    private FangGe fangGe;

    public FangGeProxy(FangGe fangGe){
        this.fangGe = fangGe;
    }
    @Override
    public void sell() {
        findBuyer();
        fangGe.sell();
        afterSell();
    }
    
    public void findBuyer(){
        System.out.println("代理幫助尋找買主");
    }
    
    public void afterSell(){
        System.out.println("達成交易后,辦理相關手續");
    }
}

可以看到,房哥的代理類通過findBuyer()和afterSell()兩個方法幫助房哥完成了其他一些雜事。

4、測試類

public class StaticProxyTest {
    public static void main(String[] args) {
        Seller seller = new FangGeProxy(new FangGe());
        seller.sell();
    }
}

輸出:

代理幫助尋找買主
房哥要出手一套四合院
達成交易后,辦理相關手續

最後,看下類圖

靜態代理的問題:

1、由於靜態代理類在編譯前已經確定了代理的對象,因此靜態代理只能代理一種類型的類,如果要給大量的類做代理,就需要編寫大量的代理類;

2、如果我們要給Seller,也就是目標對象要增加一些方法,則需要同步修改代理類,不符合開閉原則。

JDK動態代理

JDK的動態代理依賴於jdk給我們提供的類庫實現,是一種基於接口實現的動態代理,在編譯時並不知道要代理哪個類,而是在運行時動態生成代理類。同時也解決了靜態代理中存在的問題。

我們接上上面靜態代理的例子,繼續實現JDK的動態代理。

1、我們建一個方法轉發的處理器類,該類需要實現InvocationHandler接口。

public class SellerInvocationHandler implements InvocationHandler {

    // 要代理的真實對象
    private Object target;

    /**
     * 使用Proxy類靜態方法獲取代理類實例
     */
    public Object getProxyInstance(Object target){
        this.target = target;
        Class<?> clazz = target.getClass();
        return Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterfaces(),this);
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object obj = method.invoke(this.target, args);
        after();
        return obj;
    }

    private void before() {
        System.out.println("執行方法前");
    }
    
    private void after() {
        System.out.println("執行方法后");
    }
}

2、新建JDK動態代理測試類,首先代理房哥賣房子

public class JDKDynamicProxyTest {
    public static void main(String[] args) {

        // new一個房哥,下面幫房哥找個代理
        FangGe fangGe = new FangGe();
        SellerInvocationHandler sellerInvocationHandler = new SellerInvocationHandler();
        
        // 房哥的代理對象
        Seller seller = (Seller) sellerInvocationHandler.getProxyInstance(fangGe);
        seller.sell();

    }
}

輸出:

執行方法前
房哥要出手一套四合院
執行方法后

可以看到,完成了代理。

3、接下來我們新建另外一個類,User類,並使用JDK動態代理完成代理User類

public interface IUser {
    void sayHello();

    void work();
}

public class UserImpl implements IUser{
    @Override
    public void sayHello() {
        System.out.println("hello,我是小明");
    }

    @Override
    public void work() {
        System.out.println("我正在寫代碼");
    }
}

修改測試類,

public class JDKDynamicProxyTest {
    public static void main(String[] args) {

/*        // new一個房哥,下面幫房哥找個代理
        FangGe fangGe = new FangGe();
        SellerInvocationHandler sellerInvocationHandler = new SellerInvocationHandler();

        // 房哥的代理對象
        Seller seller = (Seller) sellerInvocationHandler.getProxyInstance(fangGe);
        seller.sell();*/

        // 代理user類
        IUser user = new UserImpl();
        SellerInvocationHandler sellerInvocationHandler = new SellerInvocationHandler();
        IUser userProxy = (IUser) sellerInvocationHandler.getProxyInstance(user);
        userProxy.sayHello();
        userProxy.work();

    }
}

輸出:

執行方法前
hello,我是小明
執行方法后
執行方法前
我正在寫代碼
執行方法后

可以看到,我們SellerInvocationHandler 並未做任何改動,它便能為UserImpl類生成代理,並在執行方法的前後增加額外的執行邏輯。

cglib動態代理

JDK動態代理有一個局限就是,被代理的類必須要實現接口。如果被代理的類沒有實現接口,則JDK動態代理就無能為力了。這個時候該cglib動態代理上場了。

CGLIB是一個功能強大,高性能的代碼生成包。它為沒有實現接口的類提供代理,為JDK的動態代理提供了很好的補充。通常可以使用Java的動態代理創建代理,但當要代理的類沒有實現接口或者為了更好的性能,CGLIB是一個好的選擇。

1、新建一個MyCglibInterceptor,實現MethodInterceptor接口。該類類似於JDK動態代理中的InvocationHandler實例,是實現cglib動態代理的主要類。

public class MyCglibInterceptor implements MethodInterceptor {

    public Object getCglibProxyInstance(Object object){
        // 相當於Proxy,創建代理的工具類
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(object.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }

    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object obj = methodProxy.invokeSuper(o, objects);
        after();
        return obj;
    }

    private void before() {
        System.out.println("執行方法之前");
    }

    private void after() {
        System.out.println("執行方法之後");
    }
}

2、新建cglib動態代理的測試類,先代理上面例子中的User類。

public class CglibDynamicProxyTest {
    public static void main(String[] args) {
        MyCglibInterceptor myCglibInterceptor = new MyCglibInterceptor();
        IUser userCglibProxy = (IUser) myCglibInterceptor.getCglibProxyInstance(new UserImpl());
        userCglibProxy.sayHello();
        userCglibProxy.work();
    }
}

輸出:

執行方法之前
hello,我是小明
執行方法之後
執行方法之前
我正在寫代碼
執行方法之後

3、新建一個類HelloWorld,不實現任何接口,為該類實現動態代理。

public class HelloWorld {
    public void hello(){
        System.out.println("世界這麼大,我想去看看");
    }
}

測試代理類

public class CglibDynamicProxyTest {
    public static void main(String[] args) {
/*        MyCglibInterceptor myCglibInterceptor = new MyCglibInterceptor();
        IUser userCglibProxy = (IUser) myCglibInterceptor.getCglibProxyInstance(new UserImpl());
        userCglibProxy.sayHello();
        userCglibProxy.work();*/

        // 代理未實現任何接口的類
        MyCglibInterceptor myCglibInterceptor = new MyCglibInterceptor();
        HelloWorld helloWorldProxy = (HelloWorld) myCglibInterceptor.getCglibProxyInstance(new HelloWorld());
        helloWorldProxy.hello();
    }
}

輸出:

執行方法之前
世界這麼大,我想去看看
執行方法之後

使用cglib動態代理,我們實現了對普通類的代理。

(完)

設計模式系列文章

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

※想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

※不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置,台北網頁設計,各種案例分享

Java描述設計模式(23):訪問者模式

本文源碼: ||

一、生活場景

1、場景描述

電競是遊戲比賽達到“競技”層面的體育項目。利用电子設備作為運動器械進行的、人與人之間的智力對抗運動。通過電競,可以提高人的反應能力、協調能力、團隊精神等。但是不同人群的對電競的持有的觀念不一樣,有的人認為電競就是沉迷網絡,持反對態度,而有的人就比較贊同。下面基於訪問者模式來描述該場景。

2、場景圖解

3、代碼實現

public class C01_InScene {
    public static void main(String[] args) {
        DataSet dataSet = new DataSet() ;
        dataSet.addCrowd(new Youth());
        dataSet.addCrowd(new MiddleAge());
        CrowdView crowdView = new Against() ;
        dataSet.display(crowdView);
        crowdView = new Approve() ;
        dataSet.display(crowdView);
    }
}
/**
 * 雙分派,不同人群管理
 */
abstract class Crowd {
    abstract void accept(CrowdView action);
}
class Youth extends Crowd {
    @Override
    public void accept(CrowdView view) {
        view.getYouthView(this);
    }
}
class MiddleAge extends Crowd {
    @Override
    public void accept(CrowdView view) {
        view.getMiddleAgeView (this);
    }
}
/**
 * 不同人群觀念的管理
 */
abstract class CrowdView {
    // 青年人觀念
    abstract void getYouthView (Youth youth);
    // 中年人觀念
    abstract void getMiddleAgeView (MiddleAge middleAge);
}
class Approve extends CrowdView {
    @Override
    public void getYouthView(Youth youth) {
        System.out.println("青年人贊同電競");
    }
    @Override
    public void getMiddleAgeView(MiddleAge middleAge) {
        System.out.println("中年人贊同電競");
    }
}
class Against extends CrowdView {
    @Override
    public void getYouthView(Youth youth) {
        System.out.println("青年人反對電競");
    }
    @Override
    public void getMiddleAgeView(MiddleAge middleAge) {
        System.out.println("中年人反對電競");
    }
}
/**
 * 提供一個數據集合
 */
class DataSet {
    private List<Crowd> crowdList = new ArrayList<>();
    public void addCrowd (Crowd crowd) {
        crowdList.add(crowd);
    }
    public void display(CrowdView crowdView) {
        for(Crowd crowd : crowdList) {
            crowd.accept(crowdView);
        }
    }
}

二、訪問者模式

1、基礎概念

訪問者模式是對象的行為模式,把作用於數據結構的各元素的操作封裝,操作之間沒有關聯。可以在不改變數據結構的前提下定義作用於這些元素的不同的操作。主要將數據結構與數據操作分離,解決數據結構和操作耦合問題核心原理:被訪問的類裏面加對外提供接待訪問者的接口。

2、模式圖解

3、核心角色

  • 抽象訪問者角色

聲明多個方法操作,具體訪問者角色需要實現的接口。

  • 具體訪問者角色

實現抽象訪問者所聲明的接口,就是各個訪問操作。

  • 抽象節點角色

聲明接受操作,接受訪問者對象作為參數。

  • 具體節點角色

實現抽象節點所規定的具體操作。

  • 結構對象角色

能枚舉結構中的所有元素,可以提供一個高層的接口,用來允許訪問者對象訪問每一個元素。

4、源碼實現

public class C02_Visitor {
    public static void main(String[] args) {
        ObjectStructure obs = new ObjectStructure();
        obs.add(new NodeA());
        obs.add(new NodeB());
        Visitor visitor = new VisitorA();
        obs.doAccept(visitor);
    }
}
/**
 * 抽象訪問者角色
 */
interface Visitor {
    /**
     * NodeA的訪問操作
     */
    void visit(NodeA node);
    /**
     * NodeB的訪問操作
     */
    void visit(NodeB node);
}
/**
 * 具體訪問者角色
 */
class VisitorA implements Visitor {
    @Override
    public void visit(NodeA node) {
        node.operationA() ;
    }
    @Override
    public void visit(NodeB node) {
        node.operationB() ;
    }
}
class VisitorB implements Visitor {
    @Override
    public void visit(NodeA node) {
        node.operationA() ;
    }
    @Override
    public void visit(NodeB node) {
        node.operationB() ;
    }
}
/**
 * 抽象節點角色
 */
abstract class Node {
    /**
     * 接收訪問者
     */
    abstract void accept(Visitor visitor);
}
/**
 * 具體節點角色
 */
class NodeA extends Node{
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    public void operationA(){
        System.out.println("NodeA.operationA");
    }
}
class NodeB extends Node{
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    public void operationB(){
        System.out.println("NodeB.operationB");
    }
}
/**
 * 結構對象角色類
 */
class ObjectStructure {
    private List<Node> nodes = new ArrayList<>();
    public void detach(Node node) {
        nodes.remove(node);
    }
    public void add(Node node){
        nodes.add(node);
    }
    public void doAccept(Visitor visitor){
        for(Node node : nodes) {
            node.accept(visitor);
        }
    }
}

三、Spring框架應用

1、Bean結構的訪問

BeanDefinitionVisitor類,遍歷bean的各個屬性;接口 BeanDefinition,定義Bean的各樣信息,比如屬性值、構造方法、參數等等。這裏封裝操作bean結構的相關方法,但卻沒有改變bean的結構。

2、核心代碼塊

public class BeanDefinitionVisitor {
    public void visitBeanDefinition(BeanDefinition beanDefinition) {
        this.visitParentName(beanDefinition);
        this.visitBeanClassName(beanDefinition);
        this.visitFactoryBeanName(beanDefinition);
        this.visitFactoryMethodName(beanDefinition);
        this.visitScope(beanDefinition);
        if (beanDefinition.hasPropertyValues()) {
            this.visitPropertyValues(beanDefinition.getPropertyValues());
        }
        if (beanDefinition.hasConstructorArgumentValues()) {
            ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
            this.visitIndexedArgumentValues(cas.getIndexedArgumentValues());
            this.visitGenericArgumentValues(cas.getGenericArgumentValues());
        }
    }
}

四、模式總結

1、優點描述

(1) 訪問者模式符合單一職責原則、使程序具有良好的擴展性、靈活性;

(2) 訪問者模式適用與攔截器與過濾器等常見功能,數據結構相對穩定的場景;

2、缺點描述

(1) 訪問者關注其他類的內部細節,依賴性強,違反迪米特法則,這樣導致具體元素更新麻煩;

(2) 訪問者依賴具體元素,不是抽象元素,面向細節編程,違背依賴倒轉原則;

五、源代碼地址

GitHub·地址
https://github.com/cicadasmile/model-arithmetic-parent
GitEE·地址
https://gitee.com/cicadasmile/model-arithmetic-parent

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

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

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

十年風雨,一個普通程序員的成長之路(九)一眼望到頭,一眼望不到頭

還有十幾天就是我的32歲生日,然後,33了,要過年了。

古人三十而立,我卻在這狹窄的圈子里兜兜轉轉。

多年前的喊的一句創業口號,現在還是口號。

焦慮、迷茫。

這两天一場網易的暴力裁員事件,犹如一盆涼水當頭澆下。

讓我又陷入了一年前的時刻。

渾身提不起勁。什麼都不想做。

不知前路在哪裡?

 

回過頭來看,對於當事人來說曲折圓轉的半生,之於他人,不過又是一個復讀機的普通人生而已。

上學、畢業、工作、買房、結婚、生子、還貸。

沒有家庭是形單影只,凄凄涼涼。

有了家庭卻只能蠅營狗苟,負重而行。

一眼望到頭的路罷了。

 

我們都只是這庸碌的銅爐里,亂糟糟破爛爛的一塊廢銅爛鐵而已。

所以財富神話才那麼多捧臭腳的雇從。

因為世間大多的煩惱,便是沒錢的煩惱。

所以我們信努力改變命運、知識改變命令、堅持改變命運。

其實是金錢改變命運。

 

滿是一些交錢的APP,讓你堅持下去,給你鼓勵。永遠溫馨對你。

你會改變命運的,你會財富自由的。

我們如飢似渴,似乎覺得比身邊的人多get了一門技能,升職加薪也是指日可待了。

如果你沒有升職加薪、財富自由、改變命運,那不過是你還不夠努力罷了。

不過是,交的錢還不夠多罷了。

一眼望不到頭。

 

什麼時候是個頭?

寫博客、開公眾號、寫小說。尋找出路。

我們永遠相信自己是天命之子。

堂吉訶德騎着馬,夾着騎士長槍,無知無畏地沖向了風車。

大風車吱喲喲地轉,這裏的風景呀真好看……哦,畫風跑偏了。

大風車吱喲喲地轉,不為堂吉訶德所動,不為騎士長槍所動。似能流轉萬世。

 

天地不仁,以萬物為芻狗。

你又憑什麼跳出世間這個熔爐呢?

只能在時間的鐵鎚下越來越彎曲自己的身子。

在夕陽里傴僂着身子,苟延殘喘。

一眼望到頭,又一眼望不到頭。

 

在這條短暫卻又無盡的路上,那麼多的大V與培訓機構告訴你:

劉強東曾跟你一樣賣過盜版盤跟電腦。

馬雲還沒你強,曾被肯德基拒絕臨時工。

比爾蓋茨中途就退了學,你跟惠普也就差個車庫而已了。

遺憾的是,給你上課的老師,可能正兒八經的資金來源還沒有你多。

你以為打開了得到,便真能得道。

你以為買了極客時間,便真成了極客。

你以為加了大V,便算是有了人脈。

你以為入了知識星球,便真學到了知識。

遺憾的是,大多時候,它們與書架上落灰的書籍沒什麼兩樣。

 

家庭的壓力也讓你學習的時間慢慢變少。

有了一點獨處的時間,你卻又想打兩把遊戲,松一松這命運壓迫的喉嚨,大口地喘息兩聲。

在遊戲中,孩子的哭鬧、老婆的絮絮叨叨,都已變成遙遠的過去。

可是玩了一會,你卻又充滿了負罪感。空虛與寂寞隨之而來。

因為普通人改變命運的機會太少了。

所以只能讀書改變命運。

 

在兩千年的中華文明史中,知識改變命運的箴言已印刻在了基因里。

犹如稻草。

給溺水的人,最後一點光芒與希望。

因為你這一生,我這一生。一眼便已能看到頭。

所以佛度來生。

所以道修逍遙。

若有仙人撫我頂,怎可結髮受長生?

我願化為北冥之鯤,潛於九淵,扶搖九天,逍遙星河之外。

只是一聲“爸爸,我要尿尿”。夢,便醒了。

 

你心裏嘆了一口氣,便把兒子從你跟你老婆身邊抱下了床。

穿衣、洗漱,照了照鏡子,才刮的鬍子又長了出來。

算了,反正是非單身的程序員,也沒什麼可講究的。

關上門,復讀機的一天又開始了。

可是我還是渾身提不起勁,總感覺失去了什麼。

於是,寫下此文。

一眼望到頭,一眼望不到頭。

 

——————————————————–
歡迎關注我的公眾號:姚毛毛的博客

這裡有我的編程生涯感悟與總結,有Java、Linux、Oracle、mysql的相關技術,有工作中進行的架構設計實踐和讀書理論,有JVM、Linux、數據庫的性能調優,有……

有技術,有情懷,有溫度

歡迎關注我:姚毛毛& 妖生

 

 

 

 

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

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

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

文件與文件系統壓縮

目錄

在Linux下面有相當多的壓縮命令可以運行,這些壓縮命令可以讓我們更方便地從網絡上面下載容量較大的文件。此外,我們知道在Linux下面,擴展名沒有什麼特殊的意義。 不過,針對這些壓縮命令所產生的壓縮文件,為了方便記憶,還是會有一些特殊的命名方式,就讓我們來看看吧!

文件壓縮

什麼是文件壓縮呢?我們稍微談一談它的原理,目前我們使用的計算機系統中都是使用所謂的字節單位來計量。不過,事實上,計算機最小的計量單位應該是bit才對,此外,我們也知道 1字節=8比特(1Byte=8bit),但是如果今天我們只是記錄一個数字,即1這個数字,它會如何記錄?假設一個字節可以看成下面的模樣:

由於 1Byte=8bit,所以每個字節當中會有8個空格,而每個空格只可以是0、1

由於我們記錄的数字是1,考慮計算機所謂的二進制,如此一來,1會在最右邊佔據1個位,而其他的7個位將會自動地被填上0.如下圖所示

你看看,其實在這樣的例子中,那7個位應該是空的才對。不過,為了要滿足目前我們的操作系統數據的讀寫,所以就會將該數據轉為字節的形式來記錄。而一些聰明的計算機工程師就利用一些複雜的計算方式,將這些沒有使用到的空間【丟】出來,以讓文件佔用的空間變小,這就是壓縮的技術。
另一種壓縮技術也很有趣,它是將重複的數據進行統計記錄。舉例來說,如果你的數據為【111······】共有100個1時,那麼壓縮技術會記錄為【100個1】而不是真的有100個1的位存在。這樣也能夠精簡文件記錄的容量,非常有趣吧!
簡單地說,你可以將它想成,其實文件裏面有相當多的空間存在,並不是完全填滿的,而壓縮技術就是將這些空間填滿,以讓整個文件佔用的容量下降。不過,這些壓縮過的文件並無法直接被我們的操作系統所使用,因此,若要使用這些被壓縮過的文件數據,則必須將它還原回未壓縮前的模樣,那就是所謂的解壓縮。而至於壓縮后與壓縮的文件所佔用的磁盤空間大小,就可以被稱為是壓縮比。
這個壓縮與解壓縮的操作有什麼好處呢?
1.最大的好處就是壓縮過的文件容量變小了,所以你的硬盤無形之中就可以容納更多的數據。
2.此外,在一些網絡數據的傳輸中,也會由於數據量的降低,好讓網絡帶寬可以用來做更多的工作,而不是老卡在一些大型文件傳輸上面。

Linux系統常見壓縮命令

在Linux的環境中,壓縮文件的擴展名大多是: *.tar、*.tar.gz、*.gz、*.Z、*.bz2、*.xz。為什麼會有這樣的擴展名?不是說Linux的擴展名沒有什麼作用嗎?
這是因為Linux支持的壓縮命令非常多,且不同的命令所用的壓縮技術並不相同,當然彼此之間可能就無法互通/解壓縮文件。所以,當你下載到某個文件時,自然就需要知道該文件是由哪種壓縮命令所製作出來的,好用來對照對照着解壓縮,也就是說,雖然Linux文件的屬性基本上是與文件名沒有絕對關係的,但是為了幫助我們人類小小的腦袋,所以適當的擴展名還是必要的,下面我們就列出幾個常見的壓縮文件擴展名:

*.gz         gzip程序壓縮的文件
*.bz2        bzip2程序壓縮的文件
*.xz         xz程序壓縮的文件
*.zip        zip程序壓縮的文件
*.Z          compress程序壓縮的文件
*.tar        tar程序打包的文件,並沒有壓縮過
*.tar.gz     tar程序打包的文件,並且經過gzip的壓縮
*.tar.bz2    tar程序打包的文件,並且經過bzip2的壓縮
*.tar.xz     tar程序打包的文件,並且經過xz的壓縮

Linux常見的壓縮命令就是gzip、bzip2以及最新的xz,至於compress已經不流行了。為了支持windows常見的zip,其實Linux也早就有zip命令了。gzip是由GNU計劃所開發出來的壓縮命令,該命令支持已經替換了compress。後台GNU又開發出了bzip2及xz這幾個壓縮比更好的壓縮命令。不過,這些命令通常僅能針對一個文件來壓縮與解壓縮,如此一來,每次壓縮與解壓縮都要一大堆文件,豈不煩人?此時,這個所謂的【打包軟件,tar】就顯得很重要。
這個tar可以將很多文件打包成一個文件,甚至是目錄也可以這麼玩。不過,單純的tar功能僅僅是打包而已,即將很多文件結合為一個文件,事實上,它並沒有提供壓縮的功能,後台,GNU計劃中,將整個tar與壓縮的功能結合在一起,如此一來,提供用戶更方便且更強大的壓縮與打包功能,下面我們就來談一談這些在Linux下面基本的壓縮命令。

gzip

gzip可以說是應用最廣的壓縮命令了,目前gzip可以解開compress、zip和gzip等軟件所壓縮的文件,至於gzip所建立的壓縮文件為*.gz,讓我們來看看這個命令的語法:

gzip [-cdtvn] 文件名
選項與參數:
-c: 將壓縮的數據輸出到屏幕上,可通過數據流重定向來處理;
-d: 解壓縮的參數;
-t: 可以用來檢驗一個壓縮文件的一致性,看看文件有無錯誤;
-v: 可以显示出原文件/壓縮文件的壓縮比等信息;
-n: n為数字的意思,代表壓縮等級,-1最快,但壓縮比最差,-9最慢,但是壓縮比最好,默認是-6

示例1:壓縮文件(gzip -v 文件名)

示例2:解壓縮文件(gzip -d 文件名)

示例3:按照指定壓縮比壓縮(gzip -9 文件名)

示例4:查看壓縮文件的內容(zcat 文件名)

示例5:壓縮為指定文件名(gzip -c 文件名 > 指定文件名)

當你使用gzip進行壓縮時,在默認的狀態下原本的文件會被壓縮成為.gz後綴的文件,源文件就不存在了,這點與一般習慣使用Windows做壓縮的朋友所熟悉的情況不同,要注意。cat/more/less可以使用不同的方式來讀取純文本文件,那麼zcat/zmore/zless則可以對應於cat/more/less的方式來讀取純文件文件被壓縮后的壓縮文件。

bzip2

若說gzip是為了替換compress並提供更好的壓縮比而成立的,那麼bzip2則是為了替換gzip並提供更加的壓縮比而來。bzip2真是很不錯的東西,這玩意的壓縮比竟然比gzip還要好,至於bzip2的用法幾乎與gzip相同,看看下面的用法吧!

bzip2 [-cdkzvn] 文件名
選項與參數:
-c: 將壓縮的數據輸出到屏幕上,可通過數據流重定向來處理;
-d: 解壓縮的參數;
-k: 保留原始文件,而不是刪除原始文件;
-z: 壓縮的參數(默認值,可以不加);
-v: 可以显示出原文件/壓縮文件的壓縮比等信息;
-n: n為数字的意思,代表壓縮等級,-1最快,但壓縮比最差,-9最慢,但是壓縮比最好,默認是-6

示例:

bzip2 -v 待壓縮文件名
bzip2 -d 壓縮后的文件名
bzip2 -9 -c 待壓縮的文件名 > 自定義壓縮文件名

xz

雖然bzip2已經具有很棒的壓縮比,不過顯然某些自由軟件開發者還不滿足,因此後來還推出了xz這個壓縮比更高的軟件。這個軟件的用法也跟gzip/bzip2幾乎一模一樣,那我們就來看一看。

xz [-cdtlkn] 文件名
選項與參數:
-c: 將壓縮的數據輸出到屏幕上,可通過數據流重定向來處理;
-d: 解壓縮的參數;
-k: 保留原始文件,而不是刪除原始文件;
-l: 列出壓縮文件的相關信息;
-t: 測試壓縮文件的完整性,看看有沒有錯誤;
-z: 壓縮的參數(默認值,可以不加);
-n: n為数字的意思,代表壓縮等級,-1最快,但壓縮比最差,-9最慢,但是壓縮比最好,默認是-6

示例:

xz -v 待壓縮的文件名
xz -l 壓縮后的文件名
xz -d 壓縮后的文件名
xz -k 待壓縮的文件名

打包命令

前面談到的命令大多僅能針對單一文件來進行壓縮,雖然gzip、bzip2、xz也能夠針對目錄來進行壓縮,不過,這幾個命令對目錄的壓縮指的是將目錄內的所有文件【分別】進行壓縮的操作。而不像在Windows的系統,可以使用類似WinRAR這一類的壓縮軟件來將好多數據包成一個文件的樣式。
這種將多個文件或目錄包成一個大文件的命令功能,我們可以稱它是一種打包命令,那Linux有沒有這種打包命令?有,那就是大名鼎鼎的tar,tar可以將多個目錄或文件打包成一個大文件,同時還可以通過gzip、bzip2、xz的支持,將該文件同時進行壓縮。更有趣的是,由於tar的使用太廣泛了,目前Windows的WinRAR也支持.tar.gz文件名的解壓縮。

tar

tar的選項與參數特別多,我們只講幾個常用的選項,更多選項您可以自行man tar查詢。

tar [-z|-j|-J] [cv] [-f 待建立的新文件名] filename... <== 打包與壓縮。
tar [-z|-j|-J] [cv] [-f 既有的tar文件名]  <== 查看文件名
tar [-z|-j|-J] [xv] [-f 既有的tar文件名]  <== 解壓縮
選項與參數:
-c: 建立打包文件,可搭配-v來查看過程中被打包的文件名(filename);
-t: 查看打包文件的內容含有那些文件名,重點在查看【文件名】;
-x: 解包或解壓縮功能,可以搭配-C(大寫)在特定目錄解壓,特別留意的是,-c、-t、-x不可同時出現在一串命令行中;
-z: 通過gzip的支持進行壓縮/解壓縮: 此時文件名最好為*.tar.gz;
-j: 通過bzip2的支持進行壓縮/解壓縮:此時文件名最好為*.tar.bz2;
-J: 通過xz的支持進行壓縮/解壓縮: 此時文件名最好為 *.tar.xz,特別留意,-z、-j、-J不可以同時出現在一串命令行中;
-v: 在壓縮/解壓縮的過程中,將正在處理的文件名显示出來;
-f filename: -f後面要立刻接要被處理的文件名,建議-f單獨寫一個選項(比較不會忘記)。
-C 目錄: 這個選項用在解壓縮,若要在特定目錄解壓縮,可以使用這個選項
-p(小寫): 保留備份數據的原本權限與屬性,常用於備份(-c)重要的配置文件;
-P(大寫): 保留絕對路徑,亦即允許備份數據中含有根目錄存在之意

其實最簡單的使用tar就只要記住下面的命令即可:

  • 壓縮: tar -jcv -f filename.tar.bz2 要被壓縮的文件或目錄名稱;
  • 查詢: tar -jtv -f filename.tar.bz2
  • 解壓縮: tar -jxv -f filename.tar.bz2 -C 欲解壓縮的目錄

示例:

tar -zcvf 文件名.tar.gz 文件名(目錄)

tar -ztvf 文件名.tar.gz

tar -zxvf 文件名.tar.gz

資料:

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

※公開收購3c價格,不怕被賤賣!

※想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

※不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置,台北網頁設計,各種案例分享

【決戰西二旗】|你真的懂快速排序?

本文首發於:

微信公眾號:後端技術指南針

持續乾貨 歡迎關注!

看似青銅實則王者

很多人提起快排和二分都覺得很容易的樣子,但是讓現場Code很多就翻車了,就算可以寫出個遞歸版本的代碼,但是對其中的複雜度分析、邊界條件的考慮、非遞歸改造、代碼優化等就無從下手,填鴨背誦基本上分分鐘就被面試官擺平了。

 

那年初識快速排序

快速排序Quicksort又稱劃分交換排序partition-exchange sort,簡稱快排,一種排序算法。最早由東尼·霍爾(C. A. R. Hoare)教授在1960年左右提出,在平均狀況下,排序n個項目要O(nlogn)次比較。

在最壞狀況下則需要O(n^2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他算法更快,因為它的內部循環可以在大部分的架構上很有效率地達成。

快速排序的核心思想

在計算機科學中,分治法(Divide&Conquer)是建基於多項分支遞歸的一種很重要的算法範式,快速排序是分治思想在排序問題上的典型應用。

所謂分治思想D&C就是把一個較大規模的問題拆分為若干小規模且相似的問題。再對小規模問題進行求解,最終合併所有小問題的解,從而形成原來大規模問題的解。

字面上的解釋是”分而治之”,這個技巧是很多高效算法的基礎,如排序算法(歸併排序、快速排序)、傅立恭弘=叶 恭弘變換(快速傅立恭弘=叶 恭弘變換)。

分治法中最重要的部分是循環遞歸的過程,每一層遞歸有三個具體步驟:

  • 分解:將原問題分解為若干個規模較小,相對獨立,與原問題形式相同的子問題。
  • 解決:若子問題規模較小且易於解決時,則直接解。否則,遞歸地解決各子問題。
  • 合併:將各子問題的解合併為原問題的解。

快速排序的發明者

查爾斯·安東尼·理查德·霍爾爵士(Sir Charles Antony Richard Hoare縮寫為C. A. R. Hoare,1934年1月11日-),昵稱為東尼·霍爾(Tony Hoare),生於大英帝國錫蘭可倫坡(今斯里蘭卡),英國計算機科學家,圖靈獎得主。

他設計了快速排序算法、霍爾邏輯、交談循序程式。在操作系統中,他提出哲學家就餐問題,併發明用來作為同步程序的監視器(Monitors)以解決這個問題。他同時證明了監視器與信號標(Semaphore)在邏輯上是等價的。

1980年獲頒圖靈獎、1982年成為英國皇家學會院士、2000年因為他在計算機科學與教育方面的傑出貢獻,獲得英國王室頒贈爵士頭銜、2011年獲頒約翰·馮諾依曼獎,現為牛津大學榮譽教授,並在劍橋微軟研究院擔任研究員。

快速排序的基本過程

快速排序使用分治法來把一個序列分為小於基準值和大於基準值的兩個子序列。

遞歸地排序兩個子序列,直至最小的子序列長度為0或者1,整個遞歸過程結束,詳細步驟為:

  • 挑選基準值: 從數列中挑出一個元素稱為基準pivot,選取基準值有數種具體方法,此選取方法對排序的時間性能有決定性影響。
  • 基準值分割: 重新排序數列,所有比基準值小的元素擺放在基準前面,所有比基準值大的元素擺在基準後面,與基準值相等的數可以到任何一邊,在這個分割結束之後,對基準值的排序就已經完成。
  • 遞歸子序列: 遞歸地將小於基準值元素的子序列和大於基準值元素的子序列排序,步驟同上兩步驟,遞歸終止條件是序列大小是0或1,因為此時該數列顯然已經有序。

快速排序的遞歸實現

    • 版本一 C實現
#include<stdio.h>

int a[9]={5,1,9,6,7,11,3,8,4};

void exchange(int *p,int *q){
    int temp=*p;
    *p=*q;
    *q=temp;
}

int quicksort(int left,int right){
    if(left>=right){
        return 0;
    }

    int i,j,temp;
    temp=a[left];
    i=left;
    j=right;

    while(i!=j){
        while(i<j&&a[j]>=temp){
            j--;
        }
        exchange(&a[i],&a[j]); 
        while(i<j&&a[i]<=temp){
            i++; 
        }
        exchange(&a[i],&a[j]);
    }
    quicksort(i+1,right);
    quicksort(left,i-1); 
}

int main(){
    quicksort(0,8);
    for(int i=0;i<=8;i++){
        printf("%d ",a[i]);
    }
}
    • 版本二 C++實現
 1 #include<iostream>
 2 using namespace std;
 3 
 4 template <typename T>
 5 void quick_sort_recursive(T arr[], int start, int end) {
 6     if (start >= end)
 7         return;
 8     T mid = arr[end];
 9     int left = start, right = end - 1;
10     //整個範圍內搜尋比樞紐值小或大的元素,然後左側元素與右側元素交換
11     while (left < right) {
12             //試圖在左側找到一個比樞紐元更大的元素
13         while (arr[left] < mid && left < right)
14             left++;
15                 //試圖在右側找到一個比樞紐元更小的元素
16         while (arr[right] >= mid && left < right)
17             right--;
18                 //交換元素
19         std::swap(arr[left], arr[right]);
20     }
21         //這一步很關鍵
22     if (arr[left] >= arr[end])
23         std::swap(arr[left], arr[end]);
24     else
25         left++;
26     quick_sort_recursive(arr, start, left - 1);
27     quick_sort_recursive(arr, left + 1, end);
28 }
29 
30 //模板化
31 template <typename T> 
32 void quick_sort(T arr[], int len) {
33     quick_sort_recursive(arr, 0, len - 1);
34 }
35 
36 int main()
37 {
38     int a[9]={5,1,9,6,7,11,3,8,4};
39     int len = sizeof(a)/sizeof(int);
40     quick_sort(a,len-1);
41     for(int i=0;i<len-1;i++)
42         cout<<a[i]<<endl;
43 }

兩個版本均可正確運行,但代碼有一點差異:

  • 版本一 使用雙指針交替從左(右)兩邊分別開始尋找大於基準值(小於基準值),然後與基準值交換,直到最後左右指針相遇。
  • 版本二 使用雙指針向中間集合,左指針遇到大於基準值時則停止,等待右指針,右指針遇到小於基準值時則停止,與左指針指向的元素交換,最後基準值放到合適位置。

過程說起來比較抽象,穩住別慌!靈魂畫手大白會畫圖來演示這兩個過程。

快速排序的遞歸演示

  • 版本一遞歸代碼的排序過程示意圖:

以第一次遞歸循環為例:

步驟1: 選擇第一個元素為基準值pivot=a[left]=5,right指針指向尾部元素,此時先由right自右向左掃描直至遇到<5的元素,恰好right起步元素4<5,因此需要將4與5互換位置;

步驟2: 4與5互換位置之後,輪到left指針從左向右掃描,注意一下left的起步指針指向了由步驟1交換而來的4,新元素4不滿足停止條件,因此left由綠色虛箭頭4位置遊走到元素9的位置,此時left找到9>5,因此將此時left和right指向的元素互換,也就是元素5和元素9互換位置;

步驟3: 互換之後right指針繼續向左掃描,從藍色虛箭頭9位置遊走到3的位置,此時right發現3<5,因此將此時left和right指向的元素互換,也就是元素3和元素5互換位置;

步驟4: 互換之後left指針繼續向右掃描,從綠色虛箭頭3位置遊走到6的位置,此時left發現6>5,因此將此時left和right指向的元素互換,也就是元素6和元素5互換位置;

步驟5: 互換之後right指針繼續向左掃描,從藍色虛箭頭6位置一直遊走到與left指針相遇,此時二者均停留在了pivot=5的新位置上,且左右兩邊分成了兩個相對於pivot值的子序列;

循環結束:至此出現了以5為基準值的左右子序列,接下來就是對兩個子序列實施同樣的遞歸步驟。

以第二次和第三次左子序列遞歸循環為例:

步驟1-1:選擇第一個元素為基準值pivot=a[left]=4,right指針指向尾部元素,此時先由right指針向左掃描,恰好起步元素3<4,因此將3和4互換;

步驟1-2:互換之後left指針從元素3開始向右掃描,一直遊走到與right指針相遇,此時本次循環停止,特別注意這種情況下可以看到基準值4隻有左子序列,無右子序列,這種情況是一種退化,就像冒泡排序每次循環都將基準值放置到最後,因此效率將退化為冒泡的O(n^2);

步驟1-3:選擇第一個元素為基準值pivot=a[left]=3,right指針指向尾部元素,此時先由right指針向左掃描,恰好起步元素1<3,因此將1和3互換;

步驟1-4:互換之後left指針從1開始向右掃描直到與right指針相遇,此時注意到pivot=3無右子序列且左子序列len=1,達到了遞歸循環的終止條件,此時可以認為由第一次循環產生的左子序列已經全部有序。

循環結束:至此左子序列已經排序完成,接下來對右子序列實施同樣的遞歸步驟,就不再演示了,聰明的你一定get到了。

特別注意:

以上過程中left和right指針在某個元素相遇,這種情況在代碼中是不會出現的,因為外層限制了i!=j,圖中之所以放到一起是為了直觀表達終止條件。

  • 版本二C++版本動畫演示:

 

分析一下:

個人覺得這個版本雖然同樣使用D&C思想但是更加簡潔,從動畫可以看到選擇pivot=a[end],然後左右指針分別從index=0和index=end-1向中間靠攏。

過程中掃描目標值並左右交換,再繼續向中間靠攏,直到相遇,此時再根據a[left]和a[right]以及pivot的值來進行合理置換,最終實現基於pivot的左右子序列形式。

腦補場景:

上述過程讓我覺得很像統帥命令左右兩路軍隊從兩翼會和,並且在會和過程中消滅敵人有生力量(認為是交換元素),直到兩路大軍會師。

此時再將統帥王座擺到正確的位置,此過程中沒有統帥王座的反覆變換,只有最終會師的位置,以王座位中心形成了左翼子序列和右翼子序列。

再重複相同的過程,直至完成大一統。

腦補不過癮 於是湊圖一張:

快速排序的多種版本

吃瓜時間:

印象中2017年初換工作的時候去CBD一家公司面試手寫快排,我就使用C++模板化的版本二實現的,但是面試官質疑說這不是快排,爭辯之下讓我們彼此都覺得對方很Low,於是很快就把我送出門SayGoodBye了^_^。

我想表達的意思是,雖然快排的遞歸版本是基於D&C實現的,但是由於pivot值的選擇不同、交換方式不同等諸多因素,造成了多種版本的遞歸代碼。

並且內層while循環裏面判斷>=還是>(即是否等於的問題),外層循環判斷本序列循環終止條件等寫法都會不同,因此在寫快排時切忌死記硬背,要不然邊界條件判斷不清楚很容易就死循環了。

看下上述我貼的兩個版本的代碼核心部分:

//版本一寫法
while(i!=j){
    while(i<j&&a[j]>=temp){
        j--;
    }
    exchange(&a[i],&a[j]); 
    while(i<j&&a[i]<=temp){
        i++; 
    }
    exchange(&a[i],&a[j]);
}

//版本二寫法
while (left < right) {
    while (arr[left] < mid && left < right)
        left++;
    while (arr[right] >= mid && left < right)
        right--;
    std::swap(arr[left], arr[right]);
}

覆蓋or交換:

代碼中首先將pivot的值引入局部變量保存下來,這樣就認為A[L]這個位置是個坑,可以被其他元素覆蓋,最終再將pivot的值填到最後的坑裡。

這種做法也沒有問題,因為你只要畫圖就可以看到,每次坑的位置是有相同元素的位置,也就是被備份了的元素。

個人感覺 與其叫坑不如叫備份,但是如果你代碼使用的是基於指針或者引用的swap,那麼就沒有坑的概念了。

這就是覆蓋和交換的區別,本文的例子都是swap實現的,因此沒有坑位被最後覆蓋一次的過程。

快速排序的迭代實現

所謂迭代實現就是非遞歸實現一般使用循環來實現,我們都知道遞歸的實現主要是藉助系統內的棧來實現的。

如果調用層級過深需要保存的臨時結果和關係會非常多,進而造成StackOverflow棧溢出。

Stack一般是系統分配空間有限內存連續速度很快,每個系統架構默認的棧大小不一樣,筆者在x86-CentOS7.x版本使用ulimit -s查看是8192Byte。

避免棧溢出的一種辦法是使用循環,以下為筆者驗證的使用STL的stack來實現的循環版本,代碼如下:

#include <stack>
#include <iostream>
using namespace std;

template<typename T>
void qsort(T lst[], int length) {
    std::stack<std::pair<int, int> > mystack;
    //將數組的首尾下標存儲 相當於第一輪循環
    mystack.push(make_pair(0, length - 1));

    while (!mystack.empty()) {
        //使用棧頂元素而後彈出
        std::pair<int,int> top = mystack.top();
        mystack.pop();

        //獲取當前需要處理的子序列的左右下標
        int i = top.first;
        int j = top.second;

        //選取基準值
        T pivot = lst[i];

        //使用覆蓋填坑法 而不是交換哦
        while (i < j) {
            while (i < j and lst[j] >= pivot) j--;
            lst[i] = lst[j];
            while (i < j and lst[i] <= pivot) i++;
            lst[j] = lst[i];
        }
        //注意這個基準值回填過程
        lst[i] = pivot;

        //向下一個子序列進發
        if (i > top.first) mystack.push(make_pair(top.first, i - 1));
        if (j < top.second) mystack.push(make_pair(j + 1, top.second));
    }
}

int main()
{
    int a[9]={5,1,9,6,7,11,3,8,4};
    int len = sizeof(a)/sizeof(int);
    qsort(a,len);
    for(int i=0;i<len-1;i++)
        cout<<a[i]<<endl;
}

下期精彩

由於篇幅原因,目前文章已經近6000字,因此筆者決定將快排算法的優化放到另外一篇文章中,不過可以提前預告一下會有哪些內容:

  • 基準值選擇對性能的影響
  • 基準值選擇的多種策略
  • 尾遞歸的概念原理和優勢
  • 基於尾遞歸實現快速排序
  • STL中sort函數的底層實現
  • glibc中快速排序的實現

參考資料

 

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

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

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

碼農當自強

碼農當自強

導航

  • 初出茅廬
  • 跳槽才能漲薪
  • 力拔山兮氣蓋世
  • 止步中層
  • 契約精神?聯盟
  • 技工?匠人
  • 碼農當自強

  有人的地方就有江湖。有江湖必有俠客。IT人的江湖水生草闊,從來都盛產俠客和隱士。很多人離開這片江湖,沒有留下自己的故事,而那些有故事的終究成了傳說。

初出茅廬

  本文的主人公木木君,2011年畢業於一個普通二本大學的計算機專業。那年六月,他懷揣夢想,來到西部的一座准一線城市。

  “天高任鳥飛,海闊憑魚游”。同大多數應屆畢業生一樣,木木君懷揣夢想,滿腔熱心,對未來充滿希冀,希望能夠在這座大城市打拚出自己的一片天地。“求突破,求提高,求發展”,這是他給自己設定的未來五年計劃,分三個步驟執行。

  IT行業的門檻向來很高,大多數民企很少招生應屆生,特別是非985,211大學的童鞋,容易碰壁。木木君面試了20家大大小小的公司,花了一個多月,總算找了個正規公司。這是一家生產機頂盒的工廠,幾千人的公司,僅有不到50人的研發團隊。木木君在這裏開啟了自己的IT職業生涯。團隊不比正規的軟件公司,但同事和睦相處,也能夠接觸到實際的開發項目,總算有所突破。

  “笨鳥先飛”。木木君憑藉自己的努力,半年之內就把部門內部的項目都摸了一遍,並在原有基礎上增加了很多功能。

跳槽才能漲薪

  IT江湖潛規則之跳槽才能漲薪。

  有一天,木木君和往常一樣正在配合運維同事改進新的OA系統。“滴滴滴~”,一波QQ消息來襲。木木君點開消息,是師兄。木木君和這個師兄其實不是一個專業,師兄比他高一個年級,因為大學被同一個老師帶着一起做過項目,經常串寢室,比較熟悉。師兄告訴他,他換工作了,這是他兩年來第三次換工作,在軟件園,月薪8K。聊天完畢,木木君沉思良久,開始對高大上的軟件園產生嚮往。

  在第一家公司待了11個月,木木君選擇了離開,換到了一家軟件園的公司,並有機會和華為的工程師一起合作開發項目。值得一提的是,這次跳槽工資幾乎翻了一倍。

  新公司是行業里排名靠前的,公司制度規範,開發流程標準。團隊里研發同事嚴謹,當然壓力也很大。

  剛進公司的前兩個月,壓力挺大,見識了以前沒有接觸過的框架和組件,以及很多聽不懂的術語。每晚和同事加班到8點半,有時甚至11點才從公司離開,儘管很累,但是能夠感受的技術和經驗上的進步。

  在這家公司待了兩年。見識 了一些牛人,甚至有些一個人頂一個小團隊的人。華為的狼性文化在木木君心中打下烙印,深入血液,變成做事風格的一部分。在以後的工作中,也是秉持了這種文化。

力拔山兮氣蓋世

  大多數碼農,在工作三年左右能夠迎來自己的一個技術上的小高峰。

  木木君在第二家公司待了兩年感覺就像到山上跟了一個武林高手學了一身本領,瞬間有了自信。離職的時候,我和我同事還開玩笑說,出去之後至少都是8K…

  “移動互聯網時代已經來臨,站在風口上豬也能飛”。那一年,手機APP大行其道,獨領風騷,是個公司都想做APP。y也是在那一年小米火了,華為剛開始邁入手機領域。進入第三家公司,是一個不到一百人的軟件公司,做旅遊APP的,期望在這裡能夠接觸到移動端。

  “圈子不同,不硬融”。木木君在這家公司真正見識了什麼是公司內耗。小幫派林立,你再努力也無法融入。一年不到,領導換了幾茬,還玩的是家天下。

止步中層

  對大多數人而言,職場的中層就是天花板。

  “天下之大,竟無用武之地”。就像一個武林俠客,讓他困惑不是練武的孤獨和寂寞,而是竟然沒有用武之地。

  “此處不留爺,自有留爺處”。很快,木木君便進入一件互聯網產品公司,主營電商相關Saas軟件。這裏部門分工明確,需求,產品研發,安全,運維,DBA,測試一應俱全。公司產品成熟,客戶穩定,可以學習真正的大數據和自動化運維。

  在這裏團隊從零開始搭建自動化平台,並逐步迭代,一路摸爬滾打,基本能夠滿足公司100多台服務器的自動化運維。

  木木君一腔熱情,終於受到領導器重,升入中層。團隊不大,但是業務不少,支撐多個部門的系統研發和運維,幾乎人人都掛了2+項目。身為leader的木木君,更是不在話下。特殊時期,幾乎一人承擔一個團隊開發任務。甚至非常時期通宵支持…

  四年過去,木木君也進入了而立之年。2018年,經濟不景氣的一年…公司漲薪已經渺茫…陸續身邊逐漸有人跳槽。不到半年,身邊已經有三個人跳槽,其中一個老領導走了留下一句“待了六年,已經沒有上升空間”。

契約精神?聯盟

  我們是一個團隊,不是一個家庭。

  企業跟員工應該是一種什麼樣的關係?

  領英的執行總裁在《聯盟》這本書中開頭就寫道:“我們是一個團隊,不是一個家庭”。

  近期屢屢爆出的HR被辭退事件和員工因患病被辭退事件,引起了輿論共鳴。但是希望大家認識到員工和公司的關係是合作和聯盟關係。未來更是如此…

技工?匠人

  碼農,一群靠技術謀生的人。只是在互聯網的光環加持下,變得“高精尖”。但是,放在歷史的長河中來看,也不過是特定時代的勞動者。他們和上一個時代的磚瓦工,木匠其實沒有太大差別。是社會生產力發展到一定階段,一種工種對另一種工種的替代。

  互聯網給技術人帶來紅利,容易讓技術人感到天生的優越感,再加上身處大廠就容易自我感覺良好。

  吳軍博士對的碼農層級的分類,可以看出,技術人的發展方向不只是在技術本身,還要具備綜合能力。

  • 第五級:能獨立解決問題,完成工程工作。

  • 第四級:能指導和帶領其他人一同完成更有影響力的工作。

  • 第三級:能獨立設計和實現產品,並且在市場上獲得成功。

  • 第二級:能設計和實現別人不能做出的產品,也就是說他的作用很難取代。

  • 第一級:開創一個產業。

  試問諸君在第幾層?

  說到底,技術人應該秉持匠人精神。

碼農當自強

  生活不止眼前的苟且,還有詩和遠方。

  2019年各大公司的財報,都反應很多公司效益差強人意。很多互聯網公司也在尋找新的風口和增長點。而立之年的木木君,雖然躊躇滿志,但再次陷入迷茫。深處IT行業多年,幾乎五年一個風口,技術行業更新快,玩的是創新和顛覆。移動互聯網時代是如何革傳統行業的命,木木君歷歷在目…

  同時,各大媒體充斥着中年危機的推文,一時間人人自危,催生了各種打着知識旗號販賣焦慮的二道販子。不禁讓木木君想起了之前有個大神的文章《屌絲的出路》。那個大神也是一段傳奇。

  很多碼農都在焦慮,但是又不知道如何做?技術人有一個通病,純粹。這不是缺點,但是生活是多元的,要多主動接觸技術之外的世界。

  • 副業

  同時,可以多一些副業嘗試。比如,和朋友一起接一些項目。創建自己的博客。口才好的,可以錄一些技術教學視頻放到網上。

  • 健身

  身體是革命的本錢,這裏的健身不是一定要去健身房,而是要學會鍛煉身體和合理作息。

  • 投資

  年輕的時候,做一點投資和理財。投資房產也是不錯的選擇。投資可以為未來增加一筆資產。

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

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

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選