拿來即用:用C+JS結構來處理JSON數據

【面對的問題】

        在物聯網產品的開發過程中,對JSON格式的數據處理是一個強需求,例如亞馬遜的 AWS IOT平台,設備與後台之間的通訊數據都是JSON格式,先瞄一眼大概的樣子:

     這是一個真實產品的通訊數據,設備端的代碼C代碼中利用cJSON這個開源工具來完成JSON字符的解析和組裝工作。代碼我這裏就不貼了,解析函數很長,要滾動好多次鼠標滑輪。而且一不注意釋放資源就會發生內存泄漏!

    那麼,是否有更好的方式來解決這個問題呢?

    答案就是這篇文章介紹的duktape引擎!

【Duktape簡介】

    這裏只是簡單介紹下duktape,詳細的介紹大家自己去google。

    Duktape 是一個嵌入式 Javascript引擎,專註於可移植性和空間佔用。

    易於集成到C/C++項目中,使用API實現C代碼與JS代碼的雙向調用。

 

【代碼說明】

1.文件說明

 

 

    duktape.c:引擎主要C文件。

    duktape.h:引擎頭文件。

    main.c      :  main函數所在文件。

   main.js      :Javascript文件,處理業務邏輯的代碼就放在這裏。

    Makefile   :  編譯腳本。

    miniz.c     :有時候為了js代碼的保密,不能把js源碼放到最終產品中,需要壓縮和混淆;加載的時候再進行解壓。

2.核心步驟 

 

    當C代碼中需要對JSON格式的字符串進行處理時,把JSON數據通過棧結構傳給JS程序,在JS程序中處理數據之後,把處理結果再返回給C程序中。

    在JS程序中,如果有些操作無法處理(例如:發送數據給串口),那麼就調用C程序中的函數來處理。

3.代碼說明

 

 

【測試環境】

1. x86系統

    我是在  Ubuntu16.04 下測試的,使用系統自帶 gcc 編譯器。

2.嵌入式系統

    只需要把編譯器換成對應的交叉編譯器即可。

 

【END】

1.這是原創文章,請尊重版權。如需轉載,請保留全部內容並註明來源。如果方便的話,請聯繫我確認。

2.文章中如有錯誤,或者希望交流、探討相關內容,非常歡迎聯繫我。

3.郵箱:sewain@126.com

4.公眾號:IOT物聯網小鎮

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

【其他文章推薦】

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

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

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

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

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

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

併發編程 —— 線程池

概述

在程序中,我們會用各種池化技術來緩存創建昂貴的對象,比如線程池、連接池、內存池。一般是預先創建一些對象放入池中,使用的時候直接取出使用,用完歸還以便復用,還會通過一定的策略調整池中緩存對象的數量,實現池的動態伸縮。

由於線程的創建比較昂貴,隨意、沒有控制地創建大量線程會造成性能問題,因此短平快的任務一般考慮使用線程池來處理,而不是直接創建線程。

那麼,如何正確的創建並正確的使用線程池呢,這篇文章就來細看下。

線程池

雖然在 Java 語言中創建線程看上去就像創建一個對象一樣簡單,只需要 new Thread() 就可以了,但實際上創建線程遠不是創建一個對象那麼簡單。

創建對象,僅僅是在 JVM 的堆里分配一塊內存而已;而創建一個線程,卻需要調用操作系統內核的 API,然後操作系統要為線程分配一系列的資源,這個成本就很高了。所以線程是一個重量級的對象,應該避免頻繁創建和銷毀,一般就是採用線程池來避免頻繁的創建和銷毀線程。

 

線程池原理

Java 通過用戶線程與內核線程結合的 1:1 線程模型來實現,Java 將線程的調度和管理設置在了用戶態。在 HotSpot VM 的線程模型中,Java 線程被一對一映射為內核線程。Java 在使用線程執行程序時,需要創建一個內核線程;當該 Java 線程被終止時,這個內核線程也會被回收。因此 Java 線程的創建與銷毀將會消耗一定的計算機資源,從而增加系統的性能開銷。

除此之外,大量創建線程同樣會給系統帶來性能問題,因為內存和 CPU 資源都將被線程搶佔,如果處理不當,就會發生內存溢出、CPU 使用率超負荷等問題。

為了解決上述兩類問題,Java 提供了線程池概念,對於頻繁創建線程的業務場景,線程池可以創建固定的線程數量,並且在操作系統底層,輕量級進程將會把這些線程映射到內核。

線程池可以提高線程復用,又可以固定最大線程使用量,防止無限制地創建線程。當程序提交一個任務需要一個線程時,會去線程池中查找是否有空閑的線程,若有,則直接使用線程池中的線程工作,若沒有,會去判斷當前已創建的線程數量是否超過最大線程數量,如未超過,則創建新線程,如已超過,則進行排隊等待或者直接拋出異常。

 

線程池是一種生產者 – 消費者模式

線程池的設計,普遍採用的都是生產者 – 消費者模式。線程池的使用方是生產者,線程池本身是消費者。

原理實現大致如下:

 1 package com.lyyzoo.test.concurrent.executor;  2 
 3 import java.util.ArrayList;  4 import java.util.List;  5 import java.util.concurrent.BlockingQueue;  6 import java.util.concurrent.LinkedBlockingQueue;  7 
 8 /**
 9  * @author bojiangzhou 2020/02/12 10  */
11 public class CustomThreadPool { 12 
13     public static void main(String[] args) { 14         // 使用有界阻塞隊列 創建線程池
15         CustomThreadPool pool = new CustomThreadPool(2, new LinkedBlockingQueue<>(10)); 16         pool.execute(() -> { 17             System.out.println("提交了一個任務"); 18  }); 19  } 20 
21     // 利用阻塞隊列實現生產者-消費者模式
22     final BlockingQueue<Runnable> workQueue; 23     // 保存內部工作線程
24     final List<Thread> threads = new ArrayList<>(); 25 
26     public CustomThreadPool(int coreSize, BlockingQueue<Runnable> workQueue) { 27         this.workQueue = workQueue; 28         // 創建工作線程
29         for (int i = 0; i < coreSize; i++) { 30             WorkerThread work = new WorkerThread(); 31  work.start(); 32  threads.add(work); 33  } 34  } 35 
36     // 生產者 提交任務
37     public void execute(Runnable command) { 38         try { 39             // 隊列已滿,put 會一直等待
40  workQueue.put(command); 41         } catch (InterruptedException e) { 42  e.printStackTrace(); 43  } 44  } 45 
46     /**
47  * 工作線程負責消費任務,並執行任務 48      */
49     class WorkerThread extends Thread { 50  @Override 51         public void run() { 52             // 循環取任務並執行,take 取不到任務會一直等待
53             while (true) { 54                 try { 55                     Runnable runnable = workQueue.take(); 56  runnable.run(); 57                 } catch (InterruptedException e) { 58  e.printStackTrace(); 59  } 60  } 61  } 62  } 63 }

ThreadPoolExecutor

線程池參數說明

Java 提供的線程池相關的工具類中,最核心的是 ThreadPoolExecutor,通過名字也能看出來,它強調的是 Executor,而不是一般意義上的池化資源。

ThreadPoolExecutor 的構造函數非常複雜,這個最完備的構造函數有 7 個參數:

 

各個參數的含義如下:

  • corePoolSize:表示線程池保有的最小線程數。
  • maximumPoolSize:表示線程池創建的最大線程數。
  • keepAliveTime & unit:如果一個線程空閑了 keepAliveTime & unit 這麼久,而且線程池的線程數大於 corePoolSize ,那麼這個空閑的線程就要被回收了。
  • workQueue:工作隊列,一般定義有界阻塞隊列。
  • threadFactory:通過這個參數你可以自定義如何創建線程,例如你可以給線程指定一個有意義的名字。
  • handler:通過這個參數可以自定義任務的拒絕策略。如果線程池中所有的線程都在忙碌,並且工作隊列也滿了(前提是工作隊列是有界隊列),那麼此時提交任務,線程池就會拒絕接收。ThreadPoolExecutor 已經提供了以下 4 種拒絕策略。
    •   CallerRunsPolicy:提交任務的線程自己去執行該任務。
    •   AbortPolicy:默認的拒絕策略,會 throws RejectedExecutionException。
    •   DiscardPolicy:直接丟棄任務,沒有任何異常拋出。
    •   DiscardOldestPolicy:丟棄最老的任務,其實就是把最早進入工作隊列的任務丟棄,然後把新任務加入到工作隊列。

 

ThreadPoolExecutor 構造完成后,還可以通過如下方法定製默認行為:

  • executor.allowCoreThreadTimeOut(true):將包括“核心線程”在內的,沒有任務分配的所有線程,在等待 keepAliveTime 時間后回收掉。
  • executor.prestartAllCoreThreads():創建線程池后,立即創建核心數個工作線程;線程池默認是在任務來時才創建工作線程。

 

創建線程池示例:

 1 public void test() throws InterruptedException {  2     ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(  3             // 核心線程數
 4             2,  5             // 最大線程數
 6             16,  7             // 線程空閑時間
 8             60, TimeUnit.SECONDS,  9             // 使用有界阻塞隊列
10             new LinkedBlockingQueue<>(1024), 11             // 定義線程創建方式,可自定線程名稱
12             new ThreadFactoryBuilder().setNameFormat("executor-%d").build(), 13             // 自定義拒絕策略,一般和降級策略配合使用
14             (r, executor) -> { 15                 // 隊列已滿,拒絕執行
16                 throw new RejectedExecutionException("Task " + r.toString() +
17                         " rejected from " + executor.toString()); 18  } 19  ); 20 
21     poolExecutor.submit(() -> { 22         LOGGER.info("submit task"); 23  }); 24 }

 

線程池的線程分配流程

任務提交后的大致流程如下圖所示。提交任務后,如果線程數小於 corePoolSize,則創建新線程執行任務,無論當前線程池的線程是否空閑都會創建新的線程。

當創建的線程數等於 corePoolSize 時,提交的任務會被加入到設置的阻塞隊列中。

當隊列滿了,則會創建非核心線程執行任務,直到線程池中的線程數量等於 maximumPoolSize。

當線程數量已經等於 maximumPoolSize 時, 新提交的任務無法加入到等待隊列,也無法創建非核心線程直接執行,如果沒有為線程池設置拒絕策略,這時線程池就會拋出 RejectedExecutionException 異常,即默認拒絕接受任務。

 

線程池默認的拒絕策略就是丟棄任務,所以我們在設置有界隊列時,需要考慮設置合理的拒絕策略,要考慮到高峰時期任務的數量,避免任務被丟棄而影響業務流程。

 

強烈建議使用有界隊列

創建 ThreadPoolExecutor 時強烈建議使用有界隊列。如果設置為無界隊列,那麼一般最大線程數的設置是不起作用的,而且遇到任務高峰時,如果一直往隊列添加任務,容易出現OOM,拋出如下異常。

Exception in thread "http-nio-45678-ClientPoller" java.lang.OutOfMemoryError: GC overhead limit exceeded

 

使用有界隊列時,需要注意,當任務過多時,線程池會觸發執行拒絕策略,線程池默認的拒絕策略會拋出 RejectedExecutionException,這是個運行時異常,對於運行時異常編譯器並不強制 catch 它,所以開發人員很容易忽略,因此默認拒絕策略要慎重使用。如果線程池處理的任務非常重要,建議自定義自己的拒絕策略;並且在實際工作中,自定義的拒絕策略往往和降級策略配合使用。

 

監控線程池的狀態

建議用一些監控手段來觀察線程池的狀態。線程池這個組件往往會表現得任勞任怨、默默無聞,除非是出現了拒絕策略,否則壓力再大都不會拋出一個異常。如果我們能提前觀察到線程池隊列的積壓,或者線程數量的快速膨脹,往往可以提早發現並解決問題。

 1 public static void displayThreadPoolStatus(ThreadPoolExecutor threadPool, String threadPoolName, long period, TimeUnit unit) {
 2     Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
 3         LOGGER.info("[>>ExecutorStatus<<] ThreadPool Name: [{}], Pool Status: [shutdown={}, Terminated={}], Pool Thread Size: {}, Active Thread Count: {}, Task Count: {}, Tasks Completed: {}, Tasks in Queue: {}",
 4                 threadPoolName,
 5                 threadPool.isShutdown(), threadPool.isTerminated(), // 線程是否被終止
 6                 threadPool.getPoolSize(), // 線程池線程數量
 7                 threadPool.getActiveCount(), // 工作線程數
 8                 threadPool.getTaskCount(), // 總任務數
 9                 threadPool.getCompletedTaskCount(), // 已完成的任務數
10                 threadPool.getQueue().size()); // 線程池中線程的數量
11     }, 0, period, unit);
12 }

線程池任務提交方式

提交任務可以通過 execute 和 submit 方法提交任務,下面就來看下它們的區別。

submit 方法簽名:

execute 方法簽名:

 

使用 execute 提交任務

使用 execute 提交任務,線程池內拋出異常會導致線程退出,線程池只能重新創建一個線程。如果每個異步任務都以異常結束,那麼線程池可能完全起不到線程重用的作用。

而且主線程無法捕獲(catch)到線程池內拋出的異常。因為沒有手動捕獲異常進行處理,ThreadGroup 幫我們進行了未捕獲異常的默認處理,向標準錯誤輸出打印了出現異常的線程名稱和異常信息。顯然,這種沒有以統一的錯誤日誌格式記錄錯誤信息打印出來的形式,對生產級代碼是不合適的。

 

如下,execute 提交任務,拋出異常后,從線程名稱可以看出,老線程退出,創建了新的線程。

ThreadGroup 處理未捕獲異常:直接輸出到 System.err

 

解決方式:

  • 以 execute 方法提交到線程池的異步任務,最好在任務內部做好異常處理;
  • 設置自定義的異常處理程序作為保底,比如在聲明線程池時自定義線程池的未捕獲異常處理程序。或者設置全局的默認未捕獲異常處理程序。
 1 // 自定義線程池的未捕獲異常處理程序
 2 ThreadPoolExecutor executor = new ThreadPoolExecutor(8, 8,  3         30, TimeUnit.MINUTES,  4         new LinkedBlockingQueue<>(),  5         new ThreadFactoryBuilder()  6                 .setNameFormat("pool-%d")  7                 .setUncaughtExceptionHandler((Thread t, Throwable e) -> {  8                     log.error("pool happen exception, thread is {}", t, e);  9  }) 10  .build()); 11                 
12 // 設置全局的默認未捕獲異常處理程序
13 static { 14     Thread.setDefaultUncaughtExceptionHandler((thread, throwable)-> { 15         log.error("Thread {} got exception", thread, throwable) 16  }); 17 }  

定義的異常處理程序將未捕獲的異常信息打印到標準日誌中了,老線程同樣會退出。如果要避免這個問題,就需要使用 submit 方法提交任務。

 

使用 submit 提交任務

使用 submit,線程不會退出,但是異常不會記錄,會被生吞掉。查看 FutureTask 源碼可以發現,在執行任務出現異常之後,異常存到了一個 outcome 字段中,只有在調用 get 方法獲取 FutureTask 結果的時候,才會以 ExecutionException 的形式重新拋出異常。所以我們可以通過捕獲 get 方法拋出的異常來判斷線程的任務是否拋出了異常。

 

submit 提交任務,可以通過 Future 獲取返回結果,如果拋出異常,可以捕獲 ExecutionException 得到異常棧信息。通過線程名稱可以看出,老線程也沒有退出。

需要注意的是,使用 submit 時,setUncaughtExceptionHandler 設置的異常處理器不會生效。

 

submit 與 execute 的區別

execute提交的是Runnable類型的任務,而submit提交的是Callable或者Runnable類型的任務;

execute的提交沒有返回值,而submit的提交會返回一個Future類型的對象;

execute提交的時候,如果有異常,就會直接拋出異常,而submit在遇到異常的時候,通常不會立馬拋出異常,而是會將異常暫時存儲起來,等待你調用Future.get()方法的時候,才會拋出異常;

execute 提交的任務拋出異常,老線程會退出,線程池會立即創建一個新的線程。submit 提交的任務拋出異常,老線程不會退出;

線程池設置的 UncaughtExceptionHandler 對 execute 提交的任務生效,對 submit 提交的任務不生效。

線程數設置多少合適

創建多少線程合適,要看多線程具體的應用場景。我們的程序一般都是 CPU 計算和 I/O 操作交叉執行的,由於 I/O 設備的速度相對於 CPU 來說都很慢,所以大部分情況下,I/O 操作執行的時間相對於 CPU 計算來說都非常長,這種場景我們一般都稱為 I/O 密集型計算;和 I/O 密集型計算相對的就是 CPU 密集型計算了,CPU 密集型計算大部分場景下都是純 CPU 計算。I/O 密集型程序和 CPU 密集型程序,計算最佳線程數的方法是不同的。

 

CPU 密集型計算

多線程本質上是提升多核 CPU 的利用率,所以對於一個 4 核的 CPU,每個核一個線程,理論上創建 4 個線程就可以了,再多創建線程也只是增加線程切換的成本。所以,對於 CPU 密集型的計算場景,理論上“線程的數量 = CPU 核數”就是最合適的。不過在工程上,線程的數量一般會設置為“CPU 核數 +1”,這樣的話,當線程因為偶爾的內存頁失效或其他原因導致阻塞時,這個額外的線程可以頂上,從而保證 CPU 的利用率。

 

I/O 密集型的計算場景

如果 CPU 計算和 I/O 操作的耗時是 1:1,那麼 2 個線程是最合適的。如果 CPU 計算和 I/O 操作的耗時是 1:2,那設置 3 個線程是合適的,如下圖所示:CPU 在 A、B、C 三個線程之間切換,對於線程 A,當 CPU 從 B、C 切換回來時,線程 A 正好執行完 I/O 操作。這樣 CPU 和 I/O 設備的利用率都達到了 100%。

會發現,對於 I/O 密集型計算場景,最佳的線程數是與程序中 CPU 計算和 I/O 操作的耗時比相關的,可以總結出這樣一個公式:最佳線程數 =1 +(I/O 耗時 / CPU 耗時)

對於多核 CPU,需要等比擴大,計算公式如下:最佳線程數 =CPU 核數 * [ 1 +(I/O 耗時 / CPU 耗時)]

 

線程池線程數設置 

可通過如下方式獲取CPU核數:

1 /**
2  * 獲取返回CPU核數 3  * 4  * @return 返回CPU核數,默認為8 5  */
6 public static int getCpuProcessors() { 7     return Runtime.getRuntime() != null && Runtime.getRuntime().availableProcessors() > 0 ?
8             Runtime.getRuntime().availableProcessors() : 8; 9 }

 

在一些非核心業務中,我們可以將核心線程數設置小一些,最大線程數量設置為CPU核心數量,阻塞隊列大小根據具體場景設置;不要過大,防止大量任務進入等待隊列而超時,應儘快創建非核心線程執行任務;也不要過小,避免隊列滿了任務被拒絕丟棄。

 1 public ThreadPoolExecutor executor() {  2     int coreSize = getCpuProcessors();  3     ThreadPoolExecutor executor = new ThreadPoolExecutor(  4             2, coreSize,  5             10, TimeUnit.MINUTES,  6             new LinkedBlockingQueue<>(512),  7             new ThreadFactoryBuilder().setNameFormat("executor-%d").build(), 10             new ThreadPoolExecutor.AbortPolicy() 11  );14 
15     return executor; 16 }

 

在一些核心業務中,核心線程數設置為CPU核心數,最大線程數可根據公式 最佳線程數 =CPU 核數 * [ 1 +(I/O 耗時 / CPU 耗時)] 來計算。阻塞隊列可以根據具體業務場景設置,如果線程處理業務非常迅速,我們可以考慮將阻塞隊列設置大一些,處理的請求吞吐量會大些;如果線程處理業務非常耗時,阻塞隊列設置小些,防止請求在阻塞隊列中等待過長時間而導致請求已超時。

public ThreadPoolExecutor executor() { int coreSize = getCpuProcessors(); ThreadPoolExecutor executor = new ThreadPoolExecutor( coreSize, coreSize * 8, 30, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1024), new ThreadFactoryBuilder().setNameFormat("executor-%d").build(), new ThreadPoolExecutor.AbortPolicy() );return executor; }

 

注意:一般不要將 corePoolSize 設置為 0,例如下面的線程池,使用了無界隊列,雖 maximumPoolSize > 0,但實際上只會有一個工作線程,因為其它任務都加入等待隊列了。

1 ThreadPoolExecutor executor = new ThreadPoolExecutor(0, 5, 30, TimeUnit.SECONDS, 3         new LinkedBlockingQueue<>(), 4         new ThreadFactoryBuilder().setNameFormat("test-%d").build() 5 );

 

線程池如何優先啟用非核心線程

如果想讓線程池激進一點,優先開啟更多的線程,而把隊列當成一個後備方案,可以自定義隊列,重寫 offer 方法,因為線程池是通過 offer 方法將任務放入隊列。

 

通過重寫隊列的 offer 方法,直接返回 false,造成這個隊列已滿的假象,線程池在工作隊列滿了無法入隊的情況下會擴容線程池。直到線程數達到最大線程數,就會觸發拒絕策略,此時再通過自定義的拒絕策略將任務通過隊列的 put 方法放入隊列中。這樣就可以優先開啟更多線程,而不是進入隊列了。

 1 public static void main(String[] args) {  2     // ThreadPoolExecutor 通過 offer 將元素放入隊列,重載隊列的 offer 方法,直接返回 false,造成隊列已滿的假象  3     // 隊列滿時,會創建新的線程直到達到 maximumPoolSize,之後會觸發執行拒絕策略
 4     LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {  5         private static final long serialVersionUID = 8303142475890427046L;  6 
 7  @Override  8         public boolean offer(Runnable e) {  9             return false; 10  } 11  }; 12 
13     // 當線程達到 maximumPoolSize 時會觸發拒絕策略,此時將任務 put 到隊列中
14     RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() { 15  @Override 16         public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { 17             try { 18                 // 任務拒絕時,通過 put 放入隊列
19  queue.put(r); 20             } catch (InterruptedException e) { 21  Thread.currentThread().interrupt(); 22  } 23  } 24  }; 25 
26     // 構造線程池
27     ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 28             600, TimeUnit.SECONDS, 29  queue, 30             new ThreadFactoryBuilder().setNameFormat("demo-%d").build(), 31  rejectedExecutionHandler); 32 
33     IntStream.rangeClosed(1, 50).forEach(i -> { 34         executor.submit(() -> { 35             log.info("start..."); 36             sleep(9000); 37  }); 38  }); 39 }

優雅的終止線程和線程池

優雅地終止線程

在程序中,我們不能隨便中斷一個線程,因為這是極其不安全的操作,我們無法知道這個線程正運行在什麼狀態,它可能持有某把鎖,強行中斷可能導致鎖不能釋放的問題;或者線程可能在操作數據庫,強行中斷導致數據不一致混亂的問題。正因此,JAVA里將Thread的stop方法設置為過時,以禁止大家使用。

優雅地終止線程,不是自己終止自己,而是在一個線程 T1 中,終止線程 T2;這裏所謂的“優雅”,指的是給 T2 一個機會料理後事,而不是被一劍封喉。兩階段終止模式,就是將終止過程分成兩個階段,其中第一個階段主要是線程 T1 向線程 T2發送終止指令,而第二階段則是線程 T2響應終止指令。

Java 線程進入終止狀態的前提是線程進入 RUNNABLE 狀態,而實際上線程也可能處在休眠狀態,也就是說,我們要想終止一個線程,首先要把線程的狀態從休眠狀態轉換到 RUNNABLE 狀態。如何做到呢?這個要靠 Java Thread 類提供的 interrupt() 方法,它可以將休眠狀態的線程轉換到 RUNNABLE 狀態。

線程轉換到 RUNNABLE 狀態之後,我們如何再將其終止呢?RUNNABLE 狀態轉換到終止狀態,優雅的方式是讓 Java 線程自己執行完 run() 方法,所以一般我們採用的方法是設置一個標誌位,然後線程會在合適的時機檢查這個標誌位,如果發現符合終止條件,則自動退出 run() 方法。這個過程其實就是第二階段:響應終止指令。終止指令,其實包括兩方面內容:interrupt() 方法和線程終止的標誌位。

如果我們在線程內捕獲中斷異常(如Thread.sleep()拋出了中斷一次)之後,需通過 Thread.currentThread().interrupt() 重新設置線程的中斷狀態,因為 JVM 的異常處理會清除線程的中斷狀態。

 

建議自己設置線程終止標誌位,避免線程內調用第三方類庫的方法未處理線程中斷狀態,如下所示。

 1 public class InterruptDemo {  2 
 3     /**
 4  * 輸出:調用 interrupt() 時,只是設置了線程中斷標識,線程依舊會繼續執行當前方法,執行完之後再退出線程。  5  * do something...  6  * continue do something...  7  * do something...  8  * continue do something...  9  * do something... 10  * 線程被中斷... 11  * continue do something... 12      */
13     public static void main(String[] args) throws InterruptedException { 14         Proxy proxy = new Proxy(); 15  proxy.start(); 16 
17         Thread.sleep(6000); 18  proxy.stop(); 19  } 20 
21     static class Proxy { 22         // 自定義線程終止標誌位
23         private volatile boolean terminated = false; 24 
25         private boolean started = false; 26 
27  Thread t; 28 
29         public synchronized void start() { 30             if (started) { 31                 return; 32  } 33             started = true; 34             terminated = false; 35 
36             t = new Thread(() -> { 37                 while (!terminated) { // 取代 while (true)
38                     System.out.println("do something..."); 39                     try { 40                         Thread.sleep(2000); 41                     } catch (InterruptedException e) { 42                         // 如果其它線程中斷此線程,拋出異常時,需重新設置線程中斷狀態,因為 JVM 的異常處理會清除線程的中斷狀態。
43                         System.out.println("線程被中斷..."); 44  Thread.currentThread().interrupt(); 45  } 46                     System.out.println("continue do something..."); 47  } 48                 started = false; 49  }); 50  t.start(); 51  } 52 
53         public synchronized void stop() { 54             // 設置中斷標誌
55             terminated = true; 56  t.interrupt(); 57  } 58  } 59 
60 }

 

優雅的終止線程池

線程池提供了兩個方法來中斷線程池:shutdown() 和 shutdownNow()。

shutdown():是一種很保守的關閉線程池的方法。線程池執行 shutdown() 后,就會拒絕接收新的任務,但是會等待線程池中正在執行的任務和已經進入阻塞隊列的任務都執行完之後才最終關閉線程池。

shutdownNow():相對激進一些,線程池執行 shutdownNow() 后,會拒絕接收新的任務,同時還會中斷線程池中正在執行的任務,已經進入阻塞隊列的任務也被剝奪了執行的機會,不過這些被剝奪執行機會的任務會作為 shutdownNow() 方法的返回值返回。因為 shutdownNow() 方法會中斷正在執行的線程,所以提交到線程池的任務,如果需要優雅地結束,就需要正確地處理線程中斷。如果提交到線程池的任務不允許取消,那就不能使用 shutdownNow() 方法終止線程池。

 

如果想在jvm關閉的時候進行內存清理、對象銷毀等操作,或者僅僅想起個線程然後這個線程不會退出,可以使用Runtime.addShutdownHook。

這個方法的作用就是在JVM中增加一個關閉的鈎子。當程序正常退出、系統調用 System.exit 方法或者虛擬機被關閉時才會執行系統中已經設置的所有鈎子,當系統執行完這些鈎子后,JVM才會關閉。

利用這個性質,就可以在這個最後執行的線程中把線程池優雅的關閉掉。雖然jvm關閉了,但優雅關閉線程池總是好的,特別是涉及到服務端的 tcp 連接。

 1 /**
 2  * 添加Hook在Jvm關閉時優雅的關閉線程池  3  *  4  * @param threadPool 線程池  5  * @param threadPoolName 線程池名稱  6  */
 7 public static void hookShutdownThreadPool(ExecutorService threadPool, String threadPoolName) {  8     Runtime.getRuntime().addShutdownHook(new Thread(() -> {  9         LOGGER.info("[>>ExecutorShutdown<<] Start to shutdown the thead pool: [{}]", threadPoolName); 10         // 使新任務無法提交
11  threadPool.shutdown(); 12         try { 13             // 等待未完成任務結束
14             if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) { 15                 threadPool.shutdownNow(); // 取消當前執行的任務
16                 LOGGER.warn("[>>ExecutorShutdown<<] Interrupt the worker, which may cause some task inconsistent. Please check the biz logs."); 17 
18                 // 等待任務取消的響應
19                 if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) { 20                     LOGGER.error("[>>ExecutorShutdown<<] Thread pool can't be shutdown even with interrupting worker threads, which may cause some task inconsistent. Please check the biz logs."); 21  } 22  } 23         } catch (InterruptedException ie) { 24             // 重新取消當前線程進行中斷
25  threadPool.shutdownNow(); 26             LOGGER.error("[>>ExecutorShutdown<<] The current server thread is interrupted when it is trying to stop the worker threads. This may leave an inconsistent state. Please check the biz logs."); 27 
28             // 保留中斷狀態
29  Thread.currentThread().interrupt(); 30  } 31 
32         LOGGER.info("[>>ExecutorShutdown<<] Finally shutdown the thead pool: [{}]", threadPoolName); 33  })); 34 }

Executors

考慮到 ThreadPoolExecutor 的構造函數實在是有些複雜,所以 Java 併發包里提供了一個線程池的靜態工廠類 Executors,利用 Executors 你可以快速創建線程池。

但《阿里巴巴 Java 開發手冊》中提到,禁止使用這些方法來創建線程池,而應該手動 new ThreadPoolExecutor 來創建線程池。最重要的原因是:Executors 提供的很多方法默認使用的都是無界的 LinkedBlockingQueue,高負載情境下,無界隊列很容易導致 OOM,而 OOM 會導致所有請求都無法處理,這是致命問題。最典型的就是 newFixedThreadPool 和 newCachedThreadPool,可能因為資源耗盡導致 OOM 問題。

 

newCachedThreadPool

具有緩存性質的線程池,線程最大空閑時間60s,線程可重複利用,沒有最大線程數限制。使用的是 SynchronousQueue 無容量阻塞隊列,沒有最大線程數限制。這意味着,只要有請求到來,就必須找到一條工作線程來處理,如果當前沒有空閑的線程就再創建一條新的。

高併發情況下,大量的任務進來後會創建大量的線程,導致OOM(無法創建本地線程):

1 [11:30:30.487] [http-nio-45678-exec-1] [ERROR] [.a.c.c.C.[.[.[/].[dispatcherServlet]:175 ] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; 2     nested exception is java.lang.OutOfMemoryError: unable to create new native thread] with root cause 3 java.lang.OutOfMemoryError: unable to create new native thread 

 

newFixedThreadPool

具有固定數量的線程池,核心線程數等於最大線程數,超出最大線程數進行等待。使用的是 LinkedBlockingQueue 無界阻塞隊列。雖然使用 newFixedThreadPool 可以把工作線程控制在固定的數量上,但任務隊列是無界的。如果任務較多並且執行較慢的話,隊列可能會快速積壓,撐爆內存導致 OOM。

如果一直往這個無界隊列中添加任務,不久就會出現OOM異常(內存佔滿):

1 Exception in thread "http-nio-45678-ClientPoller" 
2     java.lang.OutOfMemoryError: GC overhead limit exceeded

 

newSingleThreadExecutor

核心線程數與最大線程數均為1,可用於當鎖控制同步。使用的是 LinkedBlockingQueue 無界阻塞隊列。

 

newScheduledThreadPool

具有時間調度性的線程池,必須初始化核心線程數。

沒有最大線程數限制,線程最大空閑時間為0,空閑線程執行完即銷毀。底層使用 DelayedWorkQueue 實現延遲特性。

線程池創建正確姿勢

最後,總結一下,從如下的一些方面考慮如何正確地創建線程池。

線程池配置

我們需要根據自己的場景、併發情況來評估線程池的幾個核心參數,包括核心線程數、最大線程數、線程回收策略、工作隊列的類型,以及拒絕策略,確保線程池的工作行為符合需求,一般都需要設置有界的工作隊列和可控的線程數。

要根據任務的“輕重緩急”來指定線程池的核心參數,包括線程數、回收策略和任務隊列:

  • 對於執行比較慢、數量不大的 IO 任務,要考慮更多的線程數,而不需要太大的隊列。
  • 對於吞吐量較大的計算型任務,線程數量不宜過多,可以是 CPU 核數或核數 *2(理由是,線程一定調度到某個 CPU 進行執行,如果任務本身是 CPU 綁定的任務,那麼過多的線程只會增加線程切換的開銷,並不能提升吞吐量),但可能需要較長的隊列來做緩衝。

 

任何時候,都應該為自定義線程池指定有意義的名稱,以方便排查問題。當出現線程數量暴增、線程死鎖、線程佔用大量 CPU、線程執行出現異常等問題時,我們往往會抓取線程棧。此時,有意義的線程名稱,就可以方便我們定位問題。

除了建議手動聲明線程池以外,還建議用一些監控手段來觀察線程池的狀態。如果我們能提前觀察到線程池隊列的積壓,或者線程數量的快速膨脹,往往可以提早發現並解決問題。

 

確認線程池本身是不是復用的

既然使用了線程池就需要確保線程池是在復用的,每次 new 一個線程池出來可能比不用線程池還糟糕。如果你沒有直接聲明線程池而是使用其他同學提供的類庫來獲得一個線程池,請務必查看源碼,以確認線程池的實例化方式和配置是符合預期的。

 

斟酌線程池的混用策略

不要盲目復用線程池,別人定義的線程池屬性不一定適合你的任務,而且混用會相互干擾。

另外,Java 8 的 parallel stream 背後是共享同一個 ForkJoinPool,默認并行度是 CPU 核數 -1。對於 CPU 綁定的任務來說,使用這樣的配置比較合適,但如果集合操作涉及同步 IO 操作的話(比如數據庫操作、外部服務調用等),建議自定義一個 ForkJoinPool(或普通線程池)。因此在使用 Java8 的并行流時,建議只用在計算密集型的任務,IO密集型的任務建議自定義線程池來提交任務,避免影響其它業務。

 

CommonExecutor

如下是我自己封裝的一個線程池工具類,還提供了執行批量任務的方法,關於批量任務後面再單獨寫篇文章來介紹。

 1 package org.hzero.core.util;  2 
 3 import java.util.ArrayList;  4 import java.util.Collections;  5 import java.util.List;  6 import java.util.concurrent.*;  7 import java.util.stream.Collectors;  8 import javax.annotation.Nonnull;  9 
 10 import com.google.common.util.concurrent.ThreadFactoryBuilder;  11 import org.apache.commons.collections4.CollectionUtils;  12 import org.apache.commons.lang3.RandomUtils;  13 import org.slf4j.Logger;  14 import org.slf4j.LoggerFactory;  15 import org.springframework.dao.DuplicateKeyException;  16 
 17 import io.choerodon.core.exception.CommonException;  18 
 19 import org.hzero.core.base.BaseConstants;  20 
 21 /**
 22  * @author bojiangzhou 2020/02/24  23  */
 24 public class CommonExecutor {  25 
 26     private static final Logger LOGGER = LoggerFactory.getLogger(CommonExecutor.class);  27 
 28     private static final ThreadPoolExecutor BASE_EXECUTOR;  29 
 30     static {  31         BASE_EXECUTOR = buildThreadFirstExecutor("BaseExecutor");  32  }  33 
 34     /**
 35  * 構建線程優先的線程池  36  * <p>  37  * 線程池默認是當核心線程數滿了后,將任務添加到工作隊列中,當工作隊列滿了之後,再創建線程直到達到最大線程數。  38  *  39  * <p>  40  * 線程優先的線程池,就是在核心線程滿了之後,繼續創建線程,直到達到最大線程數之後,再把任務添加到工作隊列中。  41  *  42  * <p>  43  * 此方法默認設置核心線程數為 CPU 核數,最大線程數為 8倍 CPU 核數,空閑線程超過 5 分鐘銷毀,工作隊列大小為 65536。  44  *  45  * @param poolName 線程池名稱  46  * @return ThreadPoolExecutor  47      */
 48     public static ThreadPoolExecutor buildThreadFirstExecutor(String poolName) {  49         int coreSize = CommonExecutor.getCpuProcessors();  50         int maxSize = coreSize * 8;  51         return buildThreadFirstExecutor(coreSize, maxSize, 5, TimeUnit.MINUTES, 1 << 16, poolName);  52  }  53 
 54     /**
 55  * 構建線程優先的線程池  56  * <p>  57  * 線程池默認是當核心線程數滿了后,將任務添加到工作隊列中,當工作隊列滿了之後,再創建線程直到達到最大線程數。  58  *  59  * <p>  60  * 線程優先的線程池,就是在核心線程滿了之後,繼續創建線程,直到達到最大線程數之後,再把任務添加到工作隊列中。  61  *  62  * @param corePoolSize 核心線程數  63  * @param maximumPoolSize 最大線程數  64  * @param keepAliveTime 空閑線程的空閑時間  65  * @param unit 時間單位  66  * @param workQueueSize 工作隊列容量大小  67  * @param poolName 線程池名稱  68  * @return ThreadPoolExecutor  69      */
 70     public static ThreadPoolExecutor buildThreadFirstExecutor(int corePoolSize,  71                                                               int maximumPoolSize,  72                                                               long keepAliveTime,  73  TimeUnit unit,  74                                                               int workQueueSize,  75  String poolName) {  76         // 自定義隊列,優先開啟更多線程,而不是放入隊列
 77         LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(workQueueSize) {  78             private static final long serialVersionUID = 5075561696269543041L;  79 
 80  @Override  81             public boolean offer(@Nonnull Runnable o) {  82                 return false; // 造成隊列已滿的假象
 83  }  84  };  85 
 86         // 當線程達到 maximumPoolSize 時會觸發拒絕策略,此時將任務 put 到隊列中
 87         RejectedExecutionHandler rejectedExecutionHandler = (runnable, executor) -> {  88             try {  89                 // 任務拒絕時,通過 offer 放入隊列
 90  queue.put(runnable);  91             } catch (InterruptedException e) {  92                 LOGGER.warn("{} Queue offer interrupted. ", poolName, e);  93  Thread.currentThread().interrupt();  94  }  95  };  96 
 97         ThreadPoolExecutor executor = new ThreadPoolExecutor(  98  corePoolSize, maximumPoolSize,  99  keepAliveTime, unit, 100  queue, 101                 new ThreadFactoryBuilder() 102                         .setNameFormat(poolName + "-%d") 103                         .setUncaughtExceptionHandler((Thread thread, Throwable throwable) -> { 104                             LOGGER.error("{} catching the uncaught exception, ThreadName: [{}]", poolName, thread.toString(), throwable); 105  }) 106  .build(), 107  rejectedExecutionHandler 108  ); 109 
110  CommonExecutor.displayThreadPoolStatus(executor, poolName); 111  CommonExecutor.hookShutdownThreadPool(executor, poolName); 112         return executor; 113  } 114 
115     /**
116  * 批量提交異步任務,使用默認的線程池
117 * 118 * @param tasks 將任務轉化為 AsyncTask 批量提交 119 */ 120 public static <T> List<T> batchExecuteAsync(List<AsyncTask<T>> tasks, @Nonnull String taskName) { 121 return batchExecuteAsync(tasks, BASE_EXECUTOR, taskName); 122 } 123 124 /** 125 * 批量提交異步任務,執行失敗可拋出異常或返回異常編碼即可 <br> 126 * <p> 127 * 需注意提交的異步任務無法控制事務,一般需容忍產生一些垃圾數據的情況下才能使用異步任務,異步任務執行失敗將拋出異常,主線程可回滾事務. 128 * <p> 129 * 異步任務失敗后,將取消剩餘的任務執行. 130 * 131 * @param tasks 將任務轉化為 AsyncTask 批量提交 132 * @param executor 線程池,需自行根據業務場景創建相應的線程池 133 * @return 返回執行結果 134 */ 135 public static <T> List<T> batchExecuteAsync(@Nonnull List<AsyncTask<T>> tasks, @Nonnull ThreadPoolExecutor executor, @Nonnull String taskName) { 136 if (CollectionUtils.isEmpty(tasks)) { 137 return Collections.emptyList(); 138 } 139 140 int size = tasks.size(); 141 142 List<Callable<T>> callables = tasks.stream().map(t -> (Callable<T>) () -> { 143 try { 144 T r = t.doExecute(); 145 146 LOGGER.debug("[>>Executor<<] Async task execute success. ThreadName: [{}], BatchTaskName: [{}], SubTaskName: [{}]", 147 Thread.currentThread().getName(), taskName, t.taskName()); 148 return r; 149 } catch (Throwable e) { 150 LOGGER.warn("[>>Executor<<] Async task execute error. ThreadName: [{}], BatchTaskName: [{}], SubTaskName: [{}], exception: {}", 151 Thread.currentThread().getName(), taskName, t.taskName(), e.getMessage()); 152 throw e; 153 } 154 }).collect(Collectors.toList()); 155 156 CompletionService<T> cs = new ExecutorCompletionService<>(executor, new LinkedBlockingQueue<>(size)); 157 List<Future<T>> futures = new ArrayList<>(size); 158 LOGGER.info("[>>Executor<<] Start async tasks, BatchTaskName: [{}], TaskSize: [{}]", taskName, size); 159 160 for (Callable<T> task : callables) { 161 futures.add(cs.submit(task)); 162 } 163 164 List<T> resultList = new ArrayList<>(size); 165 for (int i = 0; i < size; i++) { 166 try { 167 Future<T> future = cs.poll(6, TimeUnit.MINUTES); 168 if (future != null) { 169 T result = future.get(); 170 resultList.add(result); 171 LOGGER.debug("[>>Executor<<] Async task [{}] - [{}] execute success, result: {}", taskName, i, result); 172 } else { 173 cancelTask(futures); 174 LOGGER.error("[>>Executor<<] Async task [{}] - [{}] execute timeout, then cancel other tasks.", taskName, i); 175 throw new CommonException(BaseConstants.ErrorCode.TIMEOUT); 176 } 177 } catch (ExecutionException e) { 178 LOGGER.warn("[>>Executor<<] Async task [{}] - [{}] execute error, then cancel other tasks.", taskName, i, e); 179 cancelTask(futures); 180 Throwable throwable = e.getCause(); 181 if (throwable instanceof CommonException) { 182 throw (CommonException) throwable; 183 } else if (throwable instanceof DuplicateKeyException) { 184 throw (DuplicateKeyException) throwable; 185 } else { 186 throw new CommonException("error.executorError", e.getCause().getMessage()); 187 } 188 } catch (InterruptedException e) { 189 cancelTask(futures); 190 Thread.currentThread().interrupt(); // 重置中斷標識 191 LOGGER.error("[>>Executor<<] Async task [{}] - [{}] were interrupted.", taskName, i); 192 throw new CommonException(BaseConstants.ErrorCode.ERROR); 193 } 194 } 195 LOGGER.info("[>>Executor<<] Finish async tasks , BatchTaskName: [{}], TaskSize: [{}]", taskName, size); 196 return resultList; 197 } 198 199 /** 200 * 根據一定周期輸出線程池的狀態 201 * 202 * @param threadPool 線程池 203 * @param threadPoolName 線程池名稱 204 */ 205 public static void displayThreadPoolStatus(ThreadPoolExecutor threadPool, String threadPoolName) { 206 displayThreadPoolStatus(threadPool, threadPoolName, RandomUtils.nextInt(60, 600), TimeUnit.SECONDS); 207 } 208 209 /** 210 * 根據一定周期輸出線程池的狀態 211 * 212 * @param threadPool 線程池 213 * @param threadPoolName 線程池名稱 214 * @param period 周期 215 * @param unit 時間單位 216 */ 217 public static void displayThreadPoolStatus(ThreadPoolExecutor threadPool, String threadPoolName, long period, TimeUnit unit) { 218 Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> { 219 LOGGER.info("[>>ExecutorStatus<<] ThreadPool Name: [{}], Pool Status: [shutdown={}, Terminated={}], Pool Thread Size: {}, Active Thread Count: {}, Task Count: {}, Tasks Completed: {}, Tasks in Queue: {}", 220 threadPoolName, 221 threadPool.isShutdown(), threadPool.isTerminated(), // 線程是否被終止 222 threadPool.getPoolSize(), // 線程池線程數量 223 threadPool.getActiveCount(), // 工作線程數 224 threadPool.getTaskCount(), // 總任務數 225 threadPool.getCompletedTaskCount(), // 已完成的任務數 226 threadPool.getQueue().size()); // 線程池中線程的數量 227 }, 0, period, unit); 228 } 229 230 /** 231 * 添加Hook在Jvm關閉時優雅的關閉線程池 232 * 233 * @param threadPool 線程池 234 * @param threadPoolName 線程池名稱 235 */ 236 public static void hookShutdownThreadPool(ExecutorService threadPool, String threadPoolName) { 237 Runtime.getRuntime().addShutdownHook(new Thread(() -> { 238 LOGGER.info("[>>ExecutorShutdown<<] Start to shutdown the thead pool: [{}]", threadPoolName); 239 // 使新任務無法提交 240 threadPool.shutdown(); 241 try { 242 // 等待未完成任務結束 243 if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) { 244 threadPool.shutdownNow(); // 取消當前執行的任務 245 LOGGER.warn("[>>ExecutorShutdown<<] Interrupt the worker, which may cause some task inconsistent. Please check the biz logs."); 246 247 // 等待任務取消的響應 248 if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) { 249 LOGGER.error("[>>ExecutorShutdown<<] Thread pool can't be shutdown even with interrupting worker threads, which may cause some task inconsistent. Please check the biz logs."); 250 } 251 } 252 } catch (InterruptedException ie) { 253 // 重新取消當前線程進行中斷 254 threadPool.shutdownNow(); 255 LOGGER.error("[>>ExecutorShutdown<<] The current server thread is interrupted when it is trying to stop the worker threads. This may leave an inconsistent state. Please check the biz logs."); 256 257 // 保留中斷狀態 258 Thread.currentThread().interrupt(); 259 } 260 261 LOGGER.info("[>>ExecutorShutdown<<] Finally shutdown the thead pool: [{}]", threadPoolName); 262 })); 263 } 264 265 /** 266 * 獲取返回CPU核數 267 * 268 * @return 返回CPU核數,默認為8 269 */ 270 public static int getCpuProcessors() { 271 return Runtime.getRuntime() != null && Runtime.getRuntime().availableProcessors() > 0 ? 272 Runtime.getRuntime().availableProcessors() : 8; 273 } 274 275 private static <T> void cancelTask(List<Future<T>> futures) { 276 for (Future<T> future : futures) { 277 if (!future.isDone()) { 278 future.cancel(true); 279 } 280 } 281 } 282 283 }

AsyncTask:

 1 package org.hzero.core.util;  2 
 3 import java.util.UUID;  4 
 5 public interface AsyncTask<T> {  6 
 7     default String taskName() {  8         return UUID.randomUUID().toString();  9  } 10 
11  T doExecute(); 12 }

 

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

 

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

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

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

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

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

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

※回頭車貨運收費標準

Linux Systemd 詳細介紹: Unit、Unit File、Systemctl、Target

Systemd

簡介

CentOS 7 使用 Systemd 替換了SysV

Ubuntu 從 15.04 開始使用 Systemd

Systemd 是 Linux 系統工具,用來啟動守護進程,已成為大多數發行版的標準配置

特點

優點:

  1. 按需啟動進程,減少系統資源消耗

  2. 并行啟動進程,提高系統啟動速度

    在 SysV-init 時代,將每個服務項目編號,依次執行啟動腳本。Ubuntu 的 Upstart 解決了沒有直接依賴的啟動之間的并行啟動。而 Systemd 通過 Socket 緩存、DBus 緩存和建立臨時掛載點等方法進一步解決了啟動進程之間的依賴,做到了所有系統服務併發啟動。對於用戶自定義的服務,Systemd 允許配置其啟動依賴項目,從而確保服務按必要的順序運行。

    SystemV Upstart 參考上一篇博文:Linux 初始化系統 SystemV Upstart

  3. 使用 CGroup 監視和管理進程的生命周期

    CGroup 提供了類似文件系統的接口,當進程創建子進程時,子進程會繼承父進程的 CGroup。因此無論服務如何啟動新的子進程,所有的這些相關進程都會屬於同一個 CGroup

    在 Systemd 之前的主流應用管理服務都是使用 進程樹 來跟蹤應用的繼承關係的,而進程的父子關係很容易通過 兩次 fork 的方法脫離。

    而 Systemd 則提供通過 CGroup 跟蹤進程關係,引補了這個缺漏。通過 CGroup 不僅能夠實現服務之間訪問隔離,限制特定應用程序對系統資源的訪問配額,還能更精確地管理服務的生命周期

  4. 統一管理服務日誌

  5. 支持快照和系統恢復

缺點:

  1. 過於複雜,與操作系統的其他部分強耦合,違反”keep simple, keep stupid”的Unix 哲學

架構圖

Unit(單元|服務)

Systemd 可以管理所有系統資源:

  1. 將系統資源劃分為12類
  2. 將每個系統資源稱為一個 Unit。Unit 是 Systemd 管理系統資源的基本單位
  3. 使用一個 Unit File 作為 Unit 的單元文件,Systemd 通過單元文件控制 Unit 的啟動

例如,MySQL服務被 Systemd 視為一個 Unit,使用一個 mysql.service 作為啟動配置文件

Unit File(單元文件|配置文件)

單元文件中包含該單元的描述、屬性、啟動命令等

類型

Systemd 將系統資源劃分為12類,對應12種類型的單元文件

系統資源類型 單元文件擴展名 單元文件描述
Service .service 封裝守護進程的啟動、停止、重啟和重載操作,是最常見的一種 Unit 文件
Target .target 定義 target 信息及依賴關係,一般僅包含 Unit 段
Device .device 對於 /dev 目錄下的硬件設備,主要用於定義設備之間的依賴關係
Mount .mount 定義文件系統的掛載點,可以替代過去的 /etc/fstab 配置文件
Automount .automount 用於控制自動掛載文件系統,相當於 SysV-init 的 autofs 服務
Path .path 用於監控指定目錄或文件的變化,並觸發其它 Unit 運行
Scope .scope 這種 Unit 文件不是用戶創建的,而是 Systemd 運行時產生的,描述一些系統服務的分組信息
Slice .slice 用於表示一個 CGroup 的樹
Snapshot .snapshot 用於表示一個由 systemctl snapshot 命令創建的 Systemd Units 運行狀態快照,可以切回某個快照
Socket .socket 監控來自於系統或網絡的數據消息
Swap .swap 定義一個用戶做虛擬內存的交換分區
Timer .timer 用於配置在特定時間觸發的任務,替代了 Crontab 的功能

對於操作單元文件的命令,如果缺省擴展名,則默認.service擴展名

而操作 target 的命令,例如 isolate,則默認.target擴展名

語法

單元文件的語法來源於 XDG桌面入口配置文件.desktop文件

Unit 文件可以分為三個配置區段:

  • Unit 段:所有 Unit 文件通用,用來定義 Unit 的元數據,以及配置與其他 Unit 的關係
  • Install 段:所有 Unit 文件通用,用來定義如何啟動,以及是否開機啟動
  • Service 段:服務(Service)類型的 Unit 文件(後綴為 .service)特有的,用於定義服務的具體管理和執行動作

單元文件中的區段名和字段名大小寫敏感

每個區段內都是一些等號連接的鍵值對(鍵值對的等號兩側不能有空格)

Unit 段

主要字段如下:

  • Description:當前服務的簡單描述

  • Documentation:文檔地址,可以是一個或多個文檔的 URL 路徑

    【依賴關係】

  • Requires:與其它 Unit 的強依賴關係,如果其中任意一個 Unit 啟動失敗或異常退出,當前 Unit 也會被退出

  • Wants:與其它 Unit 的弱依賴關係,如果其中任意一個 Unit 啟動失敗或異常退出,不影響當前 Unit 繼續執行

    只涉及依賴關係,默認情況下 兩個 Unit 同時啟動

    【啟動順序】

  • After:該字段指定的 Unit 全部啟動完成以後,才會啟動當前 Unit

  • Before:該字段指定的 Unit 必須在當前 Unit 啟動完成之後再啟動

    只涉及啟動順序,不影響啟動結果和運行情況

  • Binds To:與 Requires 相似,該字段指定的 Unit 如果退出,會導致當前 Unit 停止運行

  • Part Of:一個 Bind To 作用的子集,僅在列出的 Unit 失敗或重啟時,終止或重啟當前 Unit,而不會隨列出Unit 的啟動而啟動

http://manpages.ubuntu.com/manpages/bionic/en/man5/systemd.unit.5.html

Install 段

主要字段如下:

  • WantedBy:它的值是一個或多個 target,執行enable命令時,符號鏈接會放入/etc/systemd/system目錄下以 target 名 + .wants後綴構成的子目錄中
  • RequiredBy:它的值是一個或多個 target,執行enable命令時,符號鏈接會放入/etc/systemd/system目錄下以 target 名 + .required後綴構成的子目錄中
  • Alias:當前 Unit 可用於啟動的別名
  • Also:當前 Unit 被 enable/disable 時,會被同時操作的其他 Unit

http://manpages.ubuntu.com/manpages/bionic/en/man5/systemd.unit.5.html

Service 段

主要字段如下:

【啟動類型】

  • Type:定義啟動時的進程行為。它有以下幾種值。
    • Type=simple:默認值,ExecStart字段啟動的進程為主進程
      • 服務進程不會 fork,如果該服務要啟動其他服務,不要使用此類型啟動,除非該服務是 socket 激活型
    • Type=forkingExecStart字段將以fork()方式從父進程創建子進程啟動,創建後父進程會立即退出,子進程成為主進程。
      • 通常需要指定PIDFile字段,以便 Systemd 能夠跟蹤服務的主進程
      • 對於常規的守護進程(daemon),除非你確定此啟動方式無法滿足需求,使用此類型啟動即可
    • Type=oneshot:只執行一次,Systemd 會等當前服務退出,再繼續往下執行
      • 適用於只執行一項任務、隨後立即退出的服務
      • 通常需要指定RemainAfterExit=yes字段,使得 Systemd 在服務進程退出之後仍然認為服務處於激活狀態
    • Type=dbus:當前服務通過 D-Bus 信號啟動。當指定的 BusName 出現在 DBus 系統總線上時,Systemd認為服務就緒
    • Type=notify:當前服務啟動完畢會發出通知信號,通知 Systemd,然後 Systemd 再啟動其他服務
    • Type=idle:Systemd 會等到其他任務都執行完,才會啟動該服務。
      • 一種使用場合是:讓該服務的輸出,不與其他服務的輸出相混合

【啟動行為】

  • ExecStart:啟動當前服務的命令

    ExecStart=/bin/echo execstart1
    ExecStart=
    ExecStart=/bin/echo execstart2
    

    順序執行設定的命令,把字段置空,表示清除之前的值

  • ExecStartPre:啟動當前服務之前執行的命令

  • ExecStartPost:啟動當前服務之後執行的命令

  • ExecReload:重啟當前服務時執行的命令

  • ExecStop:停止當前服務時執行的命令

  • ExecStopPost:停止當前服務之後執行的命令

  • RemainAfterExit:當前服務的所有進程都退出的時候,Systemd 仍認為該服務是激活狀態

    • 這個配置主要是提供給一些並非常駐內存,而是啟動註冊后立即退出,然後等待消息按需啟動的特殊類型服務使用的
  • TimeoutSec:定義 Systemd 停止當前服務之前等待的秒數

    注:所有的啟動設置之前,都可以加上一個連詞號(-),表示”抑制錯誤”,即發生錯誤的時候,不影響其他命令的執行。比如,EnvironmentFile=-/etc/sysconfig/sshd(注意等號後面的那個連詞號),就表示即使/etc/sysconfig/sshd文件不存在,也不會拋出錯誤。

【重啟行為】

  • RestartSec:Systemd 重啟當前服務間隔的秒數
  • KillMode:定義 Systemd 如何停止服務,可能的值包括:
    • control-group(默認值):當前控制組裡面的所有子進程,都會被殺掉
    • process:只殺主進程(sshd 服務,推薦值)
    • mixed:主進程將收到 SIGTERM 信號,子進程收到 SIGKILL 信號
    • none:沒有進程會被殺掉,只是執行服務的 stop 命令。
  • Restart:定義何種情況 Systemd 會自動重啟當前服務,可能的值包括:
    • no(默認值):退出后不會重啟
    • on-success:只有正常退出時(退出狀態碼為0),才會重啟
    • on-failure:非正常退出時(退出狀態碼非0),包括被信號終止和超時,才會重啟(守護進程,推薦值)
    • on-abnormal:只有被信號終止和超時,才會重啟(對於允許發生錯誤退出的服務,推薦值)
    • on-abort:只有在收到沒有捕捉到的信號終止時,才會重啟
    • on-watchdog:超時退出,才會重啟
    • always:不管是什麼退出原因,總是重啟

【上下文】

  • PIDFile:指向當前服務 PID file 的絕對路徑。

  • User:指定運行服務的用戶

  • Group:指定運行服務的用戶組

  • EnvironmentFile:指定當前服務的環境參數文件。該文件內部的key=value鍵值對,可以用$key的形式,在當前配置文件中獲取

    啟動sshd,執行的命令是/usr/sbin/sshd -D $OPTIONS,其中的變量$OPTIONS就來自EnvironmentFile字段指定的環境參數文件。

http://manpages.ubuntu.com/manpages/bionic/en/man5/systemd.service.5.html

佔位符

在 Unit 文件中,有時會需要使用到一些與運行環境有關的信息,例如節點 ID、運行服務的用戶等。這些信息可以使用佔位符來表示,然後在實際運行中動態地替換為實際的值。

詳細了解見 https://cloud.tencent.com/developer/article/1516125

模板

在現實中,往往有一些應用需要被複制多份運行,就會用到模板文件

模板文件的寫法與普通單元文件基本相同,只是模板文件名是以 @ 符號結尾。例如:apache@.service

通過模板文件啟動服務實例時,需要在其文件名的 @ 字符後面附加一個用於區分服務實例的參数字符串,通常這個參數是用於監控的端口號或控制台 TTY 編譯號

systemctl start apache@8080.service

Systemd 在運行服務時,首先尋找跟單元名完全匹配的單元文件,如果沒有找到,才會嘗試選擇匹配模板

例如上面的命令,System 首先會在約定的目錄下尋找名為 apache@8080.service 的單元文件,如果沒有找到,而文件名中包含 @ 字符,它就會嘗試去掉後綴參數匹配模板文件。對於 apache@8080.service,Systemd 會找到 apache@.service 模板文件,並通過這個模板文件將服務實例化。

詳細了解見 https://cloud.tencent.com/developer/article/1516125

狀態

systemctl list-unit-files 將會列出文件的 state,包括 static, enabled, disabled, masked, indirect

  • masked

    service軟鏈接到/dev/null

    該單元文件被禁止建立啟動鏈接

  • static

    該單元文件沒有[Install]部分(無法執行),只能作為其他配置文件的依賴

  • enabled

    已建立啟動鏈接

  • disabled

    沒建立啟動鏈接

https://askubuntu.com/a/731674

示例

  1. 關掉觸摸板配置文件

    Unit]
    Description=Switch-off Touchpad
    
    [Service]
    Type=oneshot
    ExecStart=/usr/bin/touchpad-off start
    ExecStop=/usr/bin/touchpad-off stop
    RemainAfterExit=yes
    
    [Install]
    WantedBy=multi-user.target
    
    • oneshot 表明這個服務只要運行一次就夠了,不需要長期運行
    • RemainAfterExit字段設為yes,表示進程退出以後,服務仍然保持執行。這樣的話,一旦使用systemctl stop命令停止服務,ExecStop指定的命令就會執行,從而重新開啟觸摸板

Systemd 內建命令

systemd-analyze

Analyze and debug system manager, If no command is passed, Systemd-analyze time is implied

https://www.freedesktop.org/software/systemd/man/systemd-analyze.html

systemd-analyze time

查看初始化耗時

systemd-analyze blame

打印所有運行單元,按它們初始化的時間排序。此信息可用於優化啟動時間。注意,輸出可能具有誤導性,因為一個服務的初始化可能非常緩慢,因為它等待另一個服務的初始化完成

systemd-run

將一個指定的服務變成後台服務

未測試

參考 https://www.freedesktop.org/software/systemd/man/systemd-run.html

systemctl 系統服務管理命令

systemctl是 Systemd 的主命令,用於管理系統

與 service 命令的區別

  1. systemctl 融合了 service 和 chkconfig 的功能
  2. 在 Ubuntu18.04 中沒有自帶 chkconfig 命令;service 命令實際上重定向到 systemctl 命令
動作 SysV Init 指令 Systemd 指令
啟動某服務 service httpd start systemctl start httpd
停止某服務 service httpd stop systemctl stop httpd
重啟某服務 service httpd restart systemctl restart httpd
檢查服務狀態 service httpd status systemctl status httpd
刪除某服務 chkconfig –del httpd 停掉應用,刪除其配置文件
使服務開機自啟動 chkconfig –level 5 httpd on systemctl enable httpd
使服務開機不自啟動 chkconfig –level 5 httpd off systemctl disable httpd
查詢服務是否開機自啟 chkconfig –list | grep httpd systemctl is-enabled httpd
加入自定義服務 chkconfig –add test systemctl load test
显示所有已啟動的服務 chkconfig –list systemctl list-unit-files | grep enabled

參數

--all

显示加載到內存的所有單元

--type

-t --type=

显示指定類型(12種類型)的單元

--state

--state=

显示指定狀態的單元或單元文件

  • 單元狀態

    輸入 systemctl list-units --stateTab鍵,显示所有可用的值

  • 單元文件狀態

    另外還可以用 enabled static disabled 等systemctl list-unit-files 显示的狀態

--failed

--state=failed

显示加載失敗的單元

 systemctl --failed
--version

打印 Systemd 版本

lfp@legion:/lib/systemd/system$ systemctl --version
Systemd 237
+PAM +AUDIT +SELINUX +IMA +APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 +SECCOMP +BLKID +ELFUTILS +KMOD -IDN2 +IDN -PCRE2 default-hierarchy=hybrid

單元命令

我的理解

  • systemd 對單元的管理,不涉及單元文件自身屬性和內容
list-units

相當於systemctl

列出當前已加載的單元(內存)

默認情況下僅显示處於激活狀態(正在運行)的單元

UNIT 單元名

LOAD 加載狀態

ACTIVE SUB 執行狀態(大狀態 子狀態)

DESCRIPTION 描述

start

啟動單元

systemctl start mysql.service
stop

停止單元

systemctl stop mysql.service
kill

殺掉單元進程

systemctl kill mysql.service
reload

不終止單元,重新加載 針對該單元的 運行配置文件,而不是 針對 systemd的 該單元的啟動配置文件

例如啟動 MySQL 服務,reload 可以在不停止服務的情況下重載 MySQL 的配置文件 my.cnf

restart

重啟單元

該單元在重啟之前擁有的資源不會被完全清空,比如文件描述符存儲設施

systemctl reload mysql.service
status

status [unit | PID]

显示單元或進程所屬單元的運行信息

systemctl status mysql.service

Loaded行:配置文件的位置,是否設為開機啟動

Active行:表示正在運行

Main PID行:主進程ID

CGroup塊:應用的所有子進程

日誌塊:應用的日誌

is-active

判斷指定的單元是否處於激活狀態

# 默認會打印當前單元的狀態,可以通過 --quiet 參數取消打印
lfp@legion:~$ systemctl is-active mysql
active
is-failed

判斷指定的單元是否處於啟動失敗狀態

lfp@legion:~$ systemctl is-failed mysql
active
list-dependencies

查看單元之間的依賴關係

systemctl list-dependencies graphical.target 
systemctl list-dependencies mysql.service
show

show --property= Unit <==> show -p Unit

显示單元所有底層參數

lfp@legion:~$ systemctl show -p MainPID mysql
MainPID=1061
set-property

在單元啟動的時候設置運行時的某個屬性,立即生效,並保存在磁盤中作為啟動配置

如果添加了--runtime則重啟后失效

並非所有的屬性都可以設置,只是 systemd.resource-control 包含的屬性

isolate

切換到某個 target(系統狀態),立即停止該 target 未包含的單元進程。也可以理解為切換 runlevel

如果沒有指定擴展名,則默認.target

只有當.target單元文件中的AllowIsolate=yes時,才能使用 isolate 切換;也可以用IgnoreOnIsolate=yes字段來拒絕使用 isolate 切換

systemctl isolate multi-user.target
cat

显示單元配置文件的備份文件,包括插入式配置drop-ins,可以完整的看到單元服務的配置。注意這裏打印的依據是磁盤的上內容,如果用戶修改了配置文件(磁盤已修改)但是未執行daemon-reload命令(內存中未更新),那麼該命令显示的配置和實際執行情況有出入

lfp@legion:~$ systemctl cat mysql.service 
# /lib/systemd/system/mysql.service
# MySQL Systemd service file

[Unit]
Description=MySQL Community Server
...

[Install]
WantedBy=multi-user.target

[Service]
Type=forking
...
# 這段就显示的是 插入式配置 drop-in 的內容
# /etc/systemd/system/mysql.service.d/mysql.conf
# MySQL Systemd service file

[Unit]
# Description=MySQL Community Server conf

[Service]
# ExecStartPost=/home/lfp/bin/espeak.sh

單元文件命令

我的理解

  • systemd 對單元文件自身屬性和內容的管理
list-unit-files

列出所有已安裝的單元文件和它們的啟用狀態

list-units的區別是

  • list-units 僅显示當前已加載到內存中的單元
  • list-unit-files 會讀取單元文件內容,列出所有單元,包括存在於硬盤未加載進內存的單元

實際測試結果:

systemctl list-unit-files 显示“348 Unit files listed”

systemctl list-units –all 显示“405 loaded units listed”

systemctl list-units 显示 “232 loaded units listed”

enable

使某個單元開機自啟動

這會根據單元文件內容中的[Install]指定的 target 組,創建一個軟鏈接

lfp@legion:/etc/systemd/system$ vim v2rayL.service
# 文件內容 [Install] 段
......
[Install]
WantedBy=multi-user.target
......

lfp@legion:/etc/systemd/system$ systemctl is-enabled v2rayL.service 
disabled
lfp@legion:/etc/systemd/system$ systemctl enable v2rayL.service 
# 根據 [Install] 段指定的組,添加軟鏈接
Created symlink /etc/systemd/system/multi-user.target.wants/v2rayL.service → /etc/systemd/system/v2rayL.service.
lfp@legion:/etc/systemd/system$ systemctl is-enabled v2rayL.service 
enabled
disable

取消某個單元開機自啟動設置,刪除軟鏈接

這會刪除所有指向該單元文件的軟鏈接,不僅僅是 enable 操作創建的

reenable

disable 和 enable 的結合,根據單元文件內容中的 [Install] 段,重置軟鏈接

is-enabled

檢查某個單元是否是開機自啟動的(建立的啟動鏈接)

lfp@legion:~$ systemctl is-enabled mysql
enabled
get-default

獲取默認啟動 target,default-target 是指向該 target 的軟鏈接

set-default

設置默認啟動 target,同時修改 default-target 指向設定的 target

systemctl set-default multi-user.target

生命周期管理命令

daemon-reload

重新加載所有的單元文件和依賴關係

對單元文件有修改的時候,需要執行該命令重新加載文件內容

系統管理命令

reboot
systemctl reboot

重啟系統(異步操作)

it will return after the reboot operation is enqueued, without waiting for it to complete

poweroff

關閉系統,切斷電源(異步操作)

halt

僅CPU停止工作,其他硬件仍處於開機狀態(異步操作)

suspend

暫停系統(異步操作)

將觸發執行suspend.target

hibernate

讓系統進入冬眠狀態(異步操作)

將觸發執行hibernate.target

目錄、文件

/run/systemd/system/

單元(服務)運行時生成的配置文件所在目錄
/etc/systemd/system/

系統或用戶自定義的配置文件,初始化過程中Systemd只執行/etc/systemd/system目錄裏面的配置文件

/lib/systemd/system/

軟件安裝時添加的配置文件,類似於 /etc/init.d/

對於支持 Systemd 的程序,安裝的時候,會自動的在 /lib/systemd/system 目錄添加一個配置文件

其他目錄都是軟鏈接

/etc/systemd/system/default.target

Systemd 執行的第一個單元文件,符號鏈接到默認啟動 target 對應的 .target 單元文件

優先級

SysV 的啟動腳本放在/etc/init.d目錄下

Systemd 的單元文件放在/etc/systemd/system/lib/systemd/system目錄下

當一個程序在3個目錄下都存在啟動方式時,優先級是/etc/systemd/system --> /lib/systemd/system --> /etc/init.d

lfp@legion:/etc/init.d$ ll
-rwxr-xr-x   1 root root  5650 5月  19 22:09 mysql*

lfp@legion:/etc/systemd/system$ ll
-rw-r--r--  1 root root  511 5月  20 01:42  mysql.service

lfp@legion:/lib/systemd/system$ ll
-rw-r--r--  1 root root   499 5月  20 01:20  mysql.service
  • /etc/systemd/system 裏面的同名service會覆蓋/lib/systemd/system 裏面的

    注意查看文件信息,該同名文件不能是指向 /lib/systemd/system 的軟鏈接

    軟鏈接不會覆蓋而會同步

  • 如果某個程序不存在Systemd 單元文件,那麼會執行/etc/init.d裏面的啟動腳本

根據啟動過程, /etc/systemd/system/multi-user.target.wants/ 目錄下是很多指向 /lib/systemd/system/目錄的軟鏈接,所以兩個目錄下的單元文件會互相同步。

如果/etc/systemd/system//etc/systemd/system/multi-user.target.wants/ 同時存在單元文件,測試發現,不管是手動啟動還是開機自啟動,使用的都是 /etc/systemd/system/ 目錄下的service單元文件

測試
執行/etc/init.d目錄下的腳本

mysql 修改 mysql.servicemysql.service.bak 然後通過service mysql restart啟動/etc/init.d/mysql腳本

下面是啟動后的一些信息

注:在恢復mysql.service之前,需要先通過service mysql stop 利用/etc/init.d/mysql腳本中的stop結束上面的進程,否則一旦恢復,service mysql stop 執行的操作就不是 /etc/init.d/mysql腳本中的stop,無法結束上面的進程,出現命令無法正常執行的情況

結束上面的進程,恢復mysql.service,重新啟動

/etc/systemd/system 覆蓋測試

未修改前,查看MySQL的狀態

lfp@legion:~$ service mysql status
● mysql.service - MySQL Community Server
	# 可以發現這裏的 mysql.service 是在 /lib/systemd/system 下面
   Loaded: loaded (/lib/systemd/system/mysql.service; enabled; vendor preset: en
   Active: active (running) since Sat 2020-04-25 18:34:30 CST; 5h 33min ago
 Main PID: 988 (mysqld)
    Tasks: 28 (limit: 4915)
   CGroup: /system.slice/mysql.service
           └─988 /usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid

4月 25 18:34:30 legion Systemd[1]: Starting MySQL Community Server...
4月 25 18:34:30 legion Systemd[1]: Started MySQL Community Server.

將 /lib/systemd/system 下面的文件複製到 /etc/systemd/system/ 下面

sudo cp /lib/systemd/system/mysql.service /etc/systemd/system/

修改 mysql.service

sudo vim /etc/systemd/system/mysql.service

重啟 mysql.service ,系統提示需要重新加載

lfp@legion:~$ systemctl restart mysql.service 
Warning: The Unit file, source configuration file or drop-ins of mysql.service changed on disk. 
Run 'systemctl daemon-reload' to reload units.
lfp@legion:~$ systemctl daemon-reload	#  重新加載
lfp@legion:~$ systemctl restart mysql.service # 重啟
lfp@legion:~$ systemctl status mysql.service 
● mysql.service - MySQL Community Server hahahaha	# 發現這裡是修改之後的,覆蓋了 /lib/systemd/lib 中的
#                                 這裏也可以看到加載路徑
   Loaded: loaded (/etc/systemd/system/mysql.service; enabled; vendor preset: en
   Active: active (running) since Sun 【2020-04-26】 00:47:02 CST; 5s ago
  Process: 21590 ExecStart=/usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/m
  Process: 21581 ExecStartPre=/usr/share/mysql/mysql-Systemd-start pre (code=exi
 Main PID: 21592 (mysqld)
    Tasks: 27 (limit: 4915)
   CGroup: /system.slice/mysql.service
           └─21592 /usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pi

4月 26 00:47:02 legion Systemd[1]: Starting MySQL Community Server hahahaha...
4月 26 00:47:02 legion Systemd[1

Target

兩個含義

  1. 系統的某個狀態稱為一個 target(類似於”狀態點”)

  2. 達到某個系統狀態,所需的一個或多個資源(Unit)稱為一個 target(一個 Unit 組)

    1. target是一個抽象的系統資源,不像MySQL有實體

    2. 如果一個target只包含一個Unit,那麼該 target,沒有對應的目錄,指的就是這個 Unit

      例如 hibernate.target 只包含 systemd-hibernate.service一個Unit

      如果一個target包含多個Unit,那麼該target,有對應的 xxx.target.wants 目錄,指的是目錄裏面所有的Unit

      例如 multi-user.target 包含位於/etc/systemd/system/multi-user.target.wants目錄下的多個 Unit

target也是一個 Target 類型的系統資源,有對應的單元文件 xxx.target

Systemd 使用 target 來劃分和管理資源(Unit),啟動(激活)某個 xxx.target 單元文件,通過執行該 target 包含的 Unit,使系統達到某種狀態

對於狀態點的理解:

例如,執行systemd suspend命令讓系統暫停,會觸發啟動suspend.target,然後執行裏面的systemd-suspend.service Unit,使系統達到一個暫停的狀態

傳統的init啟動模式裏面,有 RunLevel 的概念,跟 Target 的作用很類似。不同的是,RunLevel 是互斥的,不可能多個 RunLevel 同時啟動,但是多個 Target 可以同時啟動

啟動 target

runlevel是 SysV init 初始化系統中的概念,在Systemd初始化系統中使用的是 Target,他們之間的映射關係是

Runlevel Target 說明
0 poweroff.target 關閉系統
1 rescue.target 維護模式
2,3,4 multi-user.target 多用戶,無圖形系統(命令行界面)
5 graphical.target 多用戶,圖形化系統(圖形用戶界面)
6 reboot.target 重啟系統

啟動過程

  1. 讀入 /boot 目錄下的內核文件

  2. 內核文件加載完之後,開始執行第一個程序/sbin/init 初始化進程,由 Systemd 初始化系統引導,完成相關的初始化工作

  3. Systemd 執行default.target ,獲知設定的啟動 target

    實際上 default.target 是指向設定的啟動 target 的軟鏈接

  4. Systemd 執行啟動 target 對應的單元文件。根據單元文件中定義的依賴關係,傳遞控制權,依次執行其他 target 單元文件,同時啟動每個 target 包含的單元

    對於圖形化界面,默認 target 是 graphical,Systemd 執行位於/lib/systemd/system/ 目錄下的 graphical.target 單元文件,根據 target 單元文件中定義的依賴關係,依次啟動其他 target 單元文件以及各個 target 包含的位於/etc/systemd/system/目錄下的單元

    例如: graphical.target 的依賴關係是

    [Unit]
    Description=Graphical Interface
    Documentation=man:systemd.special(7)
    Requires=multi-user.target #
    Wants=display-manager.service #
    Conflicts=rescue.service rescue.target
    After=multi-user.target rescue.service rescue.target display-manager.service #
    AllowIsolate=yes
    

    因此,依次啟動 multi-user.target –> basic.target –> sysinit.target –> local-fs.target –>local-fs-pre.target –> …

    同時啟動每個 target 包含的位於/etc/systemd/system/目錄下的Unit

    SysV對應的 rc5.d –> /etc/init.d 目錄下的指定的腳本就不會在開機的時候執行了

查看默認 target

systemctl get-default

lfp@legion:~$ runlevel
N 5
lfp@legion:~$ systemctl get-default
graphical.target

修改默認 target

systemctl set-default [xxx.target]

# Ubuntu18.04
# 圖形用戶界面 切換 命令行界面
sudo systemctl set-default multi-user.target
# 命令行界面 切換 圖形用戶界面
 systemctl set-default graphical.target
 reboot
 # 命令行界面 想進入 圖形用戶界面(僅進入一次,重啟系統后仍然會進入命令行模式)
 sudo systemctl start lightdm

https://ask.csdn.net/questions/695344

https://askubuntu.com/a/788465

其他操作

修改配置文件

  1. 直接修改/lib/systemd/system目錄下的單元文件

    如果軟件包更新,修改會被丟棄

  2. /lib/systemd/system中的單元文件複製到/etc/systemd/system/

    如果軟件包更新,不會同步更新

  3. /etc/systemd/system/ 中添加配置(推薦)

添加配置

步驟:

  1. /etc/systemd/system/ 目錄下新建<單元名>.d目錄
  2. <單元名>.d目錄下,新建<單元名>.conf文件
  3. <單元名>.conf文件中修改配置

測試:

  1. 創建目錄及文件

    # /mysql.service.d
    lfp@legion:/etc/systemd/system/mysql.service.d$ ls
    mysql.conf
    
  2. 修改配置

    # MySQL Systemd service config file
    # 不需要所有的組,僅添加需要修改的組及選項
    
    [Unit]
    Description=MySQL Community Server config test
    
    [Service]
    ExecStartPost=/home/lfp/bin/espeak.sh
    
  3. 重啟測試

    lfp@legion:/etc/systemd/system/mysql.service.d$ systemctl daemon-reload
    lfp@legion:/etc/systemd/system/mysql.service.d$ systemctl restart mysql.service
    lfp@legion:/etc/systemd/system/mysql.service.d$ systemctl status mysql.service
    lfp@legion:/etc/systemd/system/mysql.service.d$ systemctl status mysql.service
    #                                                                            描述已經被修改
    ● mysql.service - MySQL Community Server config test
       Loaded: loaded (/lib/systemd/system/mysql.service; enabled; vendor preset: enabled)
    # 加入了配置文件
      Drop-In: /etc/systemd/system/mysql.service.d
               └─mysql.conf
       Active: active (running) since Thu 2020-05-21 20:18:02 CST; 12min ago
    # 新增的動作執行成功
      Process: 4703 ExecStartPost=/home/lfp/bin/espeak.sh (code=exited, status=0/SUCCESS)
      Process: 4672 ExecStart=/usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid (code=exited, status=0/SUCCESS)
      Process: 4663 ExecStartPre=/usr/share/mysql/mysql-Systemd-start pre (code=exited, status=0/SUCCESS)
     Main PID: 4674 (mysqld)
        Tasks: 27 (limit: 4915)
       CGroup: /system.slice/mysql.service
               └─4674 /usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid
    
    5月 21 20:18:02 legion espeak.sh[4703]: ALSA lib pcm_route.c:867:(find_matching_chmap) Found no matching channel map
    

添加開機啟動服務

  1. 添加啟動配置文件
  2. 通過 rc.local文件

/lib/systemd/rc.local.service

# 如果存在,就自動添加到 multi-user.target
# This Unit gets pulled automatically into multi-user.target by
# Systemd-rc-local-generator if /etc/rc.local is executable.
[Unit]
Description=/etc/rc.local Compatibility
Documentation=man:systemd-rc-local-generator(8)
ConditionFileIsExecutable=/etc/rc.local
After=network.target

[Service]
Type=forking
ExecStart=/etc/rc.local start
TimeoutSec=0
RemainAfterExit=yes
GuessMainPID=no

創建 rc.local 文件,賦予可執行權限,即可添加啟動命令

sudo touch /etc/rc.local
chmod 755 /etc/rc.local

lfp@legion:/etc$ vim rc.local
#!/bin/sh -e
echo "test rc.local" > /usr/local/rclocal.log
echo "rc.local `date +%Y-%m-%d-%H:%M:%S`" >/home/lfp/log/rclocal.log
exit 0

system 工具集

hostnamectl 主機名管理命令

hostnamectl
hostnamectl status

Show current system hostname and related information

lfp@legion:/lib/systemd/system$ hostnamectl status
   Static hostname: legion
         Icon name: computer-laptop
           Chassis: laptop
        Machine ID: b28xxxxxxxx2ecafa29e
           Boot ID: 21xxxxxxxxxxxx1d3a47504d
  Operating System: Ubuntu 18.04.4 LTS
            Kernel: Linux 5.3.0-51-generic
      Architecture: x86-64

journalctl 日誌管理命令

Systemd 統一管理所有 Unit 的啟動日誌。帶來的好處就是,可以只用journalctl一個命令,查看所有日誌(內核日誌和應用日誌)。

配置文件

/etc/systemd/journald.conf

日誌保存目錄

/var/log/journal/

默認日誌最大限製為所在文件系統容量的 10%,可通過 /etc/systemd/journald.conf 中的 SystemMaxUse 字段來指定

該目錄是 systemd 軟件包的一部分。若被刪除,systemd 不會自動創建它,直到下次升級軟件包時重建該目錄。如果該目錄缺失,systemd 會將日誌記錄寫入 /run/systemd/journal。這意味着,系統重啟後日志將丟失。

journalctl -u [服務名]

查看指定單元的日誌

journalctl -b
  • journalctl -b -0 显示本次啟動的信息
  • journalctl -b -1 显示上次啟動的信息
  • journalctl -b -2 显示上上次啟動的信息

參考

http://manpages.ubuntu.com/manpages/bionic/en/man1/systemctl.1.html

http://manpages.ubuntu.com/manpages/bionic/en/man5/systemd.unit.5.html

https://www.cnblogs.com/yingsong/p/6012180.html

https://www.cnblogs.com/sparkdev/p/8472711.html

http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-commands.html

http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-part-two.html

Systemd Boot Process a Close Look in Linux

https://cloud.tencent.com/developer/article/1516125

https://www.cnblogs.com/sparkdev/p/8472711.html

https://www.ibm.com/developerworks/cn/linux/1407_liuming_init3/index.html?ca=drs-

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

【其他文章推薦】

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

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

面試官:換人!他連哈希扣的都不懂

前言

相信你面試的時候,肯定被問過 hashCode 和 equals 相關的問題 。如:

  • hashCode 是什麼?它是怎麼得來的?有什麼用?
  • 經典題,equals 和 == 有什麼區別?
  • 為什麼要重寫 equals 和 hashCode ?
  • 重寫了 equals ,就必須要重寫 hashCode 嗎?為什麼?
  • hashCode 相等時,equals 一定相等嗎?反過來呢?

好的,上面就是靈魂拷問環節。其實,這些問題仔細想一下也不難,主要是平時我們很少去思考它。

正文

下面就按照上邊的問題順序,一個一個剖析它。扒開 hashCode 的神秘面紗。

什麼是 hashCode?

我們通常說的 hashCode 其實就是一個經過哈希運算之後的整型值。而這個哈希運算的算法,在 Object 類中就是通過一個本地方法 hashCode() 來實現的(HashMap 中還會有一些其它的運算)。

public native int hashCode();

可以看到它是一個本地方法。那麼,想要了解這個方法到底是用來幹嘛的,最直接有效的方法就是,去看它的源碼註釋。

下邊我就用我蹩腳的英文翻譯一下它的意思。。。

返回當前對象的一個哈希值。這個方法用於支持一些哈希表,例如 HashMap 。

通常來講,它有如下一些約定:

  • 若對象的信息沒有被修改,那麼,在一個程序的執行期間,對於相同的對象,不管調用多少次 hashCode 方法,都應該返回相同的值。當然,在相同程序的不同執行期間,不需要保持結果一致。
  • 若兩個對象的 equals 方法返回值相同,那麼,調用它們各自的 hashCode 方法時,也必須返回相同的結果。(ps: 這句話解答了上邊的一些問題,後面會用例子來證明這一點)
  • 當兩個對象的 equals 方法返回值不同時,那麼它們的 hashCode 方法不用保證必須返回不同的值。但是,我們應該知道,在這種情況下,我們最好也設計成 hashCode 返回不同的值。因為,這樣做有助於提高哈希表的性能。

在實際情況下,Object 類的 hashCode 方法在不同的對象中確實返回了不同的哈希值。這通常是通過把對象的內部地址轉換為一個整數來實現的。

ps: 這裏說的內部地址就是指物理地址,也就是內存地址。需要注意的是,雖然 hashCode 值是依據它的內存地址而得來的。但是,不能說 hashCode 就代表對象的內存地址,實際上,hashCode 地址是存放在哈希表中的。

上邊的源碼註釋真可謂是句句珠璣,把 hashCode 方法解釋的淋漓盡致。一會兒我通過一個案例說明,就能明白我為什麼這樣說了。

什麼是哈希表?

上文中提到了哈希表。什麼是哈希表呢?我們直接看百度百科的解釋。

用一張圖來表示它們的關係。

左邊一列就是一些關鍵碼(key),通過哈希函數,它們都會得到一個固定的值,分別對應右邊一列的某個值。右邊的這一列就可以認為是一張哈希表。

而且,我們會發現,有可能有些 key 不同,但是它們對應的哈希值卻是一樣的,例如 aa,bb 都指向 1001 。但是,一定不會出現同一個 key 指向不同的值。

這也非常好理解,因為哈希表就是用來查找 key 的哈希地址的。在 key 確定的情況下,通過哈希函數計算出來的 哈希地址,一定也是確定的。如圖中的 dd 已經確定在 1002 位置了,那麼就不可能再佔據 1003 位置。

思考一下,如果有另外一個元素 ee 來了,它的哈希地址也落在 1002 位置,怎麼辦呢?

hashCode 有什麼用?

其實,上圖就已經可以說明一些問題了。我們通過一個 key 計算出它的 hashCode 值,就可以唯一確定它在哈希表中的位置。這樣,在查詢時,就可以直接定位到當前元素,提高查詢效率。

現在我們假設有這樣一個場景。我們需要在內存中的一塊兒區域存放 10000 個不同的元素(以aa,bb,cc,dd 等為例)。那怎麼實現不同的元素插入,相同的元素覆蓋呢?

我們最容易想到的方法就是,每當存一個新元素時,就遍歷一遍已經存在的元素,看有沒有相同的。這樣雖然也是可以實現的,但是,如果已經存在了 9000 個元素,你就需要去遍歷一下這 9000 個元素。很明顯,這樣的效率是非常低下的。

我們轉換一種思路,還是以上圖為例。若來了一個新元素 ff,首先去計算它的 hashCode 值,得出為 1003 。發現此處還沒有元素,則直接把這個新元素 ff 放到此位置。

然後,ee 來了,通過計算哈希值得到 1002 。此時,發現 1002 位置已經存在一個元素了。那麼,通過 equals 方法比較它們是否相等,發現只有一個 dd 元素,很明顯和 ee 不相等。那麼,就把 ee 元素放到 dd 元素的後邊(可以用鏈表形式存放)。

我們會發現,當有新元素來的時候,先去計算它們的哈希值,再去確定存放的位置,這樣就可以減少比較的次數。如 ff 不需要比較, ee 只需要和 dd 比較一次。

當元素越來越多的時候,新元素也只需要和當前哈希值相同的位置上,已經存在的元素進行比較。而不需要和其他哈希值不同的位置上的元素進行比較。這樣就大大減少了元素的比較次數。

圖中為了方便,畫的哈希表比較小。現在假設,這個哈希表非常的大,例如有這麼非常多個位置,從 1001 ~ 9999。那麼,新元素插入的時候,有很大概率會插入到一個還沒有元素存在的位置上,這樣就不需要比較了,效率非常高。但是,我們會發現這樣也有一個弊端,就是哈希表所佔的內存空間就會變大。因此,這是一個權衡的過程。

有心的同學可能已經發現了。我去,上邊的這個做法好熟悉啊。沒錯,它就是大名鼎鼎的 HashMap 底層實現的思想。對 HashMap 還不了解的,趕緊看這篇文章理一下思路。HashMap 底層實現原理及源碼分析

所以,hashCode 有什麼用。很明顯,提高了查詢,插入元素的效率呀。

equals 和 == 有什麼區別?

這是萬年不變,經久不衰的經典面試題了。讓我油然想起,當初為了面試,背誦過的面經了,簡直是一把心酸一把淚。現在還能記得這道題的標準答案:equals 比較的是內容, == 比較的是地址。

當時,真的就只是背答案,知其然而不知其所以然。再往下問,為什麼要重寫 equals ,就懵逼了。

首先,我們應該知道 equals 是定義在所有類的父類 Object 中的。

 public boolean equals(Object obj) {
     return (this == obj);
 }

可以看到,它的默認實現,就是 == ,這是用來比較內存地址的。所以,如果一個對象的 equals 不重寫的話,和 == 的效果是一樣的。

我們知道,當創建兩個普通對象時,一般情況下,它們所對應的內存地址是不一樣的。例如,我定義一個 User 類。

public class User {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public User() {

    }
}

public class TestHashCode {
    public static void main(String[] args) {
        User user1 = new User("zhangsan", 20);
        User user2 = new User("lisi", 18); 

        System.out.println(user1 == user2);
        System.out.println(user1.equals(user2));
    }
}
// 結果: false	false

很明顯,zhangsan 和 lisi 是兩個人,兩個不同的對象。因此,它們所對應的內存地址不同,而且內容也不相等。

注意,這裏我還沒有對 User 重寫 equals,實際此時 equals 使用的是父類 Object 的方法,返回的肯定是不相等的。因此,為了更好地說明問題,我僅把第二行代碼修改如下:

//User user2 = new User("lisi", 18);
User user2 = new User("zhangsan", 20);

讓 user1 和 user2 的內容相同,都是 zhangsan,20歲。按我們的理解,這雖然是兩個對象,但是應該是指的同一個人,都是張三。但是,打印結果,如下:

這有悖於我們的認知,明明是同一個人,為什麼 equals 返回的卻不相等呢。因此,此時我們就需要把 User 類中的 equals 方法重寫,以達到我們的目的。在 User 中添加如下代碼(使用 idea 自動生成代碼):

public class User {
    ... //省略已知代碼
        
    @Override
    public boolean equals(Object o) {
        //若兩個對象的內存地址相同,則說明指向的是同一個對象,故內容一定相同。
        if (this == o) return true;
        //類都不是同一個,更別談相等了
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        //比較兩個對象中的所有屬性,即name和age都必須相同,才可認為兩個對象相等
        return age == user.age &&
                Objects.equals(name, user.name);
    }
   
}
//打印結果:  false 	true

再次執行程序,我們會發現此時 equals 返回 true ,這才是我們想要的。

因此,當我們使用自定義對象時。如果需要讓兩個對象的內容相同時,equals 返回 true,則需要重寫 equals 方法。

為什麼要重寫 equals 和 hashCode ?

在上邊的案例中,其實我們已經說明了為什麼要去重寫 equals 。因為,在對象內容相同的情況下,我們需要讓對象相等。因此,不能用 Object 類的默認實現,只去比較內存地址,這樣是不合理的。

那 hashCode 為什麼要重寫呢? 這就涉及到集合,如 Map 和 Set (底層其實也是 Map)了。

我們以 HashMap JDK1.8的源碼來看,如 put 方法。

我們會發現,代碼中會多次進行 hash 值的比較,只有當哈希值相等時,才會去比較 equals 方法。當 hashCode 和 equals 都相同時,才會覆蓋元素。get 方法也是如此(先比較哈希值,再比較equals),

只有 hashCode 和 equals 都相等時,才認為是同一個元素,找到並返回此元素,否則返回 null。

這也對應 “hashCode 有什麼用?”這一小節。 重寫 equals 和 hashCode 的目的,就是為了方便哈希表這樣的結構快速的查詢和插入。如果不重寫,則無法比較元素,甚至造成元素位置錯亂。

重寫了 equals ,就必須要重寫 hashCode 嗎?

答案是肯定的。首先,在上邊的 JDK 源碼註釋中第第二點,我們就會發現這句說明。其次,我們嘗試重寫 equals ,而不重寫 hashCode 看會發生什麼現象。

public class TestHashCode {
    public static void main(String[] args) {
        User user1 = new User("zhangsan", 20);
        User user2 = new User("zhangsan", 20);

        HashMap<User, Integer> map = new HashMap<>();
        map.put(user1,90);
        System.out.println(map.get(user2));
    }
}
// 打印結果: null

對於代碼中的 user1 和 user2 兩個對象來說,我們認為他是同一個人張三。定義一個 map ,key 存儲 User 對象, value 存儲他的學習成績。

當把 user1 對象作為 key ,成績 90 作為 value 存儲到 map 中時,我們肯定希望,用 key 為 user2 來取值時,得到的結果是 90 。但是,結果卻大失所望,得到了 null 。

這是因為,我們自定義的 User 類,雖然重寫了 equals ,但是沒有重寫 hashCode 。當 user1 放到 map 中時,計算出來的哈希值和用 user2 去取值時計算的哈希值不相等。因此,equals 方法都沒有比較的機會。認為他們是不同的元素。然而,其實,我們應該認為 user1 和 user2 是相同的元素的。

用圖來說明就是,user1 和 user2 存放在了 HashMap 中不同的桶裡邊,導致查詢不到目標元素。

因此,當我們用自定義類來作為 HashMap 的 key 時,必須要重寫 hashCode 和 equals 。否則,會得到我們不想要的結果。

這也是為什麼,我們平時都喜歡用 String 字符串來作為 key 的原因。 因為, String 類默認就幫我們實現了 equals 和 hashCode 方法的重寫。如下,

// String.java
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        //從前向後依次比較字符串中的每個字符
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;
		//把字符串中的每個字符都取出來,參与運算
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        //把計算出來的最終值,存放在hash變量中。
        hash = h;
    }
    return h;
}

重寫 equals 時,可以使用 idea 提供的自動代碼,也可以自己手動實現。

public class User {
    ... //省略已知代碼
        
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
   
}
//此時,map.get(user2) 可以得到 90 的正確值

在重寫了 hashCode 后,使用自定義對象作為 key 時,還需要注意一點,不要在使用過程中,改變對象的內容,這樣會導致 hashCode 值發生改變,同樣得不到正確的結果。如下,

public class TestHashCode {
    public static void main(String[] args) {
        User user = new User("zhangsan", 20);

        HashMap<User, Integer> map = new HashMap<>();
        map.put(user,90);
        System.out.println(map.get(user));
        user.setAge(18); //把對象的年齡修改為18
        System.out.println(map.get(user));
    }
}
// 打印結果:
// 90
// null

會發現,修改后,拿到的值是 null 。這也是,hashCode 源碼註釋中的第一點說明的,hashCode 值不變的前提是,對象的信息沒有被修改。若被修改,則有可能導致 hashCode 值改變。

此時,有沒有聯想到其他一些問題。比如,為什麼 String 類要設計成不可以變的呢?這裏用 String 作為 HashMap 的 key 時,可以算作一個原因。你肯定不希望,放進去的時候還好好的,取出來的時候,卻找不到元素了吧。

String 類內部會有一個變量(hash)來緩存字符串的 hashCode 值。只有字符串不可變,才可以保證哈希值不變。

hashCode 相等時,equals 一定相等嗎?

很顯然不是的。在 HashMap 的源碼中,我們就能看到,當 hashCode 相等時(產生哈希碰撞),還需要比較它們的 equals ,才可以確定是否是同一個對象。因此,hashCode 相等時, equals 不一定相等 。

反過來,equals 相等的話, hashCode 一定相等嗎? 那必須的。equals 都相等了,那說明在 HashMap 中認為它們是同一個元素,所以 hashCode 值必須也要保證相等。

結論:

  • hashCode 相等,equals 不一定相等。
  • hashCode 不等,equals 一定不等。
  • equals 相等, hashCode 一定相等。
  • equals 不等, hashCode 不一定不等。

關於最後這一點,就是 hashCode 源碼註釋中提到的第三點。當 equals 不等時,不用必須保證它們的 hashCode 也不相等。但是為了提高哈希表的效率,最好設計成不等。

因為,我們既然知道它們不相等了,那麼當 hashCode 設計成不等時。只要比較 hashCode 不相等,我們就可以直接返回 null,而不必再去比較 equals 了。這樣,就減少了比較的次數,無疑提高了效率。

結尾

以上就是 hashCode 和 equals 相關的一些問題。相信已經可以解答你心中的疑惑了,也可以和面試官侃侃而談。再也不用擔心,面試官說換人了。

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

【其他文章推薦】

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

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

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

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

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

C++ Primer Plus(三)

完整閱讀C++ Primer Plus 

  系統重新學習C++語言部分,記錄重要但易被忽略的,關鍵但易被遺忘的。

 

使用類

  1、不能重載的運算符

 1 sizeof       sizeof運算符
 2 .            成員運算符
 3 .*           成員指針運算符
 4 ::           作用域解析運算符
 5 ?:           條件運算符
 6 typeid       一個RTTI運算符
 7 const_cast         強制類型轉換運算符
 8 dynamic_cast       強制類型轉換運算符
 9 reinterpret_cast   強制類型轉換運算符
10 static_cast       強制類型轉換運算符

  2、只能通過成員函數重載的運算符

1 =         賦值運算符
2 ()        函數調用運算符
3 []        下標運算符
4 ->        通過指針訪問類成員的運算符

  3、關於類的類型轉換函數,C++11支持對其使用explicit關鍵字,使其無法進行隱式類型轉換。

  4、對於定義了一個以上的轉換函數的類,編譯器在某些情況下(如將一個對象直接賦值給一個基本類型,或用cout輸出時)無法確定應該使用哪一個轉換函數(進行隱式類型轉換),因此將出現二義性錯誤,但只有一個轉換函數時,編譯器只能選擇這一個,因此不會出錯。

 

類和動態內存分配

  5、將新對象显示地初始化為現有對象時將調用拷貝構造函數,默認的拷貝構造函數將除靜態成員以外的所有成員按值賦值。

1 String a(b);
2 String a = b;
3 String a = String(b);
4 String * a = new String(b);

  將已有的對象賦值給另一個已有的對象時,會調用賦值構造函數。

  6、靜態成員函數不與特定的對象關聯,因此只能使用靜態數據成員(單例模式)。

  7、對於使用定位new運算符創建的對象,應顯式地調用其析構函數,需要注意的是,在析構時,對象的析構順序應該與創建順序相反,因為晚創建的對象可能依賴於早創建的對象,另外,只有當所有對象被銷毀后,才能釋放存儲這些對象地緩衝區。

  8、只有構造函數可以使用初始化列表語法,對於const類成員(C++11之前)和聲明為引用的類成員,必須使用這種語法,因為它們只能在被創建時初始化。

 

類繼承

  9、 公有繼承是最常用的繼承方式,它建立一種is-a關係,即派生類對象也是一個基類對象,可以對基類對象執行的任何操作,也可以對派生類對象執行。公有繼承不建立has-a關係;公有繼承不建立is-like-a關係;公有繼承不建立is-implemented-as-a(作為……來實現)關係;公有繼承不建立uses-a關係。在C++中,完全可以使用公有繼承來實現has-a、is-implemented-as-a或use-a關係,然而這樣做通常會導致編程方面的問題,因此,還是堅持使用is-a關係吧。

  10、 在基類的方法中使用關鍵字virtual可使該方法在基類已經所有派生類(包括從派生類派生出來的類)中是虛的,也就是說只要函數名相同,只需要在基類中聲明為虛函數,那它的派生類中,包括派生派生類中的這個函數都是虛函數,但為了可讀性,一般派生類中的虛函數也用virtual聲明。

  11、如果重新定義繼承的方法,應確保與原來的原型完全相同,但如果返回類型是基類引用或指針,則可以修改為指向派生類的引用或指針,這種特性被稱為返回類型協變,因為允許返回類型隨類類型的變化而變化。

  12、如果基類聲明被重載了,則應在派生類中重新定義所有的基類版本,如果只在派生類中只定義了一個版本,則另外的版本將被隱藏。

  13、C++允許純虛函數有定義,但不能在類內定義,可以在實現文件中定義。

  14、當基類和派生類都為至少一個成員採用了動態內存分配時,派生類的析構函數,拷貝構造函數,賦值構造函數都必須使用相應的基類方法來處理基類元素。對於析構函數,這是自動完成的;對於拷貝構造函數,是通過初始化列表中調用積累的拷貝構造函數完成的;對於賦值構造函數是通過使用作用域解析運算符显示地調用基類的賦值構造函數完成的。

  15、當派生類的友元函數需要訪問基類中的非公有成員時,做法是在派生類的友元函數中將派生類的引用強制類型轉換為基類的引用。

 

C++中的代碼重用

  16、當類的初始化列表包含多個項目時,這些項目的初始化順序為聲明它們的順序,而不是他們在初始化列表中的順序,如果代碼使用一個成員的值作為另一個成員初始化表達式的一部分時,初始化順序就需要引起注意。

  17、在繼承時,private是默認值,因此忽略訪問限定符也將導致私有繼承。

  18、在私有繼承時,訪問基類方法,需要使用類名加作用域解析運算符訪問;訪問基類對象(例如將基類對象當作返回值時),可以將派生類強制類型轉換為基類;訪問基類友元函數時,因為友元函數不屬於成員函數,因此不能顯式地限定函數名去訪問,可以通過顯式地轉換為基類來調用正確的函數。

  19、通常,應該使用包含來建立has-a關係,如果新類需要訪問原有類的保護成員,或需要重新定義虛函數,則應使用私有繼承。

  20、對於指向對象的類或引用中的隱式向上轉換,公有繼承直接支持,保護繼承只在派生類中支持,私有繼承不支持。

  21、如果要使私有繼承的基類中的私有函數可以在派生類外訪問,可以聲明一個公有函數,再去調用基類的私有函數,另外還可以使用using聲明:

1 class A:private B
2 {
3 public:
4     using B:foo; // 只需要有函數簽名即可,所有重載版本都可以使用
5 }

  另一種老式的方法是將基類方法名放在派生類的共有部分。

  22、在多重繼承中,如果出現了菱形繼承,頂端的基類應該使用虛繼承,防止底端的派生類包含兩份基類,同時,在構造函數的初始化列表裡應該顯式地調用頂端基類的構造函數,對於虛基類必須這樣做,否則將使用虛基類的默認構造函數,並且此時的虛基類無法通過中間的派生類去完成構造,但對於非虛基類,這是非法的。

  23、在混合使用虛基類和非虛基類,類通過多條虛途徑和非虛途徑繼承某個特定的基類時,該類將包含,一個表示所有的虛途徑即基類子對象和分別表示各條非虛途徑的多個基類子對象。

  24、使用非虛基類時,二義性的規則很簡單,只要類從不同類那裡繼承了同名的成員,使用時沒有用類名限定,則一定會導致二義性,但虛基類時不一定會導致二義性,如果某個名稱優先於其他名稱,則使用它。優先的規則是,派生類中的名稱優先於直接或間接祖先類中的相同名稱。

  25、有間接虛基類的派生類包含直接調用間接基類構造函數的構造函數,這對於間接非虛基類是非法的。

  26、 在類模板中,模板代碼不能修改參數的值,也不能使用參數得地址,在實例化時,用作表達式參數的值必須是常量表達式。

  27、使用關鍵字template並指出所需類型來聲明類時,編譯器將生成類聲明的顯式實例化,聲明必須位於模板定義所在的命名空間中。

  28、C++允許部分具體化:

1 template <class T1, class T2> class Pair{};  // 一般版本
2 template <class T1> class Pair<T1, int>{};    // 部分顯式具體化版本

  template后的<>中是沒有被具體化的參數,如果指定所有的類型,template后的<>將為空,這會導致顯式具體化。

  29、如果有多個版本可以選擇,編譯器將選擇具體化最高的版本,部分具體化特性使得能夠設置各種限制。

1 template<T> class Feeb{};
2 template<T*> class Feeb{}; // 也可以為指針提供特殊版本來部分具體化
3 
4 template<class T1, class T2, class T3> class Trio{}; //一般版本
5 template<class T1, class T2> class Trio<T1, T2, T2>{}; // 使用T2設置T3
6 template<class T1> class Trio<T1, T1*, T1*>{}; // 使用T1的指針來設置T2和T3

  30、較老的編譯器不支持模板成員,而另一些編譯器支持模板成員,但是不支持在類外面的定義。如果支持類外定義,則必須通過作用域解析運算符指出是哪個類的成員,並且使用嵌套模板的聲明方式。

1 template<typename T>
2 template<typename V>  // 嵌套方式  

  31、 對於模板類的非模板友元函數,它不是通過對象調用的,因為它不是成員函數,它可以訪問全局對象,可以使用全局指針訪問非全局對象,可以創建自己的對象,可以訪問獨立於對象的模板類靜態數據成員。

  32、在聲明模板類的約束模板友元函數(友元函數也是模板函數)時,需要在類中聲明具體化的友元函數,同時也需要在類外聲明並且給出友元函數的定義。

  33、除了使用typedef對模板進行重命名,C++11新增了別名

1 template<typename T>
2 using arrtype = std::array<T,12>;
3 arrtype<int> days;

  這種語法也適用於非模板,用於非模板時,它和typedef等價。

 

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

【其他文章推薦】

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

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

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

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

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

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

談談我對 Flutter 未來發展 和 “嵌套地獄” 的淺顯看法

Flutter 未來發展

提到 Flutter 就不得不提到 Fuchsia 系統,這是一個尚未正式發布的操作的系統,引用 Android 和 Chrome 的高級副總裁 Hiroshi Lockheimer 在一檔播客節目中對 Fuchsia 的介紹是:

不僅僅是手機和個人電腦,在物聯網的世界里,越來越多的設備需要操作系統、新的軟件運行環境等支持。我認為,在具有不同優勢和專業化的諸多操作系統中還存在很大的發展空間。Fuchsia 就是其中之一,所以,請繼續保持關注。

是的,Fuchsia 系統是為物聯網研發的操作系統,物聯網簡稱 IoT,現在全世界都在押注 IoT,包括華為、小米等國內公司。

那 Flutter 和 Fuchsia 又有什麼關係呢?

Flutter 是 Fuchsia 官方指定的唯一UI開發框架。

現在有很多物聯網操作系統 ,Fuchsia 就一定可以脫穎而出嗎?

不一定,未來的事情誰說的准呢,但在我看來 Fuchsia 是最有可能發展起來的物聯網操作系統,因為一個操作系統的發展除了本身優秀以外,最大的阻礙其實是生態,而 Fuchsia 在生態方面具有天然的優勢, 國外的一篇報道曾說:

Google 希望將 Android App 無縫移植到 Fuchsia 上,而且一直在做相關工作。

試想一下,一旦 Google 將 Android App 無縫移植到 Fuchsia 上,其他物聯網操作系統如何與之抗衡。

這裏引用 Google 公眾號底部的一句話送給大家:

預測未來不如創造未來

在跨平台技術上 Flutter 還有很多競爭對手,比如 HTML5、React Native、Weex、快應用、小程序等,我曾在跨平台技術發展簡介 中詳細說明了各個跨平台技術的發展歷史及優缺點。

Flutter 的出現會終結其他跨平台技術?我想不會的, React Native 發展了這麼多年也沒有完全乾掉 HTML5,應為 HTML5 有其獨特的應用場景,比如 營銷活動場景、新聞或者博客詳情頁面等,這些場景非常適合 HTML5。因此 Flutter 也不可能終結其他跨平台技術,總結一句話就是:

未來很長一段時間,將會是跨平台技術共存的時代,但 Flutter 適用場景更為廣闊。

Flutter 嵌套地獄

現在網絡上對 Flutter 吐槽最多大概就是 Flutter “嵌套地獄”寫法了,為什麼會出現這種現象?個人認為最大的原因就是目前大部分開源的 Flutter 項目都是這種嵌套寫法(包括我自己以前也是如此),導致後來的初學者認為這麼寫沒有問題,當項目越來越複雜時,這種嵌套寫法給項目的維護帶來了巨大的挑戰。下面說說如何避免這種嵌套寫法?

比如實現如下效果:

嵌套地獄 的寫法:

@override
Widget build(BuildContext context) {
  return Column(
    children: <Widget>[
      Container(
        height: 45,
        child: Row(
          children: <Widget>[
            SizedBox(
              width: 30,
            ),
            Icon(
              Icons.notifications,
              color: Colors.blue,
            ),
            SizedBox(
              width: 30,
            ),
            Expanded(
              child: Text('消息中心'),
            ),
            Container(
              padding: EdgeInsets.symmetric(horizontal: 10),
              decoration: BoxDecoration(
                  shape: BoxShape.rectangle,
                  borderRadius: BorderRadius.all(Radius.circular(50)),
                  color: Colors.red),
              child: Text(
                '2',
                style: TextStyle(color: Colors.white),
              ),
            ),
            SizedBox(
              width: 15,
            ),
          ],
        ),
      ),
      Divider(),
      //類似上面的布局寫6個
    ],
  );
}

上面還僅僅是第一項的布局,下面還有7個,一個30多行代碼,7個就是200多行的布局代碼,這還僅僅是布局代碼,如果加上邏輯,都不敢想象啊。

或許有一點封裝思想開發者會將每一個 Item封裝為一個方法,寫法如下:

_buildItem(IconData iconData, Color iconColor, String title, Widget widget) {
  return Container(
    height: 45,
    child: Row(
      children: <Widget>[
        SizedBox(
          width: 30,
        ),
        Icon(
          iconData,
          color: iconColor,
        ),
        SizedBox(
          width: 30,
        ),
        Expanded(
          child: Text('$title'),
        ),
        widget,
        SizedBox(
          width: 15,
        ),
      ],
    ),
  );
}

@override
Widget build(BuildContext context) {
  return Column(
    children: <Widget>[
      _buildItem(...),
      Divider(),
      _buildItem(...),
      Divider(),
      _buildItem(...),
      Divider(),
      _buildItem(...),
      Divider(),
      _buildItem(...),
      Divider(),
      _buildItem(...),
      Divider(),
    ],
  );
}

這樣看起來好多了,基本解決了嵌套地獄問題,但這樣寫還存在一個非常大的問題-性能問題,一旦其中一個数字發生變化,整個頁面都要重建,Flutter 開發中非常重要的一個原則就是 盡可能少的重建組件,因此將上面封裝到方法中組件變為一個 Widget。

class SettingDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        _SettingItem(
          iconData: Icons.notifications,
          iconColor: Colors.blue,
          title: '消息中心',
          suffix: _NotificationsText(
            text: '2',
          ),
        ),
        Divider(),
        _SettingItem(
          iconData: Icons.thumb_up,
          iconColor: Colors.green,
          title: '我贊過的',
          suffix: _Suffix(
            text: '121篇',
          ),
        ),
        Divider(),
        _SettingItem(
          iconData: Icons.grade,
          iconColor: Colors.yellow,
          title: '收藏集',
          suffix: _Suffix(
            text: '2個',
          ),
        ),
        Divider(),
        _SettingItem(
          iconData: Icons.shopping_basket,
          iconColor: Colors.yellow,
          title: '已購小冊',
          suffix: _Suffix(
            text: '100個',
          ),
        ),
        Divider(),
        _SettingItem(
          iconData: Icons.account_balance_wallet,
          iconColor: Colors.blue,
          title: '我的錢包',
          suffix: _Suffix(
            text: '10萬',
          ),
        ),
        Divider(),
        _SettingItem(
          iconData: Icons.location_on,
          iconColor: Colors.grey,
          title: '閱讀過的文章',
          suffix: _Suffix(
            text: '1034篇',
          ),
        ),
        Divider(),
        _SettingItem(
          iconData: Icons.local_offer,
          iconColor: Colors.grey,
          title: '標籤管理',
          suffix: _Suffix(
            text: '27個',
          ),
        ),
      ],
    );
  }
}

class _SettingItem extends StatelessWidget {
  const _SettingItem(
      {Key key, this.iconData, this.iconColor, this.title, this.suffix})
      : super(key: key);

  final IconData iconData;
  final Color iconColor;
  final String title;
  final Widget suffix;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 45,
      child: Row(
        children: <Widget>[
          SizedBox(
            width: 30,
          ),
          Icon(iconData,color: iconColor,),
          SizedBox(
            width: 30,
          ),
          Expanded(
            child: Text('$title'),
          ),
          suffix,
          SizedBox(
            width: 15,
          ),
        ],
      ),
    );
  }
}

class _NotificationsText extends StatelessWidget {
  final String text;

  const _NotificationsText({Key key, this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.symmetric(horizontal: 10),
      decoration: BoxDecoration(
          shape: BoxShape.rectangle,
          borderRadius: BorderRadius.all(Radius.circular(50)),
          color: Colors.red),
      child: Text(
        '$text',
        style: TextStyle(color: Colors.white),
      ),
    );
  }
}

class _Suffix extends StatelessWidget {
  final String text;

  const _Suffix({Key key, this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
      '$text',
      style: TextStyle(color: Colors.grey.withOpacity(.5)),
    );
  }
}

封裝為一個個單獨的小組件,將有變化的組件盡量單獨封裝,這樣就不會重建整個控件樹,增強了可讀性和可維護性,而且對性能有很大的提升。

最後總結一句:

雖然 Flutter 一切皆是組件,但並不代表一切都要寫在組件中。

當然這僅僅是我個人的看法,如果您有更好的方法歡迎一起討論,從我做起,規範寫法,為 Flutter 發展貢獻做出一點微不足道的貢獻。

交流

老孟Flutter博客地址(330個控件用法):http://laomengit.com

歡迎加入Flutter交流群(微信:laomengit)、關注公眾號【老孟Flutter】:

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

【其他文章推薦】

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

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

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

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

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

2. 背包,隊列和棧

  許多基礎數據類型都和對象的集合有關。數據類型的值就是一組對象的集合,所有操作都是關於添加,刪除或是訪問集合中的對象。背包(Bag),隊列(Quene)和棧(Stack) 它們的不同之處在於刪除或者訪問對象的順序不同。

  

  1. API

  

  Stack 和 Quene 都含有一個能夠刪除集合中特定元素的方法。

  實現上面API需要高級語言的特性:泛型,裝箱拆箱,可迭代(實現 IEnumerable 接口)。

  

  1. 背包

  背包是一種不支持從中刪除元素的集合類型——它的目的就是幫助用例收集元素并迭代遍歷所有元素。用例也可以使用棧或者隊列,但使用 Bag 可以說明元素的處理順序不重要。

  

  2.先進先出隊列

  隊列是基於先進先出(FIFO)策略的集合類型。

 

  3. 下壓棧

  下壓棧(簡稱棧)是一種基於後進先出(LIFO)策略的集合類型。

  應用例子:計算輸入字符串  (1+((2+3)*(4*5)))表達式的值。

  使用雙棧解決:

    1. 將操作數壓入操作數棧;

    2. 將運算符壓入運算符棧;

    3. 忽略做括號;

    4. 在遇到右括號時,彈出一個運算符,彈出所需數量的操作數,並將運算符和操作數的運算結果壓入操作數棧。

 

  2.用數組實現

  實現下壓棧:

    //想要數據類型可迭代,需要實現IEnumerable
    public class ResizingStack<Item> : IEnumerable<Item>
    {
        private Item[] a = new Item[1];
        private int N = 0;
        public bool IsEmpty{ get {
                return N == 0;
            } }
        public int Size { get {
                return N;
            } }
        public int Count { get; set; }

        /// <summary>
        /// 使數組處於半滿
        /// </summary>
        /// <param name="max"></param>
        private void Resize(int max)
        {
            Count = 0;
            Item[] temp = new Item[max];
            for(var i = 0;i<N;i++)
            {
                temp[i] = a[i];
                Count++;
            }
            a = temp;
        }

        public void push(Item item)
        {
            if (N == a.Length)
                Resize(a.Length * 2);
            a[N++] = item;
        }

        public Item Pop()
        {
            Item item = a[--N];
            a[N] = default(Item); //避免對象遊離
            if (N > 0 && N == a.Length / 4)
                Resize(a.Length/2);
            return item;
        }

        IEnumerator<Item> IEnumerable<Item>.GetEnumerator()
        {
            return new ResizingStackEnumerator<Item>(a);
        }

        public IEnumerator GetEnumerator()
        {
            return new ResizingStackEnumerator<Item>(a);
        }

    }
    class ResizingStackEnumerator<Item> : IEnumerator<Item>
    {
        private Item[] a;
        private int N = 0;
        public ResizingStackEnumerator(Item[] _a)
        {
            a = _a;
            N = a.Length-1;
        }

        public object Current => a[N--];

        Item IEnumerator<Item>.Current => a[N--];

        public void Dispose()
        {
            throw new NotImplementedException();
        }

        public bool MoveNext()
        {
            return N > 0;
        }

        public void Reset()
        {
            throw new NotImplementedException();
        }
    }

  

  3.鏈表

  鏈表是在集合類的抽象數據類型實現中表示數據的另一種基礎數據結構。

  定義:鏈表是一種遞歸的數據結構,它或者指向空,或者指向另一個節點的引用,該節點含有一個泛型元素和一個指向另一個鏈表的引用。

    class Node<Item>
    {
        public Item item { get; set; }
        public Node<Item> Next { get; set; }
    }

  1.構造鏈表

  鏈表表示的是一列元素。

  根據遞歸的定義,只需要一個 Node 類型的變量就能表示一條鏈表,只要保證它的 Next 值是 null 或指向另一個 Node 對象,該對象的 Next 指向另一條鏈表。

  

 

  2.在表頭插入結點

  在鏈表列表中插入新節點的最簡單位置是開始。要在首結點為 first 的給定鏈表開頭插入字符串 not ,先將 first 保存在 oldfirst 中,然後將一個新結點賦予 first ,並將 first 的 item 設為 not, Next  設置為 oldfirst 。

  

  在鏈表開頭插入一個結點所需的時間和鏈表長度無關。

 

  3.從表頭刪除結點

  只需將 first 指向 first.next 即可。first 原來指向的對象變成了一個孤兒,垃圾回收機制會將其回收。

 

  同樣,該操作所需的時間和鏈表長度無關。

 

  4.在表尾插入結點

  當鏈表不止有一個結點時,需要一個指向鏈表最後結點的鏈接 oldlast,創建新的結點,last 指向新的最後結點。然後 oldlast.next  指向 last。

  當鏈表只有一個結點時,首結點又是尾結點。只需將 last 指向新的結點,然後 first.next 指向 last。

 

  5.其他位置的插入和刪除操作

  上述操作可以很容易的實現,但是下面的操作比較複雜:

    1. 刪除指定的結點

    2. 在指定結點前插入一個新結點

  這些操作需要我們遍歷鏈表,它所需的時間和鏈表的長度成正比。想要實現任意插入和刪除結點需要使用雙向鏈表,其中每個結點都含有兩個鏈接,分別指向上一個和下一個結點。

 

  6. 遍歷

  簡單實現:

    public class Bag<Item>
    {
        private Node<Item> first;
        public void Add(Item item)
        {
            Node<Item> oldFirst = first;
            first = new Node<Item>() { 
                item = item,
                Next = oldFirst
            };

        }
    }
            Bag<int> bags = new Bag<int>();
            for (var i = 0; i < 10; i++)
            {
                bags.Add(i);
            }

            for (var x = bags.first; x != null; x = x.Next)
            {
                Console.WriteLine(x.item);
            }

  

  實現 IEnumerable 接口 實現遍歷:

    public class Bag<Item>: IEnumerable<Item>
    {
        public Node<Item> first;
        public void Add(Item item)
        {
            Node<Item> oldFirst = first;
            first = new Node<Item>() { 
                item = item,
                Next = oldFirst
            };

        }

        public IEnumerator<Item> GetEnumerator()
        {
            return new LineEnumerator<Item>(first);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return new LineEnumerator<Item>(first);
        }
    }

    public class LineEnumerator<Item> : IEnumerator<Item>
    {
        public Node<Item> first;
        public LineEnumerator(Node<Item> _first)
        {
            first = _first;
        }
        public Item Current { get {
                var oldfirst = first;
                first = first.Next;
                return oldfirst.item;
            } }

        object IEnumerator.Current => first;

        public void Dispose()
        {
            return;
        }

        public bool MoveNext()
        {
            if (first != null)
                return true;
            return false;
        }

        public void Reset()
        {
            throw new NotImplementedException();
        }
    }
        public static void LineTest()
        {
            Bag<int> bags = new Bag<int>();
            for (var i = 0; i < 10; i++)
            {
                bags.Add(i);
            }

            foreach(var bag in bags)
            {
                Console.WriteLine(bag);
            }
        }

 

  4. 用鏈表實現背包

  見上述代碼。

 

  5. 用鏈表實現棧

  Stack API 中 Pop() 刪除一個元素,按照前面的從表頭刪除結點實現,Push() 添加一個元素,按照前面在表頭插入結點。 

    public class Stack<Item> : IEnumerable<Item>
    {
        public Node<Item> first;
        private int N;


        public bool IsEmpty()
        {
            return first == null; //或 N == 0
        }

        public int Size()
        {
            return N;
        }

        public void Push(Item item)
        {
            Node<Item> oldfirst = first;
            first = new Node<Item>() { 
                item = item,
                Next = oldfirst
            };
            N++;
        }

        public Item Pop()
        {
            Item item = first.item;
            first = first.Next;
            N--;
            return item;
        }

        public IEnumerator<Item> GetEnumerator()
        {
            return new StackLineIEnumerator<Item>(first);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return new StackLineIEnumerator<Item>(first);
        }
    }

    public class StackLineIEnumerator<Item> : IEnumerator<Item>
    {
        private Node<Item> first;
        public StackLineIEnumerator(Node<Item> _first)
        {
            first = _first;
        }
        public Item Current { get {
                var oldfirst = first;
                first = first.Next;
                return oldfirst.item;
            } }

        object IEnumerator.Current => throw new NotImplementedException();

        public void Dispose()
        {
            return;
        }

        public bool MoveNext()
        {
            return first != null;
        }

        public void Reset()
        {
            throw new NotImplementedException();
        }
    }

  鏈表的使用達到了最優設計目標:

    1. 可以處理任意類型的數據;

    2. 所需的空間總是和集合的大小成正比;

    3. 操作所需的時間總是和集合的大小無關;

  

   6. 用鏈表實現隊列

  需要兩個實例變量,first 指向隊列的開頭,last 指向隊列的表尾。添加一個元素 Enquene() ,將結點添加到表尾(鏈表為空時,first 和 last 都指向新結點)。刪除一個元素 Dequene() ,刪除表頭的結點(刪除后,當隊列為空時,將 last 更新為 null)。

    public class Quene<Item> : IEnumerable<Item>
    {
        public Node<Item> first;
        public Node<Item> last;
        private int N;

        public bool IsEmpty()
        {
            return first == null;
        }

        public int Size()
        {
            return N;
        }

        public void Enquene(Item item)
        {
            var oldlast = last;
            last = new Node<Item>() { 
                item = item,
                Next = null
            };

            if (IsEmpty())
                first = last;
            else
                oldlast.Next = last;
            N++;
        }

        public Item Dequene()
        {
            if (IsEmpty())
                throw new Exception();
            Item item = first.item;
            first = first.Next;
            if (IsEmpty())
                last = null;
            N--;
            return item;
        }

        public IEnumerator<Item> GetEnumerator()
        {
            return new QueneLineEnumerator<Item>(first);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return new QueneLineEnumerator<Item>(first);
        }
    }
    public class QueneLineEnumerator<Item> : IEnumerator<Item>
    {
        private Node<Item> first;
        public QueneLineEnumerator(Node<Item> _first)
        {
            first = _first;
        }
        public Item Current { get {
                var oldfirst = first;
                first = first.Next;
                return oldfirst.item;
            } }

        object IEnumerator.Current => throw new NotImplementedException();

        public void Dispose()
        {
            return;
        }

        public bool MoveNext()
        {
            return first != null ;
        }

        public void Reset()
        {
            throw new NotImplementedException();
        }
    }

   

  7. 總結

  在結構化存儲數據集時,鏈表是數組的一種重要的替代方式。

  數組和鏈表這兩種數據類型為研究算法和更高級的數據結構打下了基礎。

  基礎數據結構:

數據結構 優點 缺點
數組 通過索引可以直接訪問任意元素 在初始化時就需要知道元素的數量
鏈表 使用的空間大小和元素數量成正比 需要同引用訪問任意元素

  

  在研究一個新的應用領域時,可以按照以下步驟識別目標,定義問題和使用數據抽象解決問題:

  1. 定義 API

  2. 根據特定的應用場景開發用例代碼

  3. 描述一種數據結構(即一組值的表示),並在 API 的實現中根據它定義類的實例變量。

  4. 描述算法,即實現 API,並根據它應用於用例

  5. 分析算法的性能

 

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

【其他文章推薦】

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

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

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

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

※超省錢租車方案

MongoDB副本集replica set (二)–副本集環境搭建

(一)主機信息

操作系統版本:centos7 64-bit

數據庫版本   :MongoDB 4.2 社區版

ip hostname
192.168.10.41 mongoserver1
192.168.10.42 mongoserver2
192.168.10.43 mongoserver3

(二)副本集搭建過程

首先需要在3台服務器上安裝MongoDB軟件,安裝過程見:https://www.cnblogs.com/lijiaman/p/12983589.html。安裝完成之後,即可進行後續的配置,具體操作如下:

(1)在一台機器上創建keyfile

openssl rand -base64 756 > /mongo/mongo-keyfile
chmod 400 /mongo/mongo-keyfile

(2)拷貝feyfile到所有節點

scp /mongo/mongo-keyfile root@192.168.10.42:/mongo/
scp /mongo/mongo-keyfile root@192.168.10.43:/mongo/

(3)以啟用身份驗證的方式開啟所有節點
這裏將所有參數設置到配置文件裏面,方便管理,配置文件如下:

[root@mongodbserver1 mongo]# cat /etc/mongod.conf
# mongod.conf

# for documentation of all options, see:
# http://docs.mongodb.org/manual/reference/configuration-options/

# where to write logging data.
systemLog:
destination: file
logAppend: true
path: /mongo/mongod.log

# Where and how to store data.
storage:
dbPath: /mongo/data
journal:
enabled: true
# engine:
# mmapv1:
# wiredTiger:

# how the process runs
processManagement:
fork: true # fork and run in background
pidFilePath: /mongo/mongod.pid # location of pidfile

# network interfaces
net:
port: 27017
bindIp: 0.0.0.0 # Listen to local interface only, comment to listen on all interfaces.

security:
authorization: enabled                 # 啟用身份驗證
keyFile: /mongo/mongo-keyfile         # 配置keyfile文件
 
replication:
replSetName: rstest                    # 設置副本集名稱

然後啟動所有節點,以節點1為例:

[root@mongodbserver1 mongo]# mongod -f /etc/mongod.conf

(4)初始化副本集
在其中一個節點執行以下腳本初始化副本集,只需在一個節點上執行即可。

rs.initiate(
{
_id : "rstest",
members: [
{ _id : 0, host : "192.168.10.41:27017" },
{ _id : 1, host : "192.168.10.42:27017" },
{ _id : 2, host : "192.168.10.43:27017" }
]
}
)

參數含義:
_id          :副本集的名稱
members :副本集的成員信息

在初始化時,會觸發投票選舉一個主節點,可以使用rs.status()來確定主節點成員

rstest:SECONDARY> rs.status()
...
"members" : [
{
"_id" : 0,
"name" : "192.168.10.41:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 280,
"optime" : {
"ts" : Timestamp(1592897767, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2020-06-23T07:36:07Z"),
"syncingTo" : "",
"syncSourceHost" : "",
"syncSourceId" : -1,
"infoMessage" : "",
"electionTime" : Timestamp(1592897607, 1),
"electionDate" : ISODate("2020-06-23T07:33:27Z"),
"configVersion" : 1,
"self" : true,
"lastHeartbeatMessage" : ""
},
...

(5)創建管理員用戶
第一個用戶必須要有創建其它用戶的權限,例如需要有userAdminAnyDatabase權限,並且需要創建在admin數據庫中。
因為是在副本集上創建用戶,故要在主節點上執行。如創建root用戶

use admin;

db.createUser(
{
user:"root",
pwd:"123456",
roles:[{role:"userAdminAnyDatabase",db:"admin"}]
}
)

(6)以管理員身份登錄數據庫
通過以下方式以管理員身份登錄到數據庫

mongo -u root -p 123456 --authenticationDatabase admin

(7)創建一個集群管理員賬戶
clusterAdmin角色被授予副本集操作的權限,如配置副本集。在admin數據庫中創建一個集群管理員並授予clusterAdmin角色。

use admin

db.createUser(
{
"user" : "replica",
"pwd" : "replica",
roles: [ { "role" : "clusterAdmin", "db" : "admin" } ]
}
)

(8)要啟用身份驗證,需要重啟數據庫
重啟完成后,就需要以用戶密碼方式登錄數據庫了,假如不使用用戶名密碼,可以登錄數據庫,但是無法訪問數據

[root@mongodbserver2 mongo]# mongo
MongoDB shell version v4.2.7
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("d49b410b-a7af-4550-a455-faa82885517b") }
MongoDB server version: 4.2.7
rstest:PRIMARY> show dbs
rstest:PRIMARY> 
rstest:PRIMARY> db
test

只有使用了用戶名密碼,才能查到數據:

[root@mongodbserver2 mongo]# mongo -u root -p 123456 --authenticationDatabase admin 
MongoDB shell version v4.2.7
connecting to: mongodb://127.0.0.1:27017/?authSource=admin&compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("a1f0da48-1266-4766-a9e4-32b97a46c3ec") }
MongoDB server version: 4.2.7
rstest:PRIMARY> 
rstest:PRIMARY> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB

【完】

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

OpenCV開發筆記(六十五):紅胖子8分鐘帶你深入了解ORB特徵點(圖文並茂+淺顯易懂+程序源碼)

若該文為原創文章,未經允許不得轉載
原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客導航:https://blog.csdn.net/qq21497936/article/details/102478062
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/106926496
各位讀者,知識無窮而人力有窮,要麼改需求,要麼找專業人士,要麼自己研究
紅胖子(紅模仿)的博文大全:開發技術集合(包含Qt實用技術、樹莓派、三維、OpenCV、OpenGL、ffmpeg、OSG、單片機、軟硬結合等等)持續更新中…(點擊傳送門)

OpenCV開發專欄(點擊傳送門)

上一篇:《OpenCV開發筆記(六十四):紅胖子8分鐘帶你深入了解SURF特徵點(圖文並茂+淺顯易懂+程序源碼)》
下一篇:持續補充中…

 

前言

  紅胖子,來也!
  識別除了傳統的模板匹配之外就是體征點了,前面介紹了Suft特徵點,還有一個傳統的就會ORB特徵點了。
  其實識別的特徵點多種多樣,既可以自己寫也可以使用opencv為我們提供的,一般來說根據特徵點的特性和效率,選擇適合我們場景的特徵就可以了。
  本篇,介紹ORB特徵提取。

 

Demo

  
  
  
  

 

ORB特徵點

概述

  ORB是ORiented Brief的簡稱,是briedf算法的改進版,於2011年在《ORB:an fficient alternative to SIFT or SURF》中提出。
ORB算法分為兩部分,分別是特徵點提取和特徵點描述:

  • 特徵提取:由FAST(Features from Accelerated Segment Test)算法發展來的;
  • 特徵點描述:根據BRIEF(Binary Robust IndependentElementary Features)特徵描述算法改進的。

  ORB特徵是將FAST特徵點的檢測方法與BRIEF特徵描述子結合起來,並在它們原來的基礎上做了改進與優化。據說,ORB算法的速度是sift的100倍,是surf的10倍。

Brief描述子

  該特徵描述子是在特徵點附近隨機選取若干點對,將這些點對的灰度值的大小,組合成一個二進制串,組合成一個二進制傳,並將這個二進制串作為該特徵點的特徵描述子。
  Brief的速度快,但是使用灰度值作為描述字計算的源頭,毫無疑問會有一些顯而易見的問題:

  • 旋轉后灰度變了導致無法識別,因其不具備旋轉不變形;
  • 由於是計算灰度,噪聲灰度化則無法去噪,所以對噪聲敏感;
  • 尺度不同影響灰度計算,所以也不具備尺度不變形;
    ORB是試圖使其具備旋轉不變性和降低噪聲敏感度而提出的。

特徵檢測步驟

步驟一:使用brief算子的方式初步提取。

  該步能夠提取大量的特徵點,但是有很大一部分的特徵點的質量不高。從圖像中選取一點P,以P為圓心畫一個半徑為N像素半徑的圓。圓周上如果有連續n個像素點的灰度值比P點的灰度值大或者小,則認為P為特徵點。
  

步驟二:機器學習的方法篩選最優特徵點。

  通俗來說就是使用ID3算法訓練一個決策樹,將特徵點圓周上的16個像素輸入決策樹中,以此來篩選出最優的FAST特徵點。

步驟三:非極大值抑制去除局部較密集特徵點。

  使用非極大值抑制算法去除臨近位置多個特徵點的問題。為每一個特徵點計算出其響應大小。計算方式是特徵點P和其周圍16個特徵點偏差的絕對值和。在比較臨近的特徵點中,保留響應值較大的特徵點,刪除其餘的特徵點。

步驟四:使用金字塔來實現多尺度不變形。

步驟五:使用圖像的矩判斷特徵點的旋轉不變性

  ORB算法提出使用矩(moment)法來確定FAST特徵點的方向。也就是說通過矩來計算特徵點以r為半徑範圍內的質心,特徵點坐標到質心形成一個向量作為該特徵點的方向。

ORB類的使用

cv::Ptr<cv::ORB> _pOrb = cv::ORB::create();
std::vector<cv::KeyPoint> keyPoints1;
//特徵點檢測
_pOrb->detect(srcMat, keyPoints1);

ORB相關函數原型

static Ptr<ORB> create(int nfeatures=500,
                       float scaleFactor=1.2f,
                       int nlevels=8,
                       int edgeThreshold=31,
                       int firstLevel=0,
                       int WTA_K=2,
                       int scoreType=ORB::HARRIS_SCORE,
                       int patchSize=31,
                       int fastThreshold=20);
  • 參數一:int類型的nfeatures,用於ORB的,保留最大的關鍵點數,默認值500;
  • 參數二:float類型的scaleFactor,比例因子,大於1時為金字塔抽取比。的等於2表示經典的金字塔,每一個下一層的像素比上一層少4倍,但是比例係數太大了將顯著降低特徵匹配分數。另一方面,太接近1個比例因子這意味着要覆蓋一定的範圍,你需要更多的金字塔級別,所以速度會受影響的,默認值1.2f;
  • 參數三:int類型的nlevels,nlevels金字塔級別的數目。最小級別的線性大小等於輸入圖像線性大小/功率(縮放因子,nlevels-第一級),默認值為8;
  • 參數四:int類型的edgeThreshold,edgeThreshold這是未檢測到功能的邊框大小。它應該大致匹配patchSize參數。;
  • 參數五:int類型的firstLevel,要將源圖像放置到的金字塔級別。以前的圖層已填充使用放大的源圖像;
  • 參數六:int類型的WTA_K,生成定向簡短描述符的每個元素的點數。這個默認值2是指取一個隨機點對並比較它們的亮度,所以我們得到0/1的響應。其他可能的值是3和4。例如,3表示我們取3隨機點(當然,這些點坐標是隨機的,但是它們是由預定義的種子,因此簡短描述符的每個元素都是從像素確定地計算出來的矩形),找到最大亮度點和獲勝者的輸出索引(0、1或2)。如此輸出將佔用2位,因此需要一個特殊的漢明距離變量,表示為NORM_HAMMING2(每箱2位)。當WTA_K=4時,我們取4個隨機點計算每個點bin(也將佔用可能值為0、1、2或3的2位)。;
  • 參數七:int類型的scoreType,HARRIS_SCORES表示使用HARRIS算法對特徵進行排序(分數寫入KeyPoint::score,用於保留最佳nfeatures功能);FAST_SCORE是產生稍微不穩定關鍵點的參數的替代值,但計算起來要快一點;
  • 參數八:int類型的patchSize,定向簡短描述符使用的修補程序的大小。當然,在較小的金字塔層特徵覆蓋的感知圖像區域將更大;
  • 參數九:int類型的fastThreshold,快速閾值;
void xfeatures2d::SURT::detect( InputArray image,
                                std::vector<KeyPoint>& keypoints,
                                InputArray mask=noArray() );
  • 參數一:InputArray類型的image,輸入cv::Mat;
  • 參數二:std::Vector類型的keypoints,檢測到的關鍵點;
  • 參數三:InputArray類型的mask,默認為空,指定在何處查找關鍵點的掩碼(可選)。它必須是8位整數感興趣區域中具有非零值的矩陣。;
void xfeatures2d::SURT::compute( InputArray image,
                                 std::vector<KeyPoint>& keypoints,
                                 OutputArray descriptors );
  • 參數一:InputArray類型的image,輸入cv::Mat;
  • 參數二:std::Vector類型的keypoints,描述符不能為其已刪除計算的。有時可以添加新的關鍵點,例如:SIFT duplicates keypoint有幾個主要的方向(每個方向);
  • 參數三:OutputArray類型的descriptors,計算描述符;
// 該函數結合了detect和compute,參照detect和compute函數參數
void xfeatures2d::SURT::detectAndCompute( InputArray image,
                                          InputArray mask,
                                          std::vector<KeyPoint>& keypoints,
                                          OutputArray descriptors,
                                          bool useProvidedKeypoints=false );

繪製關鍵點函數原型

void drawKeypoints( InputArray image,
                    const std::vector<KeyPoint>& keypoints,
                    InputOutputArray outImage,
                    const Scalar& color=Scalar::all(-1),
                    int flags=DrawMatchesFlags::DEFAULT );
  • 參數一:InputArray類型的image,;
  • 參數二:std::Vector類型的keypoints,原圖的關鍵點;
  • 參數三:InputOutputArray類型的outImage,其內容取決於定義在輸出圖像。請參閱參數五的標誌flag);
  • 參數四:cv::Scalar類型的color,繪製關鍵點的顏色,默認為Scalar::all(-1)隨機顏色,每個點都是這個顏色,那麼隨機時,每個點都是隨機的;
  • 參數五:int類型的flags,默認為DEFAULT,具體參照DrawMatchesFlags枚舉如下:

 

相關博客

  本源碼中包含了“透視變換”,請參照博文《OpenCV開發筆記(五十一):紅胖子8分鐘帶你深入了解透視變換(圖文並茂+淺顯易懂+程序源碼)》

 

特徵點總結

  根據前面連續三篇的特徵點,我們其實可以猜到了所有的匹配都是這樣提取特徵點,然後使用一些算法來匹配,至於使用什麼特徵點提取就是需要開發者根據實際的經驗去選取,單一的特徵點/多種特徵點提取混合/自己寫特徵點等等多種方式去提取特徵點,為後一步的特徵點匹配做準備,特徵點通用的就到此篇,後續會根據實際開發項目中使用的到隨時以新的篇章博文去補充。
  《OpenCV開發筆記(六十三):紅胖子8分鐘帶你深入了解SIFT特徵點(圖文並茂+淺顯易懂+程序源碼)》
  《OpenCV開發筆記(六十四):紅胖子8分鐘帶你深入了解SURF特徵點(圖文並茂+淺顯易懂+程序源碼》
  《OpenCV開發筆記(六十五):紅胖子8分鐘帶你深入了解ORB特徵點(圖文並茂+淺顯易懂+程序源碼)》

 

Demo源碼

void OpenCVManager::testOrbFeatureDetector()
{
    QString fileName1 = "13.jpg";
    int width = 400;
    int height = 300;

    cv::Mat srcMat = cv::imread(fileName1.toStdString());
    cv::resize(srcMat, srcMat, cv::Size(width, height));

    cv::String windowName = _windowTitle.toStdString();
    cvui::init(windowName);

    cv::Mat windowMat = cv::Mat(cv::Size(srcMat.cols * 2, srcMat.rows * 3),
                                srcMat.type());
    cv::Ptr<cv::ORB> _pObr = cv::ORB::create();

    int k1x = 0;
    int k1y = 0;
    int k2x = 100;
    int k2y = 0;
    int k3x = 100;
    int k3y = 100;
    int k4x = 0;
    int k4y = 100;
    while(true)
    {
        windowMat = cv::Scalar(0, 0, 0);

        cv::Mat mat;

        // 原圖先copy到左邊
        mat = windowMat(cv::Range(srcMat.rows * 1, srcMat.rows * 2),
                        cv::Range(srcMat.cols * 0, srcMat.cols * 1));
        cv::addWeighted(mat, 0.0f, srcMat, 1.0f, 0.0f, mat);

        {
            std::vector<cv::KeyPoint> keyPoints1;
            std::vector<cv::KeyPoint> keyPoints2;

            cvui::printf(windowMat, 0 + width * 1, 10 + height * 0, "k1x");
            cvui::trackbar(windowMat, 0 + width * 1, 20 + height * 0, 165, &k1x, 0, 100);
            cvui::printf(windowMat, 0 + width * 1, 70 + height * 0, "k1y");
            cvui::trackbar(windowMat, 0 + width * 1, 80 + height * 0, 165, &k1y, 0, 100);

            cvui::printf(windowMat, width / 2 + width * 1, 10 + height * 0, "k2x");
            cvui::trackbar(windowMat, width / 2 + width * 1, 20 + height * 0, 165, &k2x, 0, 100);
            cvui::printf(windowMat, width / 2 + width * 1, 70 + height * 0, "k2y");
            cvui::trackbar(windowMat, width / 2 + width * 1, 80 + height * 0, 165, &k2y, 0, 100);

            cvui::printf(windowMat, 0 + width * 1, 10 + height * 0 + height / 2, "k3x");
            cvui::trackbar(windowMat, 0 + width * 1, 20 + height * 0 + height / 2, 165, &k3x, 0, 100);
            cvui::printf(windowMat, 0 + width * 1, 70 + height * 0 + height / 2, "k3y");
            cvui::trackbar(windowMat, 0 + width * 1, 80 + height * 0 + height / 2, 165, &k3y, 0, 100);

            cvui::printf(windowMat, width / 2 + width * 1, 10 + height * 0 + height / 2, "k4x");
            cvui::trackbar(windowMat, width / 2 + width * 1, 20 + height * 0 + height / 2, 165, &k4x, 0, 100);
            cvui::printf(windowMat, width / 2 + width * 1, 70 + height * 0 + height / 2, "k4y");
            cvui::trackbar(windowMat, width / 2 + width * 1, 80 + height * 0 + height / 2, 165, &k4y, 0, 100);

            std::vector<cv::Point2f> srcPoints;
            std::vector<cv::Point2f> dstPoints;

            srcPoints.push_back(cv::Point2f(0.0f, 0.0f));
            srcPoints.push_back(cv::Point2f(srcMat.cols - 1, 0.0f));
            srcPoints.push_back(cv::Point2f(srcMat.cols - 1, srcMat.rows - 1));
            srcPoints.push_back(cv::Point2f(0.0f, srcMat.rows - 1));

            dstPoints.push_back(cv::Point2f(srcMat.cols * k1x / 100.0f, srcMat.rows * k1y / 100.0f));
            dstPoints.push_back(cv::Point2f(srcMat.cols * k2x / 100.0f, srcMat.rows * k2y / 100.0f));
            dstPoints.push_back(cv::Point2f(srcMat.cols * k3x / 100.0f, srcMat.rows * k3y / 100.0f));
            dstPoints.push_back(cv::Point2f(srcMat.cols * k4x / 100.0f, srcMat.rows * k4y / 100.0f));

            cv::Mat M = cv::getPerspectiveTransform(srcPoints, dstPoints);
            cv::Mat srcMat2;
            cv::warpPerspective(srcMat,
                                srcMat2,
                                M,
                                cv::Size(srcMat.cols, srcMat.rows),
                                cv::INTER_LINEAR,
                                cv::BORDER_CONSTANT,
                                cv::Scalar::all(0));

            mat = windowMat(cv::Range(srcMat.rows * 1, srcMat.rows * 2),
                            cv::Range(srcMat.cols * 1, srcMat.cols * 2));
            cv::addWeighted(mat, 0.0f, srcMat2, 1.0f, 0.0f, mat);

            //特徵點檢測
            _pObr->detect(srcMat, keyPoints1);
            //繪製特徵點(關鍵點)
            cv::Mat resultShowMat;
            cv::drawKeypoints(srcMat,
                             keyPoints1,
                             resultShowMat,
                             cv::Scalar(0, 0, 255),
                             cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
            mat = windowMat(cv::Range(srcMat.rows * 2, srcMat.rows * 3),
                            cv::Range(srcMat.cols * 0, srcMat.cols * 1));
            cv::addWeighted(mat, 0.0f, resultShowMat, 1.0f, 0.0f, mat);

            //特徵點檢測
            _pObr->detect(srcMat2, keyPoints2);
            //繪製特徵點(關鍵點)
            cv::Mat resultShowMat2;
            cv::drawKeypoints(srcMat2,
                             keyPoints2,
                             resultShowMat2,
                             cv::Scalar(0, 0, 255),
                             cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
            mat = windowMat(cv::Range(srcMat.rows * 2, srcMat.rows * 3),
                           cv::Range(srcMat.cols * 1, srcMat.cols * 2));
            cv::addWeighted(mat, 0.0f, resultShowMat2, 1.0f, 0.0f, mat);

            cv::imshow(windowName, windowMat);
        }
        // 更新
        cvui::update();
        // 显示
        // esc鍵退出
        if(cv::waitKey(25) == 27)
        {
            break;
        }
    }
}

 

工程模板:對應版本號v1.59.0

  對應版本號v1.59.0

 

上一篇:《OpenCV開發筆記(六十四):紅胖子8分鐘帶你深入了解SURF特徵點(圖文並茂+淺顯易懂+程序源碼)》
下一篇:持續補充中…

 

原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客導航:https://blog.csdn.net/qq21497936/article/details/102478062
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/106926496

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

FreeSql.Generator命令行代碼生成器是如何實現的

目錄

  • FreeSql介紹
  • FreeSql.Generator
  • RazorEngine.NetCore
  • 源碼解析
  • FreeSql.Tools

FreeSql

FreeSql 是功能強大的對象關係映射技術(O/RM),支持 .NETCore 2.1+ 或 .NETFramework 4.0+ 或 Xamarin。

有一個強大的ORM,也方便我們開發一個代碼生成器。

一般情況下,我們開發數據庫相關的應用,主要分為三種code first、db first、model first

我只用過前二種,

  • code first,代碼優先,數據庫都是根據實體類生成,所有的關係,可以是邏輯關聯,也可以是物理關聯。
  • DB First: 數據庫優先,直接設計表結構,用設計工具生成表,設計主鍵,外鍵、索引,關聯關係等。

當我們使用DB First時,設計好的數據庫,我們怎麼生成一些實體類、通用的代碼、控制器、服務層、Dto呢。今天我來給大家介紹一下FreeSql項目中的一些工具。當然,不使用此ORM的小夥伴也能使用此工具,因為他是通用。

FreeSql.Generator 命令行方式

通過幾行命令,就可實現生成項目中通用的代碼結構,不需要複製一段代碼后修改,加快開發速度,減少重複勞動,少用一根頭髮。

由於每個人的項目結構,代碼位置各不相同,對於ORM來說,不同的業務邏輯各不相同,所以該項目沒有相應的模板,相信使用過Razor的同學一定能實現自己的模板。

1-2年前,我和一個學長也寫過代碼生成器,這裏分享一下當時做項目時的一些模板,https://github.com/i542873057/SJNScaffolding/tree/master/SJNScaffolding.RazorPage/Templates,該項目是基於 . NET Core+Razor Page,由於已離職,所以沒有繼續維護,這些模板都和ABP相關,當時提取了一些通用的功能,單表操作,可以直接生成前後端功能,只需要在word中按統一的格式寫好數據字典的文檔,直接複製到系統,即可根據空格,定義類型等方式解析字段。

回到FreeSql.Generator 命令行

  • 對於此工具的使用可參考 https://github.com/dotnetcore/FreeSql/wiki/DbFirst
  • 源碼位置 https://github.com/dotnetcore/FreeSql/tree/master/Extensions/FreeSql.Generator
  • 前提是本地安裝了.net core 3.1 的sdk.

怎麼使用呢。

  1. 安裝 dotnet-tool 生成實體類
dotnet tool install -g FreeSql.Generator
  1. 新建目錄,在地址欄輸入 cmd 快速打開命令窗口,輸入命令:
FreeSql.Generator --help

我們可以看到

C:\Users\igeekfan\Desktop\code>FreeSql.Generator --help
        ____                   ____         __
       / __/  ____ ___  ___   / __/ ___ _  / /
      / _/   / __// -_)/ -_) _\ \  / _ `/ / /
     /_/    /_/   \__/ \__/ /___/  \_, / /_/
                                    /_/


  # Github # https://github.com/2881099/FreeSql v1.5.0

    使用 FreeSql 快速生成數據庫的實體類

    更新工具:dotnet tool update -g FreeSql.Generator


  # 快速開始 #

  > FreeSql.Generator -Razor 1 -NameOptions 0,0,0,0 -NameSpace MyProject -DB "MySql,Data Source=127.0.0.1;..."

     -Razor 1                  * 選擇模板:實體類+特性

     -Razor 2                  * 選擇模板:實體類+特性+導航屬性

     -Razor "d:\diy.cshtml"    * 自定義模板文件

     -NameOptions              * 總共4個布爾值,分別對應:
                               # 首字母大寫
                               # 首字母大寫,其他小寫
                               # 全部小寫
                               # 下劃線轉駝峰

     -NameSpace                * 命名空間

     -DB "MySql,Data Source=127.0.0.1;Port=3306;User ID=root;Password=root;Initial Catalog=數據庫;Charset=utf8;SslMode=none;Max pool size=2"

     -DB "SqlServer,Data Source=.;Integrated Security=True;Initial Catalog=數據庫;Pooling=true;Max Pool Size=2"

     -DB "PostgreSQL,Host=192.168.164.10;Port=5432;Username=postgres;Password=123456;Database=數據庫;Pooling=true;Maximum Pool Size=2"

     -DB "Oracle,user id=user1;password=123456;data source=//127.0.0.1:1521/XE;Pooling=true;Max Pool Size=2"

     -DB "Sqlite,Data Source=document.db"

     -DB "Dameng,server=127.0.0.1;port=5236;user id=2user;password=123456789;database=2user;poolsize=2"
                               Dameng 是國產達夢數據庫

     -Filter                   Table+View+StoreProcedure
                               默認生成:表+視圖+存儲過程
                               如果不想生成視圖和存儲過程 -Filter View+StoreProcedure

     -Match                    正則表達式,只生成匹配的表,如:dbo\.TB_.+

     -FileName                 文件名,默認:{name}.cs

     -Output                   保存路徑,默認為當前 shell 所在目錄
                               推薦在實體類目錄創建 gen.bat,雙擊它重新所有實體類
  • 更新命令行
dotnet tool update -g FreeSql.Generator
  1. 這裏lin-cms-dotnetcore這個項目來測試。

  • 數據庫表名是下劃線,字段也是下劃線方式。
  • -Razor 指定 第一個模板
  • -NameOptions 0,0,0,1 最後一個1,代表 下劃線轉駝峰,滿足C#命名規則
  • -NameSpace 指定了命名空間 LinCms.Core.Entities
  • -DB 就是數據庫的相關配置
  • mysql 本地地址 127.0.0.1 3306端口 用戶名 root 密碼123456 數據庫 lin-cms
  • -Match book 這樣就能只生成book,支持正則表達式,如 -Math lin_user 就會生成以lin_user開頭的表。如dbo.TB_.+,會生成以TB開頭的表。即只生成匹配的表
  1. 執行此命令。
FreeSql.Generator -Razor 1  -NameOptions 0,0,0,1 -NameSpace LinCms.Core.Entities -DB "MySql,Data Source=127.0.0.1;Port=3306;User ID=root;Password=123456;Initial Catalog=lincms;Charset=utf8;SslMode=none;Max pool size=2"

這時候代碼已經生成了

其中一個代碼 生成如下。這些類是partial ,熟悉C#的同學,應該知道,類的定義使用此關鍵字,我們能在不同的地方為該類擴展。以防止重新同步數據庫的結構時,丟失改動的字段。

namespace LinCms.Core.Entities {

	[JsonObject(MemberSerialization.OptIn), Table(Name = "book")]
	public partial class Book {

		/// <summary>
		/// 主鍵Id
		/// </summary>
		[JsonProperty, Column(Name = "id", IsPrimary = true, IsIdentity = true)]
		public long Id { get; set; }

		[JsonProperty, Column(Name = "author", DbType = "varchar(20)")]
		public string Author { get; set; } = string.Empty;

		[JsonProperty, Column(Name = "image", DbType = "varchar(50)")]
		public string Image { get; set; } = string.Empty;

        //更多xxx
	}

}

  • 最終效果圖如下

此時會生成二個文件
__重新生成.bat,下次重新點擊他就能重新生成實體類了。

FreeSql.Generator -Razor "__razor.cshtml.txt" -NameOptions 1,1,0,1 -NameSpace MyProject -DB "MySql,Data Source=127.0.0.1;Port=3306;User ID=root;Password=123456;Initial Catalog=lincms;Charset=utf8;SslMode=none;Max pool size=2" -FileName "{name}.cs"

上面的命令-Razor 指定了這個txt文件 __razor.cshtml.txt

我們可以定義自己的模板,以生成符合自已業務的的代碼,從而實現快速開發。

我們可以看下模板中的文件內容,他就是asp.net下的mvc 結構下的razor後端模板渲染,把這個.txt後綴去掉,就很明了了。對於asp.net mvc的razor,我們可以將控制器下方法的值替換掉cshtml中的值。這個過程是有一個類庫在幫我們實現的,叫RazorEngine,不過那個是.net framework下的實踐。.NET Framework 下的RazorEngine代碼生成介紹

@using FreeSql.DatabaseModel;@{
var gen = Model as RazorModel;

Func<string, string> GetAttributeString = attr => {
	if (string.IsNullOrEmpty(attr)) return null;
	return string.Concat(", ", attr.Trim('[', ']'));
};
Func<DbColumnInfo, string> GetDefaultValue = col => {
    if (col.CsType == typeof(string)) return " = string.Empty;";
    return "";
};
}
//xxx
namespace @gen.NameSpace {

@if (string.IsNullOrEmpty(gen.table.Comment) == false) {
	@:/// <summary>
	@:/// @gen.table.Comment.Replace("\r\n", "\n").Replace("\n", "\r\n		/// ")
	@:/// </summary>
}
	[JsonObject(MemberSerialization.OptIn)@GetAttributeString(gen.GetTableAttribute())]
	public partial class @gen.GetCsName(gen.FullTableName) {

	@foreach (var col in gen.columns) {

		if (string.IsNullOrEmpty(col.Coment) == false) {
		@:/// <summary>
		@:/// @col.Coment.Replace("\r\n", "\n").Replace("\n", "\r\n		/// ")
		@:/// </summary>
		}
		@:@("[JsonProperty" + GetAttributeString(gen.GetColumnAttribute(col)) + "]")
		@:public @gen.GetCsType(col) @gen.GetCsName(col.Name) { get; set; }@GetDefaultValue(col)
@:
	}
	}
@gen.GetMySqlEnumSetDefine()
}

RazorEngine.NetCore

到了.NET Core時代,我看了下FreeSql.Generator用的這個類庫RazorEngine.NetCore,實現動態操作cshtml,生成需要的文本。

Razor Engine是基於微軟Razor解析的模板引擎,它允許你使用Razor語法構建動態模板,你只需要使用Engine的靜態方法,Engine.Razor.RunCompile等。

創建一個控制台應用,然後安裝包。

Install-Package RazorEngine.NetCore
using RazorEngine;
using RazorEngine.Templating; // For extension methods.


string template = "Hello @Model.Name, welcome to RazorEngine!";
var result = Engine.Razor.RunCompile(template, "templateKey", null, new { Name = "World" });

Console.WriteLine(result);
  • 輸出如下內容
Hello World, welcome to RazorEngine!

此處使用的RunCompile方法是擴展方法,您需要引用RazorEngine.Templating命名空間。

The “templateKey” 保持唯一值,比如使用guid值。字符串,並且你可以根據此字符串key重新運行緩存的模板。

如果再次根據此key,可使用原本的模板。

var result = Engine.Razor.Run("templateKey", null, new { Name = "Max" });
  • 會輸出如下內容
Hello Max, welcome to RazorEngine!

上面中的RunCompile第三個參數,傳null,因為我們第四個參數使用的是匿名類,

根目錄創建一個HelloWord.cshtml,要選擇屬性,->如果較新則複製 內容,

Hello @Model.Name, welcome to RazorEngine!

控制台如下代碼。

string templateFilePath = "HelloWorld.cshtml";
var templateFile = File.ReadAllText(templateFilePath);
string templateFileResult = Engine.Razor.RunCompile(templateFile, Guid.NewGuid().ToString(), null, new
{
    Name = "World"
});

Console.WriteLine(templateFileResult);
  • 控制台輸出
Hello World, welcome to RazorEngine!
  • 使用強類型 CopyRightUserInfo.cs生成一個版權所有
using System;
namespace OvOv.Razor
{
    public class CopyRightUserInfo
    {
        public string UserName { get; set; }
        public string EmailAddress { get; set; }
        public DateTime CreateTime { get; set; }
        public string FileRemark { get; set; }
    }

}

根目錄創建一個CopyRightTemplate.cshtml,要選擇屬性,->如果較新則複製 內容,

@{
    var gen = Model as OvOv.Razor.CopyRightUserInfo;
}
//=============================================================
// 創建人:            @gen.UserName
// 創建時間:          @gen.CreateTime
// 郵箱:             @gen.EmailAddress
//==============================================================

控制台如下代碼。

string copyRightTemplatePath = "CopyRightTemplate.cshtml";
var copyRightTemplate = File.ReadAllText(copyRightTemplatePath);
string copyRightResult = Engine.Razor.RunCompile(copyRightTemplate, Guid.NewGuid().ToString(), typeof(CopyRightUserInfo), new CopyRightUserInfo
{
    CreateTime = DateTime.Now,
    EmailAddress = "710277267@qq.com",
    UserName = "IGeekFan"
});
Console.WriteLine(copyRightResult);

Console.ReadKey();
  • 控制台輸出
//=============================================================
// 創建人:            IGeekFan
// 創建時間:          2020/6/23 18:14:08
// 郵箱:             710277267@qq.com
//==============================================================

全放到控制台下,輸出如下結果。代碼生成器最重要的一點解決了,我們就能實現自己的代碼生成器,先構建自己的模板,實現輸入(命令行,WPF,WEB端及更多),輸出(生成文件)。

  • 以上源碼已放到示例代碼中 https://github.com/luoyunchong/dotnetcore-examples/blob/master/aspnetcore-freesql/OvOv.Razor/Program.cs

源碼解析

首先這是一個控制台應用,Main(string[] args)可接收多個參數。

  1. 處理無參數,–help
  2. 處理args數組,解析出所有的參數,如果沒有設置,則為默認值。(處理一些參數異常問題)最重要的是根據-Razor,選定對應的模板。
ArgsRazor=""//根據-Razor  1 不是2 還是模板的路徑,取出的模板文本值。
var razorId = Guid.NewGuid().ToString("N");
RazorEngine.Engine.Razor.Compile(ArgsRazor, razorId);
  1. 根據數據庫連接串,取出參數過濾后的表,視圖,存儲過程
  2. 循環數據庫的表等,
  • model為模板中需要的數據
  • razorId與上文的razorId相同,
  • sw為生成后的文本保存的值。
var sw = new StringWriter();
var model = new RazorModel(fsql, ArgsNameSpace, ArgsNameOptions, tables, table);
RazorEngine.Engine.Razor.Run(razorId, sw, null, model);
  1. 將sw字符串保存生成類.cs文件(根據參數配置生成文件名)
  2. 另外生成一個__重新生成.bat,__razor.cshtml.txt,方便後續用戶重新生成實體類。

FreeSql.Tools

這是 FreeSql 衍生出來的輔助工具包,內含生成器等功能;作者:mypeng1985
因為這個不兼容mac,linux,所以作者建議使用dotnet-tool 命令行工具生成實體類,從而支持MAC/Linux系統。對於不是使用FreeSql的開發者,也能使用此工具,你只需要修改對應的模板即可。

使用方式:不多介紹。

  • https://github.com/2881099/Freesql.tools
  • 分為WPF ,WinForm + DSkin 版本(套網頁)
  • 看了下代碼,底層生成代碼邏輯也是用的RazorEngine .NET Framework 下的RazorEngine代碼生成介紹

FreeSql官方群 4336577

預覽圖

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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