標籤: 貨運

  • 新技術如何驅動混合動力汽車市場化-第三屆混合動力汽車技術峰會

    新技術如何驅動混合動力汽車市場化-第三屆混合動力汽車技術峰會

    在中國日益嚴重的環境污染壓力和“霧霾”陰影籠罩下,混合動力及純電動汽車的發展形成了勢在必行的趨勢。即使《節能與新能源汽車產業發展規劃》以及四部委聯合發佈的《關於繼續開展新能源汽車推廣應用工作的通知》中明確了純電動以及插電式混合動力汽車的補貼政策,第二批新能源汽車試點運行城市也出臺在即,但新能源汽車產業的前景仍然未能明朗化,標準不統一,基礎建設不健全,電池技術受到局限的電動車以及補貼政策不明確的混合動力汽車都面臨各自發展的瓶頸。在全球並未形成一種成熟應用模式的狀況下,中國應該走出怎樣的一條有自我特色的路線圖。

    目前,全球能源和環境系統面臨巨大的挑戰,汽車作為石油消耗和二氧化碳排放的大戶,需要進行革命性的變革。目前全球新能源汽車發展已經形成了共識,從長期來看,包括純電動、燃料電池技術在內的純電驅動將是新能源汽車的主要技術方向,在短期內,油電混合、插電式混合動力將是重要的過渡路線。目前來看,全球新能源汽車的發展還面臨著一些共同的難題,例如關鍵技術的突破、汽車工業的轉型、基礎設施的建設以及消費者的接受度等。

    為了提升中國混合動力汽車技術發展,幫助整車廠獲得一手的技術和市場訊息,第三屆混合動力汽車技術峰會茲定於2014年6月26日至27日,在虹橋喜來登上海太平洋大飯店舉行,主題為“技術驅動混合動力汽車市場化”。

    市場方面,中國汽車技術研究中心的高級工程師張銅柱將在此次會議上對電動汽車分標委工作及標準最新制修訂情況做相關發言,介紹工作組成立背景、工作組籌備情況、成員構成、工作內容及工作計畫等。中國化學與物理電源行業協會副秘書長劉彥龍將會就新能源汽車用動力電池市場需求發表演講,深度分析目前國內外鋰電池的發展處於一個什麼狀態,決定鋰電池未來應用之路的關鍵是什麼。中國機電產品進出口商會汽車分會秘書長孟凡一將會對中國汽車進出口情況進行回顧及展望,並分析在全球經濟活動較弱以及國際貿易保護主義抬頭的態勢下,2014年國際汽車市場的總體形勢和客觀上嚴重影響我國汽車出口行業的諸多不確定因素。

    技術方面,精進電動創始人兼CTO蔡蔚將會對汽車電動化的動力總成與動力總成的電機系統做相關介紹,講述他觀點中混合動力汽車長期存在的基礎,動力總成的拓撲結構,混合動力變速箱的分析與比較,以及動力總成對電機系統要求與電機系同的開發及產業化。上海電驅動股份有限公司技術中心主任徐延東將結合領先的Demo project解析混合動力電機控制器集成裝置及其技術解決方案。其他技術題目還包括底盤,控制器軟硬體,逆變器,插電式混合動力,增程式混合動力汽車以及超級電容等領域的領先技術。

    歷屆混合動力汽車技術峰會得到了發改委能源研究所、中國汽車工業協會專家委員會、上海汽車工程學會、中國汽車技術研究中心、中國電源產業創新聯盟、上海交通大學汽車工程研究院、汽車安全與節能國家重點實驗室、中國北方車輛研究所動力電池實驗室等組織的大力支持。

    更多詳情請聯繫第三屆混合動力汽車技術峰會組委會
    朱小姐電話:+86 21 61808505*212
    郵箱:

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

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

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

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

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

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

    ※回頭車貨運收費標準

  • 中聚電池1.9億入股雲南電動車公司50%股權

    中聚電池宣佈收購一家在雲南昆明產銷電動汽車公司的五成權益。中聚電池以1.9億元的總代價收購GIANT INDUSTRY全部股權,將按每股0.5元發行3.8億股新股份支付。代價股份佔經擴大後已發行股本2.19%。

    GIANT INDUSTRY擁有雲南美的客車製造50%的間接權益。雲南美主要從事製造、銷售、組裝及維修客車、電動汽車及總成,以及零件和部件。收購事項將為集團提供即時平台,以從事電動汽車製造。

    另外,中聚電池還宣佈,建議待該公司股東於股東特別大會上批准及經百慕達公司註冊處處長批准,將該公司英文名稱由「SinopOLy Battery Limited」更改為「FDG Electric Vehicles Limited」,而中文名稱則改為“五龍電動車(集團)”。

    董事會相信,建議更改名稱將為該公司提供新企業定位,符合該公司加強重點於電動汽車相關業務的新品牌,今後業務將包括但不限於設計、生產及銷售電動汽車;以及鋰離子電池業務的研發、生產及銷售和電動汽車租賃業務等。

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

  • 雷諾計畫2016年啟動在華生産 純電動汽車也在考慮之列

    據日經新聞今(23)日報導,法國車企雷諾(Renault)將從2016年開始啟動在華生産,並與組成聯盟的日産汽車(Nissan)在採購和生産等廣泛領域展開合作,通過共享日産的採購網等舉措來削減成本,以提高價格競爭力。值得注意的是,雷諾還將考慮在中國生産純電動汽車(EV)。

    在正在舉行的北京國際車展上,雷諾亞太區董事長吉爾斯•諾曼德(Gilles Normand)表示,「(在中國)起步較晚的雷諾要想脫穎而出,如何推進本地化生産非常重要」。

    雷諾將積極利用日産的採購網等,將零部件的本地採購率提高至85%左右。德國福斯(VW)和美國通用汽車(GM)等主要企業已經進駐中國,而本地採購率被認為達到80~90%。

    雷諾去年12月從中國政府獲得了工廠建設許可,距離其計劃發佈已有9年。諾曼德表示,亞太地區汽車銷量佔世界的45%,而雷諾全球份額中僅有約10%來自亞太,「希望提高存在感」。

    雷諾在中國已經與東風汽車建立了合資公司。工廠位於湖北省武漢市,將於2016年開始生産多功能運動車(SUV)等,年産能在15萬輛左右。該公司首席執行官(CEO)卡洛斯•戈恩表示,將來希望擴大至年産60~70萬輛。

    中國目前面臨著嚴重的大氣污染問題。諾曼德表示,「在中國無法避開環境問題」,針對純電動車的生産,「公司內部正在進行各方面討論」,承認正在考慮純電動車的本地化生産。

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

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

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

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

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

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

    ※回頭車貨運收費標準

  • 特斯拉Q1營收同比增27% 中國市場未來備受關注

    美國電動汽車廠商特斯拉今(8)日發布Q1財報,不按美國通用會計準則計算(Non-GAAP),特斯拉第一季度營收達7.13億美元,同比增長27%;凈利潤1700萬美元,同比增長10%

    在與財報一同發布的季度股東信中,特斯拉CEO伊隆馬斯克(Elon Musk)指出中國市場的重要性,並將其視為未來幾年間的最大市場之一。上個月,特斯拉向中國車主交付了首批Model S轎車,並借勢掀起一場“特斯拉熱”。

    股東信中還談到上海政府部門對於特斯拉的支持。一項舉措是,上海車主購買Model S可以免費上牌,節約了7萬元以上的牌照費用;另一項則是在超級充電站方面大開綠燈。這也正好是特斯拉目前在中國面臨的兩大痛點:上牌難和充電難。

    進口純電動車獲免費牌照

    上海承諾今年撥出3000張免費牌照,供進口純電動車使用,這是此類政策首次向國外品牌開放。北京也計劃效仿上海,今年拿出500張牌照,專門提供給購買國外品牌純電動車的消費者。

    充電問題依然棘手

    馬斯克訪華期間接受媒體採訪時曾表示,希望建設更多采用太陽能的超級充電站。但截至目前,只有上海的一座超級充電站已經投入使用,而北京的兩個站點建設完畢,尚未對外開放。

    近期,特斯拉在美國新澤西州開設了第100家超級充電站。今年,該公司還計劃在全球開設超過200家超級充電站,美國占大多數,其余則位於加拿大、歐洲和中國。

    電池供給不足限制產能

    目前,特斯拉位於美國加州佛蒙特(Fremont)的工廠每周可生產約700輛Model S,比2013年底提升了15%。特斯拉計劃在今年底將這一數字提升至1000輛。與動輒以萬為量單位的通用、福特等傳統汽車廠商相比,特斯拉的量很低,一方面是由於只有一座工廠,另一方面則是受到了電池供給不足的限制。

    Model S採用的鎳鈷鋁電池由松下代工。特斯拉正在推進一個名為“巨型工廠”(Gigafactory)的計劃,並與松下及其他廠商進行談判,希望徹底解決電池能不足的問題。

    然而,根據股東信披露的信息,“巨型工廠”目前仍處於選址階段,直到2017年才能生產出首批電池,2020年才能達到最大產能,即每年生產50GWh的電池包和35GWh的電池單元。特斯拉認為,到那時“將不會面臨任何供貨限制”。

    此外,特斯拉的SUV車型Model X將於今年第四季度推出原型車。這是特斯拉規劃的“三步走”計劃的第二步。根據此前披露的檔案,特斯拉將在2017年左右推出第三代車型——一款廉價的家用轎車,售價預計在3萬至3.5萬美元之間。

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

  • 併發編程 —— 線程池

    併發編程 —— 線程池

    概述

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

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

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

    線程池

    雖然在 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網頁設計為架站首選

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

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

    ※回頭車貨運收費標準

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

    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.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

    預覽圖

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

  • 布袋蓮洗刷惡名 成為肯亞再生能源金礦

    摘錄自2019年9月30日聯合報報導

    布袋蓮為原產於南美洲的水生植物,20世紀初首次出現於非洲。自從布袋蓮開始在1900年從南非開普敦開始向外蔓延,並開始堵塞主要水壩和河流後,科學家稱它為「世界最嚴重的水生雜草」。布袋蓮常因阻塞重要水道且難以根除而遭到敵視,但它在非洲肯亞卻有助提供更乾淨的能源,進而保護居民健康。

    英國衛報報導,在肯亞基蘇木郡(Kisumu)維多利亞湖的Winam灣岸邊附近,大量布袋蓮腐爛在湖邊,但那些逐漸腐壞的蠟質葉片卻是可再生能源的金礦。事實證明,布袋蓮不僅繁殖能力出眾,它的葉子還含有高比例的碳和氮。早期研究預測,僅約4公斤的乾燥布袋蓮,就能滿足一個大家庭的日常能源需求。

    肯亞學者證實,布袋蓮能將有機物轉化為甲烷(沼氣的主要成分)、二氧化碳及水。因為沼氣容易燃燒,一般天然氣能做的事情它都能辦到,像是煮飯、加熱或作為動力來源。Dunga村2018年獲贈兩個沼氣池,能為村莊60%人口提供服務,現在肯亞約有50個沼氣池。

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

    【其他文章推薦】

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

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

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

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

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

    ※回頭車貨運收費標準

  • 世界最毒蕈類 澳洲首度發現「火焰茸」

    摘錄自2019年10月3日自由時報報導

    澳洲詹姆士庫克大學(James Cook University)3日宣布,當地攝影師在凱恩斯(Cairns)的郊區拍到「火焰茸」,經科學家確定是一種又被稱為毒火珊瑚(Poison Fire Coral)的真菌。

    外媒稱,火焰茸的原生棲地在日本和南韓的山區,這種鮮紅色的真菌經常被誤認為是可食用的紅珊瑚菌(Clavulinopsis miyabeana)、冬蟲夏草,誤食而中毒甚至死亡。

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

  • NASA最新公布 洞察號錄下「火星震」低沉鳴叫

    摘錄自2019年10月3日自由時報報導

    美國太空總署(NASA)探測器「洞察號」(InSight)去年底飛抵火星表面至今,,分別從今年5月22日發生的「Sol 173震盪」及7月25日的「Sol 235震盪」錄下,這兩次火星震若換算成常用單位,分別為芮氏規模3.7與3.3。

    「洞察號」火星任務最主要的目標是探測火星內部結構,科學家希望藉此理解,所謂「類地行星」(terrestrial planet,又稱為岩質行星)是如何形成的。

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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