超乾貨!為了讓你徹底弄懂MySQL事務日誌,我通宵肝出了這份圖解!_如何寫文案

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

什麼是銷售文案服務?A就是幫你撰寫適合的廣告文案。當您需要販售商品、宣傳活動、建立個人品牌,撰寫廣告文案都是必須的工作。

還記得剛上研究生的時候,導師常掛在嘴邊的一句話,“科研的基礎不過就是數據而已。”如今看來,無論是人文社科,還是自然科學,或許都可在一定程度上看作是數據的科學。

倘若剝開研究領域的外衣,將人的操作抽象出來,那麼科研的過程大概就是根據數據流動探索其中的未知信息吧。當然科學研究的範疇涵蓋甚廣,也不是一兩句話能夠拎得清的。不過從這個角度上的闡述,也只是為了引出數據的重要性。

在當今社會,充斥着大量的數據。從眾多APP上的賬戶資料到銀行信用體系等個人檔案,都離不開對大量數據的組織、存儲和管理。而這,便是數據庫存在的目的和價值。

目前數據庫的類型主要分為兩種,一種是關係型數據庫,另一種是非關係型數據庫(NoSQL)。而我們今天的主角MySQL就是關係型數據庫中的一種。

1 關係型數據庫與NoSQL

關係型數據庫,顧名思義,是指存儲的數據之間具有關係。這種所謂的關係通常用二維表格中的行列來表示,即一個二維表的邏輯結構能夠反映表中數據的存儲關係。

概念總是拗口難懂的。那麼簡單來說,關係型數據庫的存儲就是按照表格進行的。數據的存儲實際上就是對一個或者多個表格的存儲。通過對這些表格進行分類、合併、連接或者選取等運算來實現對數據庫的管理。常見的關係型數據庫有MySQL、Oracle、DB2和SqlServer等。

非關係型數據庫(NoSQL)是相對於關係型數據庫的一種泛指,它的特點是去掉了關係型數據庫中的關係特性,從而可獲得更好的擴展性。NoSQL並沒有嚴格的存儲方式,但採用不同的存儲結構都是為了獲得更高的性能和更高的併發。NoSQL根據存儲方式可分為四大類,鍵值存儲數據庫、列存儲數據庫、文檔型數據庫和圖形數據庫。這四種數據的存儲原理不盡相同,因而在應用場景上也有些許的差異。一般常用的有作為數據緩存的redis和分佈式系統的HBase。目前常見的數據庫排名可見網站:

https://db-engines.com/en/ranking

關係型數據庫與非關係型數據庫本質上的區別就在於存儲的數據是否具有一定的邏輯關係,由此產生的兩類數據庫看的性能和優劣勢上也有一定的區別。二者對比可見下圖。

2 MySQL簡介

介紹

在關係型數據庫中,MySQL可以說是其中的王者。它是目前最流行的數據庫之一,由瑞典 MySQL AB 公司開發,目前屬於 Oracle 公司。MySQL數據庫具有以下幾個方面的優勢:

  • 體積小、速度快;
  • 代碼開源,採用了 GPL 協議,可以修改源碼來開發自己的 MySQL 系統;
  • 支持大型的數據庫,可以處理擁有上千萬條記錄的大型數據庫;
  • 使用標準的 SQL 數據語言形式,並採用優化的 SQL 查詢算法,有效地提高查詢速度;
  • 使用 C 和 C++ 編寫,並使用多種編譯器進行測試,保證源代碼的可移植性;
  • 可運行在多個系統上,並且支持多種語言;
  • 核心程序採用完全的多線程編程,可以靈活地為用戶提供服務,充分利用CPU資源。

邏輯架構

MySQL的邏輯架構可分為四層,包括連接層、服務層、引擎層和存儲層,各層的接口交互及作用如下圖所示。需要注意的是,由於本文將主要講解事務的實現原理,因此下文針對的都是InnoDB引擎下的情況。

連接層:負責處理客戶端的連接以及權限的認證。

服務層:定義有許多不同的模塊,包括權限判斷,SQL接口,SQL解析,SQL分析優化, 緩存查詢的處理以及部分內置函數執行等。MySQL的查詢語句在服務層內進行解析、優化、緩存以及內置函數的實現和存儲。

引擎層:負責MySQL中數據的存儲和提取。MySQL中的服務器層不管理事務,事務是由存儲引擎實現的。其中使用最為廣泛的存儲引擎為InnoDB,其它的引擎都不支持事務。

存儲層:負責將數據存儲與設備的文件系統中。

3 MySQL事務

事務是MySQL區別於NoSQL的重要特徵,是保證關係型數據庫數據一致性的關鍵技術。事務可看作是對數據庫操作的基本執行單元,可能包含一個或者多個SQL語句。這些語句在執行時,要麼都執行,要麼都不執行。

事務的執行主要包括兩個操作,提交和回滾。

提交:commit,將事務執行結果寫入數據庫。

回滾:rollback,回滾所有已經執行的語句,返回修改之前的數據。

MySQL事務包含四個特性,號稱ACID四大天王。

原子性(Atomicity) :語句要麼全執行,要麼全不執行,是事務最核心的特性,事務本身就是以原子性來定義的;實現主要基於undo log日誌實現的。

持久性(Durability :保證事務提交后不會因為宕機等原因導致數據丟失;實現主要基於redo log日誌。

隔離性(Isolation) :保證事務執行盡可能不受其他事務影響;InnoDB默認的隔離級別是RR,RR的實現主要基於鎖機制、數據的隱藏列、undo log和類next-key lock機制。

一致性(Consistency) :事務追求的最終目標,一致性的實現既需要數據庫層面的保障,也需要應用層面的保障。

原子性

事務的原子性就如原子操作一般,表示事務不可再分,其中的操作要麼都做,要麼都不做;如果事務中一個SQL語句執行失敗,則已執行的語句也必須回滾,數據庫退回到事務前的狀態。只有0和1,沒有其它值。

事務的原子性表明事務就是一個整體,當事務無法成功執行的時候,需要將事務中已經執行過的語句全部回滾,使得數據庫回歸到最初未開始事務的狀態。

事務的原子性就是通過undo log日誌進行實現的。當事務需要進行回滾時,InnoDB引擎就會調用undo log日誌進行SQL語句的撤銷,實現數據的回滾。

持久性

事務的持久性是指當事務提交之後,數據庫的改變就應該是永久性的,而不是暫時的。這也就是說,當事務提交之後,任何其它操作甚至是系統的宕機故障都不會對原來事務的執行結果產生影響。

事務的持久性是通過InnoDB存儲引擎中的redo log日誌來實現的,具體實現思路見下文。

隔離性

原子性和持久性是單個事務本身層面的性質,而隔離性是指事務之間應該保持的關係。隔離性要求不同事務之間的影響是互不干擾的,一個事務的操作與其它事務是相互隔離的。

由於事務可能並不只包含一條SQL語句,所以在事務的執行期間很有可能會有其它事務開始執行。因此多事務的併發性就要求事務之間的操作是相互隔離的。這一點跟多線程之間數據同步的概念有些類似。

鎖機制

事務之間的隔離,是通過鎖機制實現的。當一個事務需要對數據庫中的某行數據進行修改時,需要先給數據加鎖;加了鎖的數據,其它事務是不運行操作的,只能等待當前事務提交或回滾將鎖釋放。

鎖機制並不是一個陌生的概念,在許多場景中都會利用到不同實現的鎖對數據進行保護和同步。而在MySQL中,根據不同的劃分標準,還可將鎖分為不同的種類。

按照粒度劃分:行鎖、表鎖、頁鎖

按照使用方式劃分:共享鎖、排它鎖

按照思想劃分:悲觀鎖、樂觀鎖

鎖機制的知識點很多,由於篇幅不好全部展開講。這裏對按照粒度劃分的鎖進行簡單介紹。

粒度:指數據倉庫的數據單位中保存數據的細化或綜合程度的級別。細化程度越高,粒度級就越小;相反,細化程度越低,粒度級就越大。

MySQL按照鎖的粒度劃分可以分為行鎖、表鎖和頁鎖。

行鎖:粒度最小的鎖,表示只針對當前操作的行進行加鎖;

表鎖:粒度最大的鎖,表示當前的操作對整張表加鎖;

頁鎖:粒度介於行級鎖和表級鎖中間的一種鎖,表示對頁進行加鎖。

這三種鎖是在不同層次上對數據進行鎖定,由於粒度的不同,其帶來的好處和劣勢也不一而同。

表鎖在操作數據時會鎖定整張表,因而併發性能較差;

行鎖則只鎖定需要操作的數據,併發性能好。但是由於加鎖本身需要消耗資源(獲得鎖、檢查鎖、釋放鎖等都需要消耗資源),因此在鎖定數據較多情況下使用表鎖可以節省大量資源。

MySQL中不同的存儲引擎能夠支持的鎖也是不一樣的。MyIsam只支持表鎖,而InnoDB同時支持表鎖和行鎖,且出於性能考慮,絕大多數情況下使用的都是行鎖。

併發讀寫問題

在併發情況下,MySQL的同時讀寫可能會導致三類問題,臟讀、不可重複度和幻讀。

(1)臟讀:當前事務中讀到其他事務未提交的數據,也就是臟數據。

以上圖為例,事務A在讀取文章的閱讀量時,讀取到了事務B為提交的數據。如果事務B最後沒有順利提交,導致事務回滾,那麼實際上閱讀量並沒有修改成功,而事務A卻是讀到的修改后的值,顯然不合情理。

(2)不可重複讀:在事務A中先後兩次讀取同一個數據,但是兩次讀取的結果不一樣。臟讀與不可重複讀的區別在於:前者讀到的是其他事務未提交的數據,後者讀到的是其他事務已提交的數據。

以上圖為例,事務A在先後讀取文章閱讀量的數據時,結果卻不一樣。說明事務A在執行的過程中,閱讀量的值被其它事務給修改了。這樣使得數據的查詢結果不再可靠,同樣也不合實際。

(3)幻讀:在事務A中按照某個條件先後兩次查詢數據庫,兩次查詢結果的行數不同,這種現象稱為幻讀。不可重複讀與幻讀的區別可以通俗的理解為:前者是數據變了,後者是數據的行數變了。

以上圖為例,當對0<閱讀量<100的文章進行查詢時,先查到了一個結果,後來查詢到了兩個結果。這表明同一個事務的查詢結果數不一,行數不一致。這樣的問題使得在根據某些條件對數據篩選的時候,前後篩選結果不具有可靠性。

隔離級別

根據上面這三種問題,產生了四種隔離級別,表明數據庫不同程度的隔離性質。

在實際的數據庫設計中,隔離級別越高,導致數據庫的併發效率會越低;而隔離級別太低,又會導致數據庫在讀寫過程中會遇到各種亂七八糟的問題。

因此在大多數數據庫系統中,默認的隔離級別時讀已提交(如Oracle)或者可重複讀RR(MySQL的InnoDB引擎)。

MVCC

又是一個難嚼的大塊頭。MVCC就是用來實現上面的第三個隔離級別,可重複讀RR。

MVCC:Multi-Version Concurrency Control,即多版本的併發控制協議。

MVCC的特點就是在同一時刻,不同事務可以讀取到不同版本的數據,從而可以解決臟讀和不可重複讀的問題。

MVCC實際上就是通過數據的隱藏列和回滾日誌(undo log),實現多個版本數據的共存。這樣的好處是,使用MVCC進行讀數據的時候,不用加鎖,從而避免了同時讀寫的衝突。

在實現MVCC時,每一行的數據中會額外保存幾個隱藏的列,比如當前行創建時的版本號和刪除時間和指向undo log的回滾指針。這裏的版本號並不是實際的時間值,而是系統版本號。每開始新的事務,系統版本號都會自動遞增。事務開始時的系統版本號會作為事務的版本號,用來和查詢每行記錄的版本號進行比較。

每個事務又有自己的版本號,這樣事務內執行數據操作時,就通過版本號的比較來達到數據版本控制的目的。

另外,InnoDB實現的隔離級別RR時可以避免幻讀現象的,這是通過next-key lock機制實現的。這裏簡單講講吧。

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

next-key lock實際上就是行鎖的一種,只不過它不只是會鎖住當前行記錄的本身,還會鎖定一個範圍。比如上面幻讀的例子,開始查詢0<閱讀量<100的文章時,只查到了一個結果。next-key lock會將查詢出的這一行進行鎖定,同時還會對0<閱讀量<100這個範圍進行加鎖,這實際上是一種間隙鎖。間隙鎖能夠防止其他事務在這個間隙修改或者插入記錄。這樣一來,就保證了在0<閱讀量<100這個間隙中,只存在原來的一行數據,從而避免了幻讀。

間隙鎖:封鎖索引記錄中的間隔

雖然InnoDB使用next-key lock能夠避免幻讀問題,但卻並不是真正的可串行化隔離。再來看一個例子吧。

首先提一個問題,在T6事務A提交事務之後,猜一猜文章A和文章B的閱讀量為多少?

答案是,文章AB的閱讀量都被修改成了10000。這代表着事務B的提交實際上對事務A的執行產生了影響,表明兩個事務之間並不是完全隔離的。雖然能夠避免幻讀現象,但是卻沒有達到可串行化的級別。

這還說明,避免臟讀、不可重複讀和幻讀,是達到可串行化的隔離級別的必要不充分條件。可串行化是都能夠避免臟讀、不可重複讀和幻讀,但是避免臟讀、不可重複讀和幻讀卻不一定達到了可串行化。

一致性

一致性是指事務執行結束后,數據庫的完整性約束沒有被破壞,事務執行的前後都是合法的數據狀態。

一致性是事務追求的最終目標:前面提到的原子性、持久性和隔離性,都是為了保證數據庫狀態的一致性。

這就不多說了吧。你細品。

4 MySQL日誌系統

了解完MySQL的基本架構,大體上能夠對MySQL的執行流程有了比較清晰的認知。接下來我將在講述MySQL事務之前,先為大家介紹以下日誌系統,以方便之後更好的理解事務的特性和實現。

MySQL日誌系統是數據庫的重要組件,用於記錄數據庫的更新和修改。若數據庫發生故障,可通過不同日誌記錄恢複數據庫的原來數據。因此實際上日誌系統直接決定着MySQL運行的魯棒性和穩健性。

MySQL的日誌有很多種,如二進制日誌(binlog)、錯誤日誌、查詢日誌、慢查詢日誌等,此外InnoDB存儲引擎還提供了兩種日誌:redo log(重做日誌)和undo log(回滾日誌)。這裏將重點針對InnoDB引擎,對重做日誌、回滾日誌和二進制日誌這三種進行分析。

重做日誌(redo log)

重做日誌(redo log)是InnoDB引擎層的日誌,用來記錄事務操作引起數據的變化,記錄的是數據頁的物理修改。

重做日記的作用其實很好理解,我打個比方。數據庫中數據的修改就好比你寫的論文,萬一哪天論文丟了怎麼呢?以防這種不幸的發生,我們可以在寫論文的時候,每一次修改都拿個小本本記錄一下,記錄什麼時間對某一頁進行了怎麼樣的修改。這就是重做日誌。

InnoDB引擎對數據的更新,是先將更新記錄寫入redo log日誌,然後會在系統空閑的時候或者是按照設定的更新策略再將日誌中的內容更新到磁盤之中。這就是所謂的預寫式技術(Write Ahead logging)。這種技術可以大大減少IO操作的頻率,提升數據刷新的效率。

臟數據刷盤

值得注意的是,redo log日誌的大小是固定的,為了能夠持續不斷的對更新記錄進行寫入,在redo log日誌中設置了兩個標誌位置,checkpointwrite_pos,分別表示記錄擦除的位置和記錄寫入的位置。redo log日誌的數據寫入示意圖可見下圖。

write_pos標誌到了日誌結尾時,會從結尾跳至日誌頭部進行重新循環寫入。所以redo log的邏輯結構並不是線性的,而是可看作一個圓周運動。write_poscheckpoint中間的空間可用於寫入新數據,寫入和擦除都是往後推移,循環往複的。

write_pos追上checkpoint時,表示redo log日誌已經寫滿。這時不能繼續執行新的數據庫更新語句,需要停下來先刪除一些記錄,執行checkpoint規則騰出可寫空間。

checkpoint規則:checkpoint觸發后,將buffer中臟數據頁和臟日誌頁都刷到磁盤。

臟數據:指內存中未刷到磁盤的數據。

redo log中最重要的概念就是緩衝池buffer pool,這是在內存中分配的一個區域,包含了磁盤中部分數據頁的映射,作為訪問數據庫的緩衝。

當請求讀取數據時,會先判斷是否在緩衝池命中,如果未命中才會在磁盤上進行檢索後放入緩衝池;

當請求寫入數據時,會先寫入緩衝池,緩衝池中修改的數據會定期刷新到磁盤中。這一過程也被稱之為刷臟

因此,當數據修改時,除了修改buffer pool中的數據,還會在redo log中記錄這次操作;當事務提交時,會根據redo log的記錄對數據進行刷盤。如果MySQL宕機,重啟時可以讀取redo log中的數據,對數據庫進行恢復,從而保證了事務的持久性,使得數據庫獲得crash-safe能力。

臟日誌刷盤

除了上面提到的對於臟數據的刷盤,實際上redo log日誌在記錄時,為了保證日誌文件的持久化,也需要經歷將日誌記錄從內存寫入到磁盤的過程。redo log日誌可分為兩個部分,一是存在易失性內存中的緩存日誌redo log buff,二是保存在磁盤上的redo log日誌文件redo log file

為了確保每次記錄都能夠寫入到磁盤中的日誌中,每次將redo log buffer中的日誌寫入redo log file的過程中都會調用一次操作系統的fsync操作。

fsync函數:包含在UNIX系統頭文件#include <unistd.h>中,用於同步內存中所有已修改的文件數據到儲存設備。

在寫入的過程中,還需要經過操作系統內核空間的os buffer。redo log日誌的寫入過程可見下圖。

二進制日誌(binlog)

二進制日誌binlog是服務層的日誌,還被稱為歸檔日誌。binlog主要記錄數據庫的變化情況,內容包括數據庫所有的更新操作。所有涉及數據變動的操作,都要記錄進二進制日誌中。因此有了binlog可以很方便的對數據進行複製和備份,因而也常用作主從庫的同步。

這裏binlog所存儲的內容看起來似乎與redo log很相似,但是其實不然。redo log是一種物理日誌,記錄的是實際上對某個數據進行了怎麼樣的修改;而binlog是邏輯日誌,記錄的是SQL語句的原始邏輯,比如”給ID=2這一行的a字段加1 “。binlog日誌中的內容是二進制的,根據日記格式參數的不同,可能基於SQL語句、基於數據本身或者二者的混合。一般常用記錄的都是SQL語句。

這裏的物理和邏輯的概念,我的個人理解是:

物理的日誌可看作是實際數據庫中數據頁上的變化信息,只看重結果,而不在乎是通過“何種途徑”導致了這種結果;

邏輯的日誌可看作是通過了某一種方法或者操作手段導致數據發生了變化,存儲的是邏輯性的操作。

同時,redo log是基於crash recovery,保證MySQL宕機后的數據恢復;而binlog是基於point-in-time recovery,保證服務器可以基於時間點對數據進行恢復,或者對數據進行備份。

事實上最開始MySQL是沒有redo log日誌的。因為起先MySQL是沒有InnoDB引擎的,自帶的引擎是MyISAM。binlog是服務層的日誌,因此所有引擎都能夠使用。但是光靠binlog日誌只能提供歸檔的作用,無法提供crash-safe能力,所以InnoDB引擎就採用了學自於Oracle的技術,也就是redo log,這才擁有了crash-safe能力。這裏對redo log日誌和binlog日誌的特點分別進行了對比:

在MySQL執行更新語句時,都會涉及到redo log日誌和binlog日誌的讀寫。一條更新語句的執行過程如下:

從上圖可以看出,MySQL在執行更新語句的時候,在服務層進行語句的解析和執行,在引擎層進行數據的提取和存儲;同時在服務層對binlog進行寫入,在InnoDB內進行redo log的寫入。

不僅如此,在對redo log寫入時有兩個階段的提交,一是binlog寫入之前prepare狀態的寫入,二是binlog寫入之後commit狀態的寫入。

之所以要安排這麼一個兩階段提交,自然是有它的道理的。現在我們可以假設不採用兩階段提交的方式,而是採用“單階段”進行提交,即要麼先寫入redo log,后寫入binlog;要麼先寫入binlog,后寫入redo log。這兩種方式的提交都會導致原先數據庫的狀態和被恢復后的數據庫的狀態不一致。

先寫入redo log,后寫入binlog:

在寫完redo log之後,數據此時具有crash-safe能力,因此系統崩潰,數據會恢復成事務開始之前的狀態。但是,若在redo log寫完時候,binlog寫入之前,系統發生了宕機。此時binlog沒有對上面的更新語句進行保存,導致當使用binlog進行數據庫的備份或者恢復時,就少了上述的更新語句。從而使得id=2這一行的數據沒有被更新。

先寫入binlog,后寫入redo log:

寫完binlog之後,所有的語句都被保存,所以通過binlog複製或恢復出來的數據庫中id=2這一行的數據會被更新為a=1。但是如果在redo log寫入之前,系統崩潰,那麼redo log中記錄的這個事務會無效,導致實際數據庫中id=2這一行的數據並沒有更新。

由此可見,兩階段的提交就是為了避免上述的問題,使得binlog和redo log中保存的信息是一致的。

回滾日誌(undo log)

回滾日誌同樣也是InnoDB引擎提供的日誌,顧名思義,回滾日誌的作用就是對數據進行回滾。當事務對數據庫進行修改,InnoDB引擎不僅會記錄redo log,還會生成對應的undo log日誌;如果事務執行失敗或調用了rollback,導致事務需要回滾,就可以利用undo log中的信息將數據回滾到修改之前的樣子。

但是undo log不redo log不一樣,它屬於邏輯日誌。它對SQL語句執行相關的信息進行記錄。當發生回滾時,InnoDB引擎會根據undo log日誌中的記錄做與之前相反的工作。比如對於每個數據插入操作(insert),回滾時會執行數據刪除操作(delete);對於每個數據刪除操作(delete),回滾時會執行數據插入操作(insert);對於每個數據更新操作(update),回滾時會執行一個相反的數據更新操作(update),把數據改回去。undo log由兩個作用,一是提供回滾,二是實現MVCC。

5 主從複製

主從複製的概念很簡單,就是從原來的數據庫複製一個完全一樣的數據庫,原來的數據庫稱作主數據庫,複製的數據庫稱為從數據庫。從數據庫會與主數據庫進行數據同步,保持二者的數據一致性。

主從複製的原理實際上就是通過bin log日誌實現的。bin log日誌中保存了數據庫中所有SQL語句,通過對bin log日誌中SQL的複製,然後再進行語句的執行即可實現從數據庫與主數據庫的同步。

主從複製的過程可見下圖。主從複製的過程主要是靠三個線程進行的,一個運行在主服務器中的發送線程,用於發送binlog日誌到從服務器。兩外兩個運行在從服務器上的I/O線程和SQL線程。I/O線程用於讀取主服務器發送過來的binlog日誌內容,並拷貝到本地的中繼日誌中。SQL線程用於讀取中繼日誌中關於數據更新的SQL語句並執行,從而實現主從庫的數據一致。

之所以需要實現主從複製,實際上是由實際應用場景所決定的。主從複製能夠帶來的好處有:

1 通過複製實現數據的異地備份,當主數據庫故障時,可切換從數據庫,避免數據丟失。

2 可實現架構的擴展,當業務量越來越大,I/O訪問頻率過高時,採用多庫的存儲,可以降低磁盤I/O訪問的頻率,提高單個機器的I/O性能。

3 可實現讀寫分離,使數據庫能支持更大的併發。

4 實現服務器的負載均衡,通過在主服務器和從服務器之間切分處理客戶查詢的負荷。

6 總結

MySQL數據庫應該算是程序員必須掌握的技術之一了。無論是項目過程中還是面試中,MySQL都是非常重要的基礎知識。不過,對於MySQL來說,真的東西太多了。我在寫這篇文章的時候,查閱了大量的資料,發現越看不懂的越多。還真是應了那句話:

你知道的越多,不知道的也就越多。

這篇文章着重是從理論的角度去解析MySQL基本的事務和日誌系統的基本原理,我在表述的時候盡可能的避免採用實際的代碼去描述。即便是這篇將近一萬字+近二十副純手工繪製的圖解,也難以將MySQL的博大精深分析透徹。

但是我相信,對於初學者而言,這些理論能夠讓你對MySQL有一個整體的感知,讓你對“何謂關係型數據庫”這麼一個問題有了比較清晰的認知;而對於熟練掌握MySQL的大佬來說,或許本文也能夠喚醒你塵封已久的底層理論基礎,對你之後的面試也會有一定幫助。

技術這種東西沒有絕對的對錯,倘若文中有誤還請諒解,並歡迎與我討論。自主思考永遠比被動接受更有效。

7 reference

https://www.cnblogs.com/kismetv/p/10331633.html

https://www.cnblogs.com/ivy-zheng/p/11094528.html

https://blog.csdn.net/qq_39016934/article/details/90116706

https://www.jianshu.com/p/5af73b203f2a

https://www.cnblogs.com/f-ck-need-u/archive/2018/05/08/9010872.html#auto_id_2

微信搜索業餘碼農,閱讀更多技術隨筆。

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

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

銷售文案是什麼?A文案是廣告用的文字。舉凡任何宣傳、行銷、販賣商品時所用到的文字都是文案。在網路時代,文案成為行銷中最重要的宣傳方式,好的文案可節省大量宣傳資源,達成行銷目的。

帶你學夠浪:Go語言基礎系列-環境配置和 Hello world_網頁設計公司

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

當全世界的人們隨著網路時代而改變向上時您還停留在『網站美醜不重要』的舊有思維嗎?機會是留給努力改變現況的人們,別再浪費一分一秒可以接觸商機的寶貴時間!

文章每周持續更新,原創不易,「三連」讓更多人看到是對我最大的肯定。可以微信搜索公眾號「 後端技術學堂 」第一時間閱讀(一般比博客早更新一到兩篇)

前面幾周陸陸續續寫了一些後端技術的文章,包括數據庫、微服務、內存管理等等,我比較傾向於成體系的學習,所以數據庫和微服務還有後續系列文章補充。

最近工作上比較多的 Golang 編程,現在很多互聯網公司都在轉向 Golang 開發,所以打算寫一寫有關 Go 語言學習的系列文章,目標是從 Go 基礎到進階輸出一系列文章,沉澱下這些知識同時也給大家做參考,力求做到通俗易懂,即使你是 Golang 小白也能看懂,如果你是老手也能溫故知新。

本文將要和你分享 linux 下安裝 Golang 環境,並且講解如何通過配置 VSCode 遠程開發調試 Golang 程序。

下載源碼

你可以用系統自帶的包管理工具比如 yumapt-get 來安裝Golang開發環境。不過,為了通用性,我選擇通過源碼的方式來安裝和講解,在官網下載源碼,下載地址 https://golang.org/dl/

手動安裝

解壓安裝

我這裏下載下來的源碼包 go1.14.2.linux-amd64.tar.gz 放到遠程 Linux 服務器目錄下。執行以下命令安裝到 /usr/local 目錄。

tar -zxvf -C /usr/local/ `go1.14.2.linux-amd64.tar.gz`

創建工作空間

工作空間是你Go項目的「工作目錄」,挑選一個合適目錄,執行下面操作:

mkdir GoPath
mkdir -p GoPath/src
mkdir -p GoPath/bin
mkdir -p GoPath/pkg

三個目錄含義:

  src: 源碼路徑(例如:.go、.c、.h、.s 等)
  pkg: 編譯包時,生成的.a文件存放路徑
  bin: 編譯生成的可執行文件路徑

配置環境變量

安裝過程中有這麼幾個環境變量需要配置,先來了解一下:

GOROOT:Go的安裝路徑,也就是前面我們解壓到的目錄 /usr/local/go

GOBIN:Go項目的二進制文件存放目錄。

GOPATH:Go的工作空間。前面有介紹的工作空間目錄。

/etc/profile 文件追加以下內容完成設置。

export GOROOT=/usr/local/go
export GOPATH=/yourpath/GoPath # 設置你自己的GoPath路徑 
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOROOT/bin  # 加入到PATH環境變量
export PATH=$PATH:$GOPATH/bin
# source /etc/profile #立即生效

驗證安裝

# go version  #檢查版本
# go version go1.14.2 linux/amd64 # 輸出版本號

如果看到版本信息就代表安裝成功了!

遠程開發

上面我們在 Linux 環境下安裝好了 Golang 開發環境,但我不想每次打開終端登錄服務器編寫調試程序,怎麼才能在本地PC開發調試Golang程序呢?

看過我上一篇Vscode遠程開發的小夥伴應該能想到方法,我們就要用Vscode搭建Golang遠程開發環境。具體的遠程開發配置可以查看我的另一篇文章。

Golang開發插件

首先安裝官方推薦的 Go 開發插件,如下,點他安裝。

接着還會出現如下的提示,是因為缺少其他 Go 開發相關插件,點 install all 全都裝上就行。

Hello World

編程界有個慣例,什麼語言開始學習都是從 Hello World 開始。現在,我們就用 Golang 編寫第一個 HelloWorld 程序吧。

上代碼:

package main // 所有Go程序從main包開始運行

import "fmt" // 導入fmt包

func main() {
	fmt.Print("hello world", " i am ready to go :)\n")
	fmt.Println("hello world", "i am ready to go :)")
}

格式化 包

fmt 實現了類似 C++/C 語言的格式IO庫功能。

PrintPrintln 都可用於打印輸出,但是功能略有不同。可以看到我在Print 函數中,對后一個字符串加了空格和換行符,這樣兩個打印出來的結果是相同的。

Print

func Print(a ...interface{}) (n int, err error)

Print採用默認格式將其參數格式化並寫入標準輸出。如果兩個相鄰的參數都不是字符串,會在它們的輸出之間添加空格。返回寫入的字節數和遇到的任何錯誤。

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

透過資料庫的網站架設建置,建立公司的形象或購物系統,並提供最人性化的使用介面,讓使用者能即時接收到相關的資訊

Println

func Println(a ...interface{}) (n int, err error)

Println採用默認格式將其參數格式化並寫入標準輸出。總是會在相鄰參數的輸出之間添加空格並在輸出結束后添加換行符。返回寫入的字節數和遇到的任何錯誤。

調試

終端調試

在終端命令行源碼所在目錄輸入go run 運行程序。


# go run HelloWorld.go 
//輸出
hello world i am ready to go :)
hello world i am ready to go :)

也可以先編譯go build 得到可執行文件后再運行。

# go build HelloWorld.go 
# ls
HelloWorld  HelloWorld.go
# ./HelloWorld 
hello world i am ready to go :)
hello world i am ready to go :)

Vscode調試

F5啟動調試,編輯與調試控制台輸出如下:

命令行參數獲取

命令行參數可以通過os 包的 Args 函數獲取,代碼如下:

package main

import (
	"fmt"
	"os"
	"strconv"
)

func main() {
	// 命令行參數獲取,os.Args第一個參數是程序自身
	fmt.Println(os.Args)
	for idx, args := range os.Args {
		fmt.Println("參數"+strconv.Itoa(idx)+":", args)
	}
}

終端設置

以下是帶參數argv1 argv2 運行golang程序和輸出。

# go run basic.go argv1 argv2 

# 輸出
[/tmp/go-build441686724/b001/exe/basic argv1 argv2]
參數0: /tmp/go-build441686724/b001/exe/basic
參數1: argv1
參數2: argv2

VSCode設置

launch.json文件的 args 屬性配置可以設置程序啟動調試的參數。

設置之後,按F5 啟動調試,就會在調試控制台輸出配置的參數。

環境變量獲取

命令行參數可以通過os 包的 Getenv 函數獲取,代碼如下:

package main

import (
	"fmt"
	"os"
)

func main() {
	// 獲取環境變量
	fmt.Println(os.Getenv("type"), os.Getenv("name"), os.Getenv("GOROOT"))
}

VSCode設置環境變量

launch.json 文件的 args 屬性配置可以設置 VSCode 調試的 Golang 程序環境變量。

設置的格式是:name:vaule 形式,注意都是字符串。

終端設置環境變量

終端的環境變量設置就是可以用 Linux 的 export 命令設置,之後就可以用 os.Getenv 函數讀取。

比如我們最初設置 GOROOT 環境變量的命令

export GOROOT=/usr/local/go

就可以用 os.Getenv("GOROOT") 讀取,比較簡單,這裏就不多說了。

總結

現在,你有了一個可以遠程開發調試 Golang 的環境,趕緊去寫個 hello world 體驗一下吧!今天的分享就到這,下一篇文章講解基礎語法。

老規矩,感謝各位的閱讀,文章的目的是分享對知識的理解,技術類文章我都會反覆求證以求最大程度保證準確性,若文中出現明顯紕漏也歡迎指出,我們一起在探討中學習。今天的技術分享就到這裏,我們下期再見。

Reference

設置GOPATH

Visual Studio Code變量參考

Golang 獲取系統環境變量

os庫獲取命令行參數

原創不易,不想被白票,如果在我這有收穫,就動動手指「點贊」和「轉發」是對我持續創作的最大支持。

可以微信搜索公眾號「 後端技術學堂 」回復「資料」「1024」有我給你準備的各種編程學習資料。文章每周持續更新,我們下期見!

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

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

RWD(響應式網頁設計)是透過瀏覽器的解析度來判斷要給使用者看到的樣貌

學習源碼的第八個月,我成了Spring的開源貢獻者_網頁設計

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

擁有專業的維修技術團隊,同時聘請資深iphone手機維修專家,現場說明手機問題,快速修理,沒修好不收錢

我的經歷

關注我的朋友都知道,關注兩個字划重點,要考!

我最近一直在寫Spring的文章,而且僅僅是Spring FrameWork的文章 ,從最開始的官網入門到現在源碼的深度分析。主要就是三個系列

官網入門系列,Spring官網讀書筆記,這一系列的文章是入門Spring的不二之選,也是後續源碼閱讀的基礎

雜談系列,Spring雜談,這主要是一些補充內容,可以幫助大家更全面學習到Spring中的各個知識點,同時也會分享一些源碼閱讀技巧,個人學習心得之類的,雜談嘛,就是不知道放哪裡的文章都打算放這裏,比如這篇文章。

源碼分析系列,Spring源碼解析,該專欄目前正在創作中,相對而言學習難度比較大,而且因為筆者寫的比較細,估計大部分同學看起來會很費勁,不過如果你能認真看完,收穫絕對巨大!當然有不懂得地方也可以給筆者留言,或者關注文章末尾的公眾號。

本文的主要目的是教(zhuang)學(bi)

就是從筆者的實際經驗出發,談一談怎麼成為一個開源項目的貢獻者

我先說說我自己的經歷吧,在創作上篇文章的時候,筆者發現Spring在實例化對象的時候有這麼一段代碼,在org.springframework.beans.factory.support.ConstructorResolver#resolveConstructorArguments方法中

// 本文不探討技術細節,只是為了簡單說明這個問題,所以省略無關代碼	
private int resolveConstructorArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw,
			ConstructorArgumentValues cargs, ConstructorArgumentValues resolvedValues) {

      // ....
		for (Map.Entry<Integer, ConstructorArgumentValues.ValueHolder> entry : cargs.getIndexedArgumentValues().entrySet()) {
			int index = entry.getKey();
			if (index < 0) {
				throw new BeanCreationException(mbd.getResourceDescription(), beanName,
						"Invalid constructor argument index: " + index);
			}
            // 問題就出在這裏
			if (index > minNrOfArgs) {
				minNrOfArgs = index + 1;
			}
       // ..... 

上述代碼中,minNrOfArgs這個變量就是保存方法需要的最小參數個數,但是index是下標索引,索引是從0開始的,如果有下標為n的元素,那麼最小的參數個數應該是n+1嘛,所以if中的邏輯是沒有問題的,但是if這個判斷是有問題的,正確的做法應該是

if (index+1 > minNrOfArgs) {
    minNrOfArgs = index + 1;
}

當發現這個問題的時候,第一反應就是肯定是我的姿勢不對,錯的怎麼可能是代碼,肯定是我!

接下來,我就對這段代碼進行了慘無人道的調試,在無數次debug后,我發現,這個地方確實有問題!

在確認了這個問題之後,我要思考的就是怎麼把自己的想法反饋給Spring,換而言之,怎麼為偉大的開源來做貢獻呢?正常來要達到這個目的有兩個方式

  • 提交issue
  • 直接在GitHub上提交PR(pull request)

對應的就是在GitHub上點擊下圖紅框選中的兩個位置

如果是使用提交issue的方式,相當於給官方團隊提交了一個議題,這個議題可能是你發現代碼中的某個bug,也可能是你覺得官方的做法不夠好,你有更好的想法等等。感興趣的話,大家可以去看看Spring中現在有哪些還未關閉的issue,說不定其中一個你就能解決呢~!

如果要採用提交PR的方式的話,首先你得將代碼fork到自己的GitHub中,然後在從自己的GitHub檢出到本地,在本地做完修改后,提交到GitHub倉庫中,最後從自己的GitHub向Spring官方倉庫發起一個PR。

像我的話很早就已經將代碼fork到了自己GitHub

上圖中的第一個紅框,說明我這個倉庫是從Spring官方fork過來的,第二個紅框就是可以從這裏向Spring官方提交一個PR。關於詳細的如何提交PR,大家可以自行百度,這裏不做詳細的介紹了。

另外,說了這麼多,先給大家看下我提交的issue吧。

issue鏈接:https://github.com/spring-projects/spring-framework/issues/25130

因為內容也不長,所以我這裏把原文就直接放到下面了

In ConstructorResolver:

private int resolveConstructorArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw,
			ConstructorArgumentValues cargs, ConstructorArgumentValues resolvedValues) {
		TypeConverter customConverter = this.beanFactory.getCustomTypeConverter();
		// ...

		for (Map.Entry<Integer, ConstructorArgumentValues.ValueHolder> entry : cargs.getIndexedArgumentValues().entrySet()) {
			int index = entry.getKey();
			if (index < 0) {
				throw new BeanCreationException(mbd.getResourceDescription(), beanName,
						"Invalid constructor argument index: " + index);
			}
			if (index > minNrOfArgs) {
				minNrOfArgs = index + 1;
			}
			// ....
		}
// ....
 return minNrOfArgs;
}

I assume that method resolveConstructorArguments is to resolve contructor arguments in the XML file and return the minimum number of parameters required by contructor 。but if the first parameter is autowired , the second parameter is config by XML file,the method will not work well。

example:

public class FactoryObject {
	
 public DmzService getDmz(String name, int age, Date birthDay, OrderService orderService) {

	public DmzService getDmz(OrderService orderService,String name) {
		
		return new DmzService(orderService,name);
	}

}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
	   default-autowire="constructor">
	<bean id="factoryObject" class="com.dmz.spring.first.instantiation.service.FactoryObject"/>

	<bean class="com.dmz.spring.first.instantiation.service.OrderService" id="orderService"/>

	<bean id="dmzService" factory-bean="factoryObject" factory-method="getDmz">
		<constructor-arg index="1"  value="dmz"/>
	</bean>

</beans>

the resolveConstructorArguments method will return 1,but correct answer is 2。

I think the problem arises because of this judgment:

if (index > minNrOfArgs) {
 minNrOfArgs = index + 1;
}

It might be better to change it to look like this

if (index + 1 > minNrOfArgs) {
 minNrOfArgs = index + 1;
}s

我在提交issue時主要是按照這種思路

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

窩窩以「數位行銷」「品牌經營」「網站與應用程式」「印刷品設計」等四大主軸,為每一位客戶客製建立行銷脈絡及洞燭市場先機。

  1. 首先擺出有問題的代碼
  2. 描述具體的問題,我是直接通過一個例子來描述的
  3. 說出自己的建議

這幾天我又多看了看別人提交的issue,對比起來,我覺得至少應該還要添加一點

  • 應該要明確的指出具體哪個版本上出現的問題

碰到的問題

1、擔心鬧烏龍

雖然在之前我已經調試過了無數次代碼,但是心裏還是沒譜啊。畢竟我這麼謹(cai)慎(ji)的一個人,萬一被人噴了怎麼辦?不知道你會不會這麼想,反正我當時就是這麼想的,如果你是這麼想的,建議你去看看別人提交的issue。搜索條件如下

is:closed label:”status: invalid”

我覺得你看幾個,自然就有信心了!

2、不知道要怎麼提交

每個開源的項目,只要作者希望這個項目越來越好的話,都會詳細的說明如何給這個項目做開源貢獻,Spring肯定也不例外,這裏還是以提交issue為例,當你點擊New issue的時候會出現下面這張圖

在上圖左邊的框里很明確的告訴了你提交issue應該要注意什麼

  • 首先,你應該要去Stack Overflow提問
  • 如果是bug,你應該要指明版本以及你想要做什麼
  • 如果是一個增強的話,要提供上下文並且描述清楚問題
  • 同一個問題,issue跟PR最好只提交一個,因為GitHub認為它們是一樣的,如果你還不能確定的話,先提交一個issue

而右上角還有更加詳細的文檔可供參考。

3、英文

大家應該看到了,整個issue都是用英文寫的,那麼英文不好怎麼辦呢?這個時候就要掏出我們的神器了

嗯,就是詞典,筆者習慣是使用有道詞典。我建議英文不好的同學可以這樣,先將整個issue用中文寫好,如果你真的英文一竅不通的話,可以直接通過翻譯軟件逐句翻譯,然後粘貼到GitHub上。但是千萬千萬不要使用中文,就像下面這個哥們

issue鏈接:https://github.com/spring-projects/spring-framework/pull/25127

像這種issue是會被直接打上invalid(不合格)標籤的,你就想想吧,你學不會英文,你指望我們的外國朋友能看懂中文嘛?是我中華上線五千年的文化不夠博大精深嗎?

4、擔心問題描述的不清楚

其實這個問題就是因為英文不好衍生出來的。因為英文不好,自然就會擔心我寫的東西他能不能看懂呢?我的建議就是,結合你測試的代碼去描述問題。你不用去擔心別人看不懂你寫的代碼,就以我那個issue的處理流程為例吧。

在你剛剛提交issue時,有專門的issuemaster(issue管理員)會給你提交的issue打上一個wait-for-triage的標籤,標誌這個issue是待處理的。

隨後我提交的這個issue,就被指派給了jhoeller。你要擔心他看不懂代碼嗎?給你看兩個東西吧

你知道那個紅框是啥意思嗎?就是說我發現的那個有問題代碼的類的作者就是他。

再看一張

就是說,jhoeller從2003年開始就已經是Spring這個項目的管理者以及發布經理了。2003年,我還是一個小學生……..

所以啊,只要你稍微正常點,基本上人家都能get到你的點。

給你的建議

其實筆者從發現這個問題到最終提交issue大概經過了一周時間,期間一直在猶豫要不要提交issue,就是因為上面提到的幾個問題,一直躊躇不前。但是等我下定決心要去做這件事的時候總共就花了幾個小時的時間。包括研究issue提交的規則以及寫一篇英文版的issue。並且我提交issue的第二天就馬上被處理了,並且jhoeller在 f9aae8d 這個commit中已經接受我的建議。

所以我要說的就是,

真正動手的話,不管什麼問題總能找到解決方案

而只是停留在空想,在躊躇,你永遠有一堆問題

臨淵羡魚,不如退而結網

以此文與君共勉!

如果本文對你由幫助的話,記得點個贊吧!也歡迎關注我的公眾號,微信搜索:程序員DMZ,或者掃描下方二維碼,跟着我一起認認真真學Java,踏踏實實做一個coder。

我叫DMZ,一個在學習路上匍匐前行的小菜鳥!

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

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

網動是一群專業、熱情、向前行的工作團隊,我們擁有靈活的組織與溝通的能力,能傾聽客戶聲音,激發創意的火花,呈現完美的作品

初窺Ansible playbook_貨運

※回頭車貨運收費標準

宇安交通關係企業,自成立迄今,即秉持著「以誠待人」、「以實處事」的企業信念

Ansible是一個系列文章,我會盡量以通俗易懂、詼諧幽默的總結方式給大家呈現這些枯燥的知識點,讓學習變的有趣一些。
Ansible系列博文直達鏈接:Ansible入門系列

前言

在上一篇文章中說到Ansible有兩種玩法,一種是Ansible Ad-Hoc,另一種是就是這裏要說的playbook。playbook是Ansible進行配置管理的組件,雖然Ansible的日常Ad-Hoc命令功能很強大,能完成一些基本的配置管理工作,但是Ad-Hoc命令無法支撐複雜環境的配置管理工作。在我們實際使用Ansible的工作中,大部分時間都是在編寫playbook,接下來就重點說說如何玩轉這個playbook。

執行playbook命令

我們都是按照yaml語法規則來編寫playbook,至於yaml怎麼玩,後面的文章我會總結一下的。在我們按照要求編寫好了yaml文件后,如何來執行這個yaml文件呢?

Ansible提供了一個單獨的命令:ansible-playbook命令,我們可以通過這個命令來執行yaml腳本。常見的ansible-playbook的使用方法如下:

最簡單的使用方法:

ansible-playbook copyDemo.yaml

我們還可以使用以下命令查看輸出的細節:

ansible-playbook copyDemo.yaml --verbose

我們也可以使用以下命令查看該yaml腳本將影響的主機列表:

ansible-playbook copyDemo.yaml --list-hosts

還可以使用以下命令檢查yaml腳本語法是否正確:

ansible-playbook copyDemo.yaml --syntax-check

上面的幾種使用方法基本就涵蓋了我們日常工作中80%的場景了,剩餘的20%場景,比如并行、異步等,很少用到,等真正用到的時候再去查閱相關資料也來的及。而工作中,更多的時候,我們不是在編寫playbook,就是在編寫playbook的路上。所以,接下來我重點說說如何寫這個playbook,也就是playbook的基本語法。

playbook基本語法

最基本的playbook腳本分為三個部分:

  1. 在哪些機器上以什麼身份執行
  2. 執行的任務有哪些
  3. 善後任務有哪些

我們在編寫playbook腳本的時候,總是離不開上面的三個部分的。下面先來一個稍微有點複雜的playbook腳本,讓大家先有一個整體的認識。

---
- hosts: server1
  user: root
  vars:
    http_port: 80
    max_clients: 200

  tasks:
    - name: Write apache config file
      template: src=/home/test1/httpd.j2 dest=/home/test2/httpd.conf
      notify:
        - restart apache
    - name: Ensure apache is running
      service: name=httpd state=started

  handlers:
    - name: restart apache
      service: name=httpd state=restarted

現在就對上述三部分稍作詳細總結。

主機和用戶

上面的yaml腳本,我們一開始就會看到hostsuservars,其中vars在後面的文章進行專門總結。而這裏的hostsuser就是表示我們這個yaml將要在哪些主機上用哪個用戶身份去操作。而這裏的深一層次的關係如下錶所示:

key 含義
hosts 為主機的IP,或者主機組名,或者關鍵字all
user 在遠程以哪個身份執行
become 切換成其他用戶身份執行,值為yes或者no
become_method 與become一起使用,值可以為sudo/su
become_user 與become一起使用,可以是root或者其它用戶名

在實際工作中,如果我們不指定user時,則默認使用連接遠程主機的用戶進行操作,如果指定了執行用戶而與ansible_ssh_user指定用戶不一致時,則需要開啟become操作,這裏的become配置與ansible.cfg中配置將相互配合完成工作,yaml中的become優先級高於ansible.cfg中配置中的優先級。

任務列表

任務列表是整個playbook的核心,對於任務列表,我們首先需要知道以下三點內容:

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

網動結合了許多網際網路業界的菁英共同研發簡單易操作的架站工具,及時性的更新,為客戶創造出更多的網路商機。

  • 任務是從上到下順序執行的,如果中間發生錯誤,那麼整個playbook會中止;
  • 每一個任務都是對模塊的一次調用,只是使用不同的參數和變量而已;
  • 每一個任務最好有一個name屬性,這樣在執行yaml腳本時,可以看到執行進度信息。

對於任務的參數有兩種不同的寫法,我們在編寫yaml腳本時,可以按照自己的喜好進行選擇。

寫法一:

- name: Write apache config file
  template: src=/home/test1/httpd.j2 dest=/home/test2/httpd.conf

寫法二:

- name: Write apache config file
  template: 
    src: /home/test1/httpd.j2
    dest: /home/test2/httpd.conf

這兩種寫法都是OK的,我一般喜歡第二種寫法。

最後,對於任務我們還需要特別一個點,那就是任務的執行狀態。我們在執行Ansible Ad-Hoc或者ansible-playbook的時候,在輸出中都會有一個changed字段,比如:

192.168.1.3                : ok=2    changed=0    unreachable=0    failed=0  

或者

192.168.1.3                : ok=2    changed=1    unreachable=0    failed=0

這裏的這個changed就是人物的執行狀態,但是它為什麼一會是0,一會有是1呢?這就要說到Ansible中一個叫做“冪等性”的概念。

冪等性

冪等性是數學和計算機科學上一個常見的概念,多次執行產生的結果不會發生改變,這樣的特性就被成為冪等性。

大多數的Ansible模塊在設計時保證了冪等性,冪等性保證了Ansible腳本多次執行情況下的相同結果,盡可能的避免使用那些不能滿足冪等性的模塊。比如我們經常使用的shell模塊就是非冪等性的。

我們要明白Ansible是以“結果為導向的”,我們指定了一個“目標狀態”,Ansible會自動判斷“當前狀態”是否與“目標狀態”一致,如果一致,則不進行任何操作;如果不一致,那麼就將“當前狀態”變成“目標狀態”,這就是“冪等性”,“冪等性”可以保證我們重複的執行同一項操作時,得到的結果是一樣的。

那這個冪等性與上面的changed又有什麼關係呢?且聽我下面慢慢道來!

  • changed為false或者0時,表示Ansible沒有進行任何操作,沒有“改變什麼”;
  • changed為true或者大於0時,表示Ansible執行了操作,“當前狀態”已經被Ansible改變成了“目標狀態”。

copy這個模塊來舉例子說明,當我們準備將一個文件通過Ansible拷貝到遠程主機時,copy模塊首先檢查遠程是否已經存在了該文件,如果不存在,則把文件拷貝過去,返回changed為大於0;如果存在時,則開始比對兩個文件的md5值,如果md5值一致,則說明兩個文件是一樣的,則不需要拷貝,此時copy模塊則什麼都不幹,返回changed為0。

總結

通過三篇文章總結了Ansible中的常用模塊、Ansible Ad-Hoc和ansible-playbook的一些慣用用法,從我的實際學習經驗來說,學到這裏,你可以將這三塊內容結合起來使用了,至少可以在你們生產環境鼓搗一下了。生來就是折騰,更何況我們這麼拚命、努力的學習呢!

果凍想,認真玩技術的地方。

2019年5月18日,於內蒙古呼和浩特。

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

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

搬家價格與搬家費用透明合理,不亂收費。本公司提供下列三種搬家計費方案,由資深專業組長到府估價,替客戶量身規劃選擇最經濟節省的計費方式

面試官問我會不會Elasticsearch,我語塞了…_網頁設計公司

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

節能減碳愛地球是景泰電動車的理念,是創立景泰電動車行的初衷,滿意態度更是服務客戶的最高品質,我們的成長來自於你的推薦。

少點代碼,多點頭髮

本文已經收錄至我的GitHub,歡迎大家踴躍star 和 issues。

https://github.com/midou-tech/articles

從今天開始準備給大家帶來全新的一系列文章,Elasticsearch系列

新系列肯定會有很多疑惑,先為大家答疑解惑,下面是今天要講的問題

為什麼寫Elasticsearch系列文章?

之前在文章中也陸陸續續的提到過,龍叔是做搜索引擎的。搜索引擎技術屬於商業技術,大家耳熟能詳的百度搜索,Google搜索,這可都是因為把握核心搜索技術,從而誕生了商業帝國。

每個互聯網大廠都想去分一杯搜索的羹,360搜索、神馬、頭條、搜狗搜索等等,由此可見搜索技術的商業作用和機密性了。

搜索把握用戶的入口

蘑菇街的搜索引擎是一款使用C++開發、完全自研、沒有開源的搜索引擎,沒有開源就是不能隨便寫出來的。

但是現在不一樣了

第一、我離職了,離開了意味着不在持有那些商業機密了,就算不講出來我也沒啥心理負擔(但還是不能講的,離職協議寫的很清楚,不能泄露公司商業機密)。

第二、去新的公司還是在搜索領域,他們用Es Elasticsearch是一個開源搜索,開源的東西可以隨便說,但還是不能說公司的商業數據

自己一直在搜索領域做,輸出搜索相關的文章,第一個可以讓自己更好的學習和總結,第二個可以讓粉絲們了解到搜索這個神秘的技術,增加大家自身的核心競爭力。

後面會說到,Elasticsearch是搜索引擎,但不簡單隻能使用在搜索領域,他可以作用的場景非常多。

Elasticsearch是什麼?

Elasticsearch 是一個分佈式的開源搜索分析引擎,適用於所有類型的數據,包括文本、数字、地理空間、結構化和非結構化數據。

Elasticsearch 在 Apache Lucene 的基礎上開發而成,Elasticsearch 以其簡單的 REST 風格 API、分佈式特性、速度和可擴展性而聞名,是 Elastic Stack 的核心組件。

Elastic Stack 是適用於數據採集、充實、存儲、分析和可視化的一組開源工具。人們通常將 Elastic Stack 稱為 ELK Stack(代指 Elasticsearch、Logstash 和 Kibana),目前 Elastic Stack 包括一系列豐富的輕量型數據採集代理,這些代理統稱為 Beats,可用來向 Elasticsearch 發送數據。

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

搬家費用:依消費者運送距離、搬運樓層、有無電梯、步行距離、特殊地形、超重物品等計價因素後,評估每車次單

Elasticsearch 的實現原理主要分為以下幾個步驟,首先用戶將數據提交到Elasticsearch 數據中心,再通過分詞控制器去將對應的數據分詞,將其權重和分詞結果一併存入數據,當用戶搜索數據時候,再根據權重將結果排名,打分,再將返回結果呈現給用戶。

是什麼差不多搞清楚了,再說說ES都哪些成熟的應用以及在哪些領域使用。

Elasticsearch在哪些領域使用?

  • 應用程序搜索
  • 網站搜索
  • 企業搜索
  • 日誌處理和分析
  • 基礎設施指標和容器監測
  • 應用程序性能監測
  • 地理空間數據分析和可視化
  • 安全分析
  • 業務分析

Elasticsearch有哪些特點?

Elasticsearch 很快。 由於 Elasticsearch 是在 Lucene 基礎上構建而成的,所以在全文本搜索方面表現十分出色。Elasticsearch 同時還是一個近實時的搜索平台,這意味着從文檔索引操作到文檔變為可搜索狀態之間的延時很短,一般只有一秒。因此,Elasticsearch 非常適用於對時間有嚴苛要求的用例,例如安全分析和基礎設施監測。

Elasticsearch 具有分佈式的本質特徵。 Elasticsearch 中存儲的文檔分佈在不同的容器中,這些容器稱為分片,可以進行複製以提供數據冗餘副本,以防發生硬件故障。Elasticsearch 的分佈式特性使得它可以擴展至數百台(甚至數千台)服務器,並處理 PB 量級的數據。

Elasticsearch 包含一系列廣泛的功能。 除了速度、可擴展性和彈性等優勢以外,Elasticsearch 還有大量強大的內置功能(例如數據匯總和索引生命周期管理),可以方便用戶更加高效地存儲和搜索數據。

Elastic Stack 簡化了數據採集、可視化和報告過程。 通過與 Beats 和 Logstash 進行集成,用戶能夠在向 Elasticsearch 中索引數據之前輕鬆地處理數據。同時,Kibana 不僅可針對 Elasticsearch 數據提供實時可視化,同時還提供 UI 以便用戶快速訪問應用程序性能監測 (APM)、日誌和基礎設施指標等數據。

學習Elasticsearch能提高哪些競爭力?

看到Elasticsearch在這麼多的領域在使用,特點也這麼明顯。看到這裏估計都不用在說什麼核心競爭力,你已經意識到了。

Elastic 於 2018 年 6 月 29 日正式推出 Elastic Certified Engineer 認證考試,認證通過可以獲得官方頒發的證書和徽章,title就是 Elastic認證工程師

具體認證的細節和含金量,沒有具體研究過,但是可以很明顯的感受到官方出了這樣一個認證,表明社會需要大量這樣的人才,而這方面人才的培養和考核指標還欠缺。

有沒有必要一定要考這個認證?

個人覺得,和英語四六級一樣,通過了再說沒用。

如果你是學生,可以考慮去考一個認證,因為你很難有業務場景驅使你去做這方面的成長,認證一定是有難度的,一個一個的困難會驅使你成長,最終這個認證也會成為招聘時一個非常大的亮點。

這個認證會有哪些幫助?

  • 對於快速的構建知識體系幫助。

  • 對於全面的熟悉官方文檔幫助。

  • 對於實戰解決線上問題幫助。(遇到了相關技術問題基本上不需要再求助於社區,80%以上的問題自己基本就能解決。)

  • 對於增強信心、克服英文恐懼幫助。

Elasticsearch 支持哪些編程語言?

  • Java
  • JavaScript (Node.js)
  • Go
  • .NET (C#)
  • PHP
  • Perl
  • Python
  • Ruby

哪裡可以找到有關 Elasticsearch 的更多信息?

  • Elasticsearch GitHub 存儲庫:https://github.com/elastic
  • Elasticsearch 官方文檔:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
  • Elasticsearch中文社區:https://elasticsearch.cn

我是龍叔,一個分享互聯網技術和心路歷程的star。

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

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

透過選單樣式的調整、圖片的縮放比例、文字的放大及段落的排版對應來給使用者最佳的瀏覽體驗,所以不用擔心有手機版網站兩個後台的問題,而視覺效果也是透過我們前端設計師優秀的空間比例設計,不會因為畫面變大變小而影響到整體視覺的美感。

和付費網盤說再見,跟着本文自己起個網盤(Java 開源項目)_租車

※超省錢租車方案

商務出差、學生出遊、旅遊渡假、臨時用車!GO 神州租賃有限公司!合法經營、合法連鎖、合法租賃小客車!

本文適合有 Java 基礎知識的人群,跟着本文可學習和運行 Java 網盤項目。

本文作者:HelloGitHub-秦人

HelloGitHub 推出的《講解開源項目》系列。

今天給大家帶來一款開源 Java 版網盤項目—— kiftd-source,本文將用 3 分鐘帶大家搭建一個個人網盤,技術便利生活,你值得擁有~

項目地址:https://github.com/KOHGYLW/kiftd-source

一、項目介紹

kiftd 是一款開源、使用簡單、功能完整的 Java 網盤/雲盤系統。支持在線視頻播放、文檔在線預覽、音樂播放、圖片查看等功能的文件雲存儲平台。

技術棧

  • JDK 版本:1.8.0_131
  • 項目管理框架:Maven(m2e 1.8.0 for Eclipse)
  • Archetype:mavem-archetype-quickstart 1.1
  • Spring Boot:SpringBoot 基於 Spring 開發,旨在提高微服務的開發效率。
  • MyBatis:一款優秀的持久層框架,它支持自定義 SQL、存儲過程以及高級映射。
  • H2 DB:一款開源的嵌入式數據庫引擎,採用 Java 語言編寫,不受平台的限制。

二、網盤搭建

2.1 Windows 環境運行

2.1.1 下載安裝包

直接從官網下載最新的安裝包,安裝地址:https://kohgylw.gitee.io/

項目比較溫馨,支持三種下載方式:Github、阿里雲、Gitee 下載。如下圖:

2.1.2 檢查配置

這裏主要檢查一下本地 JDK 是否已安裝,在命令行窗口執行 java -version 查看 Java 版本。如下所示表示已安裝,就可以進行下一步操作。

java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)

2.1.3 運行 jar

雙擊 kiftd-1.0.29-RELEASE.jar,或者在命令行執行 java -jar kiftd-1.0.29-RELEASE.jar 命令都運行可以jar 文件,會彈出安裝的界面,如下圖:

這個界面的這幾個按鈕說明一下:

  • 開啟(Start):運行網盤服務,初次啟動的端口默認是 8080
  • 文件(Files):這個按鈕菜單中主要有網盤文件導入,導出,刪除,刷新功能。
  • 設置(Setting):設置功能主要可以設置網盤的服務端口,網盤的物理存儲路徑等信息。
  • 退出(Exit):關閉網盤系統。

點擊 開啟(Start) 按鈕即可運行網盤,這裏我設置的端口是 8090,在瀏覽器訪問: localhost:8090,運行效果如下圖:

項目是運行了,發現一個問題無法上傳文件?因為我們忘了登錄這個操作。點擊系統 登錄按鈕,填入賬號和密碼即可登錄。那麼登錄密碼在哪裡呢?這裏我直接告訴大家,用戶信息在 conf/account.properties,文件內容如下:

#<This is the default kiftd account setting file. >
#Sun May 10 21:56:28 CST 2020
admin.pwd=000000  #用戶名.密碼=000000
authOverall=l
admin.auth=cudrm
  • 用戶名:admin
  • 密碼:000000

這樣登錄之後就可以使用網盤的所有功能了。

2.2 Linux 環境運行

2.2.1 安裝 Screen 工具

Screen 工具能夠虛擬出一個終端並執行相應的操作。因為本篇所講的網盤需要一個終端。執行如下命令安裝 Screen

yum install screen

2.2.2 Screen 常用命令

screen -S myScreen #創建虛擬終端
java -jar kiftd-1.0.29-RELEASE.jar -console #在虛擬終端中以命令模式啟動 kiftd
screen -r myScreen #返回之前的虛擬終端並繼續操作 kiftd。

2.2.3 命令行操作

Linux 環境上使用 console 模式啟動的效果是這樣的:

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

有別於一般網頁架設公司,除了模組化的架站軟體,我們的營業主軸還包含:資料庫程式開發、網站建置、網頁設計、電子商務專案開發、系統整合、APP設計建置、專業網路行銷。

命令行輸入 -start 即可運行項目。例如輸入 -files 控制台显示是這樣的:

其實和 Windows 上一樣,包括文件導入,導出,刪除功能,多了幾個命令是查看文件,切換目錄等功能。

三、開發環境運行

3.1 下載項目

兩種方式下載項目,使用 GitBash 下載項目:

git clone https://github.com/KOHGYLW/kiftd-source.git

另外一種方式直接下載 zip 壓縮包,如下圖:

3.2 運行

打開 kohgylw.kiftd.mc.MC 類,進行測試運行。注意:本文使用 Eclipse 工具打開。

3.3 閱讀代碼

3.3.1 前台請求

就以創建目錄這個功能為例。我們先看前端功能。點擊“操作”->“新建文件夾”,填寫文件夾名稱,點擊保存如下圖:

我們知道前台 新建文件夾 功能調用的後台接口是 newFolder.ajax

3.3.2 後端接口
通過前台請求可知調用的後台接口為 homeController/newFolder.ajax。打開代碼實現,我們會看到下面這個方法。

public String newFolder(final HttpServletRequest request) {
		
        ...
        //參數校驗的部分代碼已省略
		Folder f = new Folder();
		f.setFolderId(UUID.randomUUID().toString());
		f.setFolderName(folderName);
		f.setFolderCreationDate(ServerTimeUtil.accurateToDay());
		if (account != null) {
			f.setFolderCreator(account);
		} else {
			f.setFolderCreator("匿名用戶");
		}
		f.setFolderParent(parentId);
		int i = 0;
		while (true) {
			try {
                // 數據庫插入新建文件夾的數據
				final int r = this.fm.insertNewFolder(f);
				if (r > 0) {
					if (fu.isValidFolder(f)) {
						this.lu.writeCreateFolderEvent(request, f);
						return "createFolderSuccess";
					} else {
						return "cannotCreateFolder";
					}
				}
				break;
			} catch (Exception e) {
				f.setFolderId(UUID.randomUUID().toString());
				i++;
			}
			if (i >= 10) {
				break;
			}
		}
		return "cannotCreateFolder";
	}

四、功能說明

4.1 上傳

  1. 點擊 操作,可以上傳文件和上傳文件夾,如下圖:

  2. 將本地需要上傳的文件,拖拽網盤頁面也可以上傳此文件。

4.2 視頻/音頻播放

  1. 上傳視頻到網盤,網盤也支持在線視頻播放,效果如下圖:

  2. 上傳音頻,例如我最喜歡 周杰倫 的歌曲,可以在線播放了。

4.3 快捷鍵使用

網盤還對一些常用功能添加了快捷鍵。功能和快捷鍵參照如下:

功能 快捷鍵
上傳文件夾 Shift +U
上傳文件 Shift +F
新建文件 Shift +N
複製 Shift +C
剪切 Shift +X
刪除 Shift +D

4.4 配置文件修改

配置文件在項目 conf 目錄,包括兩個配置文件:

  • account.properties:配置賬號信息,權限信息
  • server.properties:服務器的配置文件,可配置服務器端口,緩衝文件大小等

4.5 在線預覽

網盤支持文檔 txtpdfdocxppt 在線預覽功能,支持圖片的在線預覽。圖片預覽效果如下:

pdf 文件預覽效果如下:

4.6 分享下載鏈接

網盤也考慮文件的分享,它可以生成下載鏈接,瀏覽器訪問下載鏈接就可以直接下載文件。選擇需要下載的文件,點擊 下載 按鈕,選擇 下載鏈接+,既可以生成文件下載鏈接。如下圖:

五、最後

教程至此已經結束,你自己的網盤跑起來了嗎?網盤是不是還不錯?而且搭建也特別簡單。一些重要的東西就可以存放到自己的網盤啦!說到底,編程語言只是工具,我們只要很好的使用工具,再加上自己天馬行空的思想,我想會創造出更多不可思議的項目。

Java 語言為什麼經久不衰,因為它能做的事情太多了,而且生態也特別豐富。如果你也有興趣那就加入 Javaer 開發者的大家庭吧!開源分享讓我們彼此認識,有了開源項目讓我們看到編程語言的絢麗多彩。

教程至此,你應該也能快速運行個人網盤了。編程是不是也特別有意思呢?先下載安裝包給自己部署一套網盤系統吧。對源碼感興趣的朋友可以開始學習項目源碼了~

關注公眾號加入交流群

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

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

日本、大陸,發現這些先進的國家已經早就讓電動車優先上路,而且先進國家空氣品質相當好,電動車節能減碳可以減少空污

Python編程思想(3):数字及其相關運算_包裝設計

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

上新台中搬家公司提供您一套專業有效率且人性化的辦公室搬遷、公司行號搬家及工廠遷廠的搬家服務

Python 提供了三種數值類型:int(整型),float(浮點型)和complex(複數)。

  • int:通常被稱為整型或者整數,如200、299、10都屬於整型;
  • float:浮點數包含整數和小數部分,如3.1415926,2.71828都屬於浮點數;
  • complex:複數包含實數部分和虛數部分,形如 a+bj,其實部和虛部都是浮點類型;

需要注意的是,Python3 已經廢棄了 Python2 的 Long(長整型),在 Python3 中,int 的大小沒有限制,可以作為 Long 使用。這也是為什麼Python非常適合科學計算的原因,因為Python可以處理無限大的整數。在Python中進行數值運算,並不需要考慮溢出問題,因為Python的數值永遠不會溢出。  
1. 數值類型之間的轉換   Python 的三種數值類型可以進行相互轉換,轉換方式為:数字類型+圓括號,如下實例:

a = 456
b = 2.71828
print("int(b)=",int(b))
print("float(a)=",float(a))
print("complex(a)=",complex(a))
print("complex(a,b)=",complex(a,b))

執行結果如下圖所示:    
2. 常用的數學函數   Python 提供了豐富的數學函數以降低編程實現的難度,本問將介紹一些常用的函數。

import math
#求絕對值:abs(x)
print("abs(-200)=",abs(-200))

#向上取整:ceil(x)
print("ceil(3.1415)=",math.ceil(3.1415))

#向下取整:floor(x)
print("floor(3.678)=",math.floor(3.678))

#四舍五入:round(x)
print("round(3.678)=",round(3.678))

#乘方運算:pow(x,y),x的y次方
print("pow(3,4)=",pow(3,4))

#求平方根:sqrt(x)
print("sqrt(144)=",math.sqrt(144))

執行結果如下圖所示:

 
3. 運算符   計算機的最基本用途之一就是執行數學運算,作為一門計算機編程語言,Python 也提供了一套豐富的運算符來滿足各種運算需求。 Python 運算符主要可以分為6種:算術運算符、比較運算符、賦值運算符、邏輯運算符、位運算符和成員運算符。  
(1)算術運算符 對於算術運算,大家並不陌生,常用的加減乘除就是算術運算。不過,在編程語言里,算術運算符特殊一些,Python 中的算術運算有7種:加(+)、減(-)、乘( * )、除(/)、取模(%)、冪運算( ** )和取整預算(//)。以下通過實例演示算術運算符的用法。

#初始化測試數據
x = 30
y = 24
z = 12
#分別進行7種算術運算
z = x + y
print("x + y =", z)
z = x - y
print("x - y =", z)
z = x * y
print("x * y =", z)
z = x / y
print("x / y =", z)
z = x % y
print("x % y =", z)
z = x ** y
print("x ** y =", z)
z = x // y
print("x // y =", z)

執行結果如下圖所示:

 

  (2)比較運算符

比較無處不在,大於、小於、等於、不等於……和 C/C++、Java 等編程語言一樣,Python 也提供了6種比較運算符:>(大於),<(小於),==(等於),!=(不等於),>=(大於等於),<=(小於等於)。比較運算的結果是一個布爾值,True 或者 False,看下面的案例代碼:

# 初始化變量
x = 12
y = 7
#分別進行6種比較運算
print("x == y:", x == y)
print("x != y:", x != y)
print("x > y:", x > y)
print("x < y:", x < y)
print("x >= y:", x >= y)
print("x <= y:", x <= y)

執行結果如下圖所示:  
(3)賦值運算符 其實在前面的代碼中已經用到賦值運算,如 x = 12,就是一個最簡單的賦值運算,“=”就是最簡單的賦值運算符。將簡單的賦值運算與算術運算結合,Python 形成了更豐富的賦值運算符:+=、-=、=、/=、%=、*=、//=。看下面的案例代碼:

#初始化變量
x = 12
y = 21
#分別進行7種賦值運算
y = x
print("y = x, y =", y)
y += x
print("y += x, y =", y)
y -= x
print("y -= x, y =", y)
y *= x
print("y *= x, y =", y)
y /= x
print("y /= x, y =", y)
y **= x
print("y **= x, y =", y)
y //= x
print("y //= x, y =", y)

執行結果:

 

 

(4)邏輯運算符 所謂邏輯運算,就是:與、或、非。Python 中的3種邏輯運算符分別為:and(與),or(或),not(非),邏輯運算的結果是布爾值:True 或者 False。

  1. A and B:當A和B有一個為False時,結果就為False,只有A和B都為True時,結果才為True;
  2. A or B:只有當A和B都是False時,結果才為False,只要有一個為True,結果就為True;
  3. not A:當 A 為 True 時,返回 False,否則返回 True。

看下面的案例:

#初始化變量
a = 2
b = 3
c = 5
#分別執行3種邏輯運算
print("a>b and a<c :", a>b and a<c)
print("a<b and c :", a<b and c)
print("a>b or c :", a>b or a<c)
print("a<b or c :", a<b or c)
print("a or a<c :", a or a<c)
print("not a :", not a)
print("not a<b :", not a<b)

執行結果如下圖所示:

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

窩窩觸角包含自媒體、自有平台及其他國家營銷業務等,多角化經營並具有國際觀的永續理念。

 
(5)位運算符 程序中的所有數值在計算機內存中都是以二進制的形式儲存的。位運算就是直接對整數在內存中的二進制位進行操作。Python 中有6種位運算符:

  • &:按位與,參与運算的兩個值,如果兩個相應位都為1,則該位的結果為1,否則為0;
  • |:按位或,只要對應的2個二進位有一個為1時,結果位就為1;
  • ^:按位異或,當兩對應的二進位相異時,結果為1;
  • ~:按位取反,對數據的每個二進制位取反,即把1變為0,把0變為1;
  • >>:按位右移,將>>左側的數按位向右移動>>右邊的數指定的位;
  • <<:按位左移,將<<左側的數按位向左移動<<右邊的數指定的位;

看下面的案例代碼:

a = 21
b = 6
print('a & b = ',a & b)
print('a | b = ',a | b)
print('a ^ b = ',a ^ b)
print('~a = ',~a)
print('a << 2 = ',a << 2)
print('a >> 2 = ',a >> 2)

執行結果如下圖所示:

 

 

 這裏講a=21,b=6,轉換為二進制如下:

a = 0001 0101
b = 0000 0110

a&b = 0000 0100
a|b = 0001 0111
a^b = 0001 0011
~a = 1110 1010
a<<2 = 01010100
a>>2 = 0000 0101

(6)成員運算符 除了前面介紹的5種運算符,Python 還支持成員運算符。介紹成員運算符之前,我們需要提前了解一個概念:數據結構,如字符串、列表、元組、字典。在接下來的文章中我們將詳細介紹這些基礎的數據結構。字符串、列表、字典,它們就像一個集合,其中包含若干元素,這些元素就是集合的成員;對於一個給定的元素,它有可能在一個給定的集合中,也可能不在,Python 中採用成員運算符來判斷元素是否屬於成員,成員運算的結果為布爾值,True 或者 False。 看下面的代碼:

#初始化字符串和列表
temp1 = "abcdefg"
temp2 = [4,2,3,5,8,9]
a = "cdf"
b = 5
c = "cde"
print("a in temp1?", a in temp1)
print("b in temp2?", b in temp2)
print("c in temp1?", c in temp1)

  執行結果如下圖所示:    

 

 

 

 

 

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

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

網動廣告出品的網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上她。

居然還有人這樣解說mybatis運行原理_台中搬家

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

目錄

  • Mybatis基本認識
    • 動態代理
      • JDK實現
      • CGLIB動態代理
      • 總結
    • 反射
  • Configuration對象作用
  • 映射器結構
  • sqlsession執行流程(源碼跟蹤)
    • Executor
    • StatementHandler
    • 結果處理器(ResultSetHandler)
    • 總結
  • 主題

mybatis運行分為兩部分,第一部分讀取配置文件緩存到Configuration對象中。用以創建SqlSessionFactory,第二部分是SqlSession的執行過程。

Mybatis基本認識

動態代理

  • 之前我們知道Mapper僅僅是一個接口,而不是一個邏輯實現類。但是在Java中接口是無法執行邏輯的。這裏Mybatis就是通過動態代理實現的。關於動態代理我們常用的有Jdk動態代理和cglib動態代理。兩種卻別這裏不做贅述。關於CGLIB代理在框架中使用的比較多。

  • 關於動態代理就是所有的請求有一個入口,由這個入口進行分發。在開發領域的一個用途就是【負載均衡】

  • 關於Mybatis的動態代理是使用了兩種的結合。

  • 下面看看JDK和cglib兩種實現

JDK實現

  • 首先我們需要提供一個接口 , 這個接口是對我們程序員的一個抽象。 擁有編碼和改BUG的本領

public interface Developer {

    /**
     * 編碼
     */
    void code();

    /**
     * 解決問題
     */
    void debug();
}

  • 關於這兩種本領每個人處理方式不同。這裏我們需要一個具體的實例對象

public class JavaDeveloper implements Developer {
    @Override
    public void code() {
        System.out.println("java code");
    }

    @Override
    public void debug() {
        System.out.println("java debug");
    }
}

  • 我們傳統的調用方式是通過java提供的new 機制創造一個JavaDeveloper對象出來。而通過動態代理是通過java.lang.reflect.Proxy對象創建對象調用實際方法的。

  • 通過newProxyInstance方法獲取接口對象的。而這個方法需要三個參數
    ClassLoader loader : 通過實際接口實例對象獲取ClassLoader
    Class<?>[] interfaces : 我們抽象的接口
    InvocationHandler h : 對我們接口對象方法的調用。在調用節點我們可以進行我們的業務攔截


JavaDeveloper jDeveloper = new JavaDeveloper();
Developer developer = (Developer) Proxy.newProxyInstance(jDeveloper.getClass().getClassLoader(), jDeveloper.getClass().getInterfaces(), (proxy, method, params) -> {
    if (method.getName().equals("code")) {
        System.out.println("我是一個特殊的人,code之前先分析問題");
        return method.invoke(jDeveloper, params);
    }
    if (method.getName().equals("debug")) {
        System.out.println("我沒有bug");

    }
    return null;
});
developer.code();
developer.debug();

CGLIB動態代理

  • cglib動態代理優點在於他不需要我們提前準備接口。他代理的實際的對象。這對於我們開發來說就很方便了。

public class HelloService {
    public HelloService() {
        System.out.println("HelloService構造");
    }

    final public String sayHello(String name) {
        System.out.println("HelloService:sayOthers>>"+name);
        return null;
    }

    public void sayHello() {
        System.out.println("HelloService:sayHello");
    }
}

  • 下面我們只需要實現cglib提供的MethodInterceptor接口,在初始化設置cglib的時候加載這個實例化對象就可以了

public class MyMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("======插入前置通知======");
        Object object = methodProxy.invokeSuper(o, objects);
        System.out.println("======插入後者通知======");
        return object;
    }
}

  • 下面我們就來初始化設置cglib

public static void main(String[] args) {
    //代理類class文件存入本地磁盤方便我們反編譯查看源代碼
    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/root/code");
    //通過CGLIB動態代理獲取代理對象過程
    Enhancer enhancer = new Enhancer();
    //設置enhancer對象的父類
    enhancer.setSuperclass(HelloService.class);
    // 設置enhancer的回調對象
    enhancer.setCallback(new MyMethodInterceptor());
    //創建代理對象
    HelloService helloService = (HelloService) enhancer.create();
    //通過代理對象調用目標方法
    helloService.sayHello();
}

  • 仔細看看cglib和spring的aop特別像。針對切點進行切面攔截控制。

總結

  • 通過對比兩種動態代理我們很容易發現,mybatis就是通過JDK代理實現Mapper調用的。我們Mapper接口實現通過代理到xml中對應的sql執行邏輯

反射

  • 相信有一定經驗的Java工程師都對反射或多或少有一定了解。其實從思想上看不慣哪種語言都是有反射的機制的。
  • 通過反射我們就擺脫了對象的限制我們調用方法不再需要通過對象調用了。可以通過Class對象獲取方法對象。從而通過invoke方法進行方法的調用了。

Configuration對象作用

  • Configuration對象存儲了所有Mybatis的配置。主要初始化一下參數
    • properties
    • settings
    • typeAliases
    • typeHandler
    • ObjectFactory
    • plugins
    • environment
    • DatabaseIdProvider
    • Mapper映射器

映射器結構

  • BoundSql提供三個主要的屬性 parameterMappings 、parameterObject、sql

  • parameterObject參數本身。我們可以傳遞java基本類型、POJO、Map或者@Param標註的參數。

  • 當我們傳遞的是java基本類型mybatis會轉換成對應的包裝對象 int -> Integer

  • 如果我們傳遞POJO、Map。就是對象本身

  • 我們傳遞多個參數且沒有@Param指定變量名則parameterObject 類似
    {“1″:p1,”2″:p2,”param1″:p1,”param2”:p2}

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

    台中搬家公司推薦超過30年經驗,首選台中大展搬家

  • 我們傳遞多個參數且@Param指定變量名 則parameterObject類似
    {“key1″:p1,”key2″:p2,”param1″:p1,”param2”:p2}

  • parameterMapping 是記錄屬性、名稱、表達式、javaType,jdbcType、typeHandler這些信息

  • sql 屬性就是我們映射器中的一條sql. 正常我們在常見中對sql進行校驗。正常不需要修改sql。

sqlsession執行流程(源碼跟蹤)

  • 首先我們看看我們平時開發的Mapper接口是如何動態代理的。這就需要提到MapperProxyFactory這個類了。該類中的newInstance方法

protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  • 通過上滿代碼及上述對jdk動態代理的表述。我們可以知道mapperProxy是我們代理的重點。
  • MapperProxy是InvocationHandler的實現類。他重寫的invoke方法就是代理對象執行的方法入口。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
    if (Object.class.equals(method.getDeclaringClass())) {
    return method.invoke(this, args);
    } else if (isDefaultMethod(method)) {
    return invokeDefaultMethod(proxy, method, args);
    }
} catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}


private boolean isDefaultMethod(Method method) {
return (method.getModifiers()
    & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC
    && method.getDeclaringClass().isInterface();
}

  • 通過源碼發現。invoke內部首先判斷對象是否是類 。 通過打斷點發現最終會走到cacheMapperMethod這個方法去創建MapperMethod對象。
  • 繼續查看MapperMethod中execute方法我們可以了解到內部實現其實是一個命令行模式開發。通過判斷命令從而執行不同的語句。判斷到具體執行語句然後將參數傳遞給sqlsession進行sql調用並獲取結果。到了sqlsession就和正常jdbc開發sql進行關聯了。sqlsession中ExecutorStatementHandlerParameterHandlerResulthandler四大天王

Executor

  • 顧名思義他就是一個執行器。將java提供的sql提交到數據庫。Mybatis提供了三種執行器。

  • Configuration.classnewExecutor源碼

  • 根據uml我們不難看出mybatis中提供了三類執行器分別SimpleExecutor、ReuseExecutor、BatchExecutor

public SqlSession openSession() {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      // 得到configuration 中的environment
      final Environment environment = configuration.getEnvironment();
      // 得到configuration 中的事務工廠
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 獲取執行器
      final Executor executor = configuration.newExecutor(tx, execType);
      // 返回默認的SqlSession
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

  • 通過上述源碼我們知道在sqlsession獲取一個數據庫session對象時我們或根據我們的settings配置加載一個Executor對象。在settings中配置也很簡單

<settings>
<!--取值範圍 SIMPLE, REUSE, BATCH -->
	<setting name="defaultExecutorType" value="SIMPLE"/>
</settings>

  • 我們也可以通過java代碼設置

factory.openSession(ExecutorType.BATCH);

StatementHandler

  • 顧名思義,StatementHandler就是專門處理數據庫回話的。這個對象的創建還是在Configuration中管理的。

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

  • 很明顯Mybatis中StatementHandler使用的是RoutingStatementHandler這個class
  • 關於StatementHandler和RoutingStatementHandler之間的關係我們通過源碼可以看出這裏和Executor一樣都是適配器模式。採用這種模式的好處是方便我們對這些對象進行代理。這裏讀者可以猜測一下是使用了哪種動態代理。給點提示 這裏使用了接口哦

  • 在查看BaseStatementHandler結構我們會發現和Executor一模一樣。同樣的Mybatis在構造RoutingStatementHandler的時候會根據setting中配置來加載不同的具體子類。這些子類都是繼承了BaseStatementHandler.

  • 前一節我們跟蹤了Executor。 我們知道Mybatis默認的是SimpleExecutor。 StatementHandler我們跟蹤了Mybaits默認的是PrePareStatementHandler。在SimpleExecutor執行查詢的源碼如下

  • 我們發現在executor查詢錢會先讓statementHandler構建一個Statement對象。最終就是StatementHandler中prepare方法。這個方法在抽象類BaseStatmentHandler中已經封裝好了。
  • 這個方法的邏輯是初始化statement和設置連接超時等一些輔助作用
  • 然後就是設置一些參數等設置。最後就走到了執行器executor的doquery
  • PrepareStatement在我們jdbc開發時是常見的一個類 。 這個方法執行execute前我們需要設置sql語句,設置參數進行編譯。這一系列步驟就是剛才我們說的流程也是PrepareStatementHandler.prepareStatement幫我們做的事情。那麼剩下的我們也很容易想到就是我們對數據結果的封裝。正如代碼所示下馬就是resultSetHandler幫我們做事情了。

結果處理器(ResultSetHandler)


@Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }

    return collapseSingleResultList(multipleResults);
  }

  • 這個方法我們可以導出來是結果xml中標籤配置對結果的一個封裝。

總結

  • SqlSession在一個查詢開啟的時候會先通過CacheExecutor查詢緩存。擊穿緩存後會通過BaseExector子類的SimpleExecutor創建StatementHandler。PrepareStatementHandler會基於PrepareStament執行數據庫操作。並針對返回結果通過ResultSetHandler返回結果數據

主題

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

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

萬字超強圖文講解AQS以及ReentrantLock應用(建議收藏)_台中搬家公司

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

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

| 好看請贊,養成習慣

  • 你有一個思想,我有一個思想,我們交換后,一個人就有兩個思想

  • If you can NOT explain it simply, you do NOT understand it well enough

現陸續將Demo代碼和技術文章整理在一起 Github實踐精選 ,方便大家閱讀查看,本文同樣收錄在此,覺得不錯,還請Star

寫在前面

進入源碼階段了,寫了十幾篇的 併發系列 知識鋪墊終於要派上用場了。相信很多人已經忘了其中的一些理論知識,別擔心,我會在源碼環節帶入相應的理論知識點幫助大家回憶,做到理論與實踐相結合,另外這是超長圖文,建議收藏,如果對你有用還請點贊讓更多人看到

Java SDK 為什麼要設計 Lock

曾幾何時幻想過,如果 Java 併發控制只有 synchronized 多好,只有下面三種使用方式,簡單方便

public class ThreeSync {

	private static final Object object = new Object();

	public synchronized void normalSyncMethod(){
		//臨界區
	}

	public static synchronized void staticSyncMethod(){
		//臨界區
	}

	public void syncBlockMethod(){
		synchronized (object){
			//臨界區
		}
	}
}

如果在 Java 1.5之前,確實是這樣,自從 1.5 版本 Doug Lea 大師就重新造了一個輪子 Lock

我們常說:“避免重複造輪子”,如果有了輪子還是要堅持再造個輪子,那麼肯定傳統的輪子在某些應用場景中不能很好的解決問題

不知你是否還記得 Coffman 總結的四個可以發生死鎖的情形 ,其中【不可剝奪條件】是指:

線程已經獲得資源,在未使用完之前,不能被剝奪,只能在使用完時自己釋放

要想破壞這個條件,就需要具有申請不到進一步資源就釋放已有資源的能力

很顯然,這個能力是 synchronized 不具備的,使用 synchronized ,如果線程申請不到資源就會進入阻塞狀態,我們做什麼也改變不了它的狀態,這是 synchronized 輪子的致命弱點,這就強有力的給了重造輪子 Lock 的理由

顯式鎖 Lock

舊輪子有弱點,新輪子就要解決這些問題,所以要具備不會阻塞的功能,下面的三個方案都是解決這個問題的好辦法(看下面表格描述你就明白三個方案的含義了)

特性 描述 API
能響應中斷 如果不能自己釋放,那可以響應中斷也是很好的。Java多線程中斷機制 專門描述了中斷過程,目的是通過中斷信號來跳出某種狀態,比如阻塞 lockInterruptbly()
非阻塞式的獲取鎖 嘗試獲取,獲取不到不會阻塞,直接返回 tryLock()
支持超時 給定一個時間限制,如果一段時間內沒獲取到,不是進入阻塞狀態,同樣直接返回 tryLock(long time, timeUnit)

好的方案有了,但魚和熊掌不可兼得,Lock 多了 synchronized 不具備的特性,自然不會像 synchronized 那樣一個關鍵字三個玩法走遍全天下,在使用上也相對複雜了一丟丟

Lock 使用範式

synchronized 有標準用法,這樣的優良傳統咱 Lock 也得有,相信很多人都知道使用 Lock 的一個範式

Lock lock = new ReentrantLock();
lock.lock();
try{
	...
}finally{
	lock.unlock();
}

既然是範式(沒事不要挑戰更改寫法的那種),肯定有其理由,我們來看一下

標準1—finally 中釋放鎖

這個大家應該都會明白,在 finally 中釋放鎖,目的是保證在獲取到鎖之後,最終能被釋放

標準2—在 try{} 外面獲取鎖

不知道你有沒有想過,為什麼會有標準 2 的存在,我們通常是“喜歡” try 住所有內容,生怕發生異常不能捕獲的

try{} 外獲取鎖主要考慮兩個方面:

  1. 如果沒有獲取到鎖就拋出異常,最終釋放鎖肯定是有問題的,因為還未曾擁有鎖談何釋放鎖呢
  2. 如果在獲取鎖時拋出了異常,也就是當前線程並未獲取到鎖,但執行到 finally 代碼時,如果恰巧別的線程獲取到了鎖,則會被釋放掉(無故釋放)

不同鎖的實現方式略有不同,範式的存在就是要避免一切問題的出現,所以大家盡量遵守範式

Lock 是怎樣起到鎖的作用呢?

如果你熟悉 synchronized,你知道程序編譯成 CPU 指令后,在臨界區會有 moniterentermoniterexit 指令的出現,可以理解成進出臨界區的標識

從範式上來看:

  • lock.lock() 獲取鎖,“等同於” synchronized 的 moniterenter指令

  • lock.unlock() 釋放鎖,“等同於” synchronized 的 moniterexit 指令

那 Lock 是怎麼做到的呢?

這裏先簡單說明一下,這樣一會到源碼分析時,你可以遠觀設計輪廓,近觀實現細節,會變得越發輕鬆

其實很簡單,比如在 ReentrantLock 內部維護了一個 volatile 修飾的變量 state,通過 CAS 來進行讀寫(最底層還是交給硬件來保證原子性和可見性),如果CAS更改成功,即獲取到鎖,線程進入到 try 代碼塊繼續執行;如果沒有更改成功,線程會被【掛起】,不會向下執行

但 Lock 是一個接口,裏面根本沒有 state 這個變量的存在:

它怎麼處理這個 state 呢?很顯然需要一點設計的加成了,接口定義行為,具體都是需要實現類的

Lock 接口的實現類基本都是通過【聚合】了一個【隊列同步器】的子類完成線程訪問控制的

那什麼是隊列同步器呢? (這應該是你見過的最強標題黨,聊了半個世紀才入正題,評論區留言罵我)

隊列同步器 AQS

隊列同步器 (AbstractQueuedSynchronizer),簡稱同步器或AQS,就是我們今天的主人公

問:為什麼你分析 JUC 源碼,要從 AQS 說起呢?

答:看下圖

相信看到這個截圖你就明白一二了,你聽過的,面試常被問起的,工作中常用的

  • ReentrantLock
  • ReentrantReadWriteLock
  • Semaphore(信號量)
  • CountDownLatch
  • 公平鎖
  • 非公平鎖
  • ThreadPoolExecutor (關於線程池的理解,可以查看 為什麼要使用線程池? )

都和 AQS 有直接關係,所以了解 AQS 的抽象實現,在此基礎上再稍稍查看上述各類的實現細節,很快就可以全部搞定,不至於查看源碼時一頭霧水,丟失主線

上面提到,在鎖的實現類中會聚合同步器,然後利同步器實現鎖的語義,那麼問題來了:

為什麼要用聚合模式,怎麼進一步理解鎖和同步器的關係呢?

我們絕大多數都是在使用鎖,實現鎖之後,其核心就是要使用方便

從 AQS 的類名稱和修飾上來看,這是一個抽象類,所以從設計模式的角度來看同步器一定是基於【模版模式】來設計的,使用者需要繼承同步器,實現自定義同步器,並重寫指定方法,隨後將同步器組合在自定義的同步組件中,並調用同步器的模版方法,而這些模版方法又回調用使用者重寫的方法

我不想將上面的解釋說的這麼抽象,其實想理解上面這句話,我們只需要知道下面兩個問題就好了

  1. 哪些是自定義同步器可重寫的方法?
  2. 哪些是抽象同步器提供的模版方法?

同步器可重寫的方法

同步器提供的可重寫方法只有5個,這大大方便了鎖的使用者:

按理說,需要重寫的方法也應該有 abstract 來修飾的,為什麼這裏沒有?原因其實很簡單,上面的方法我已經用顏色區分成了兩類:

  • 獨佔式
  • 共享式

自定義的同步組件或者鎖不可能既是獨佔式又是共享式,為了避免強制重寫不相干方法,所以就沒有 abstract 來修飾了,但要拋出異常告知不能直接使用該方法:

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

暖暖的很貼心(如果你有類似的需求也可以仿照這樣的設計)

表格方法描述中所說的同步狀態就是上文提到的有 volatile 修飾的 state,所以我們在重寫上面幾個方法時,還要通過同步器提供的下面三個方法(AQS 提供的)來獲取或修改同步狀態:

而獨佔式和共享式操作 state 變量的區別也就很簡單了

所以你看到的 ReentrantLock ReentrantReadWriteLock Semaphore(信號量) CountDownLatch 這幾個類其實僅僅是在實現以上幾個方法上略有差別,其他的實現都是通過同步器的模版方法來實現的,到這裡是不是心情放鬆了許多呢?我們來看一看模版方法:

同步器提供的模版方法

上面我們將同步器的實現方法分為獨佔式和共享式兩類,模版方法其實除了提供以上兩類模版方法之外,只是多了響應中斷超時限制 的模版方法供 Lock 使用,來看一下

先不用記上述方法的功能,目前你只需要了解個大概功能就好。另外,相信你也注意到了:

上面的方法都有 final 關鍵字修飾,說明子類不能重寫這個方法

看到這你也許有點亂了,我們稍微歸納一下:

程序員還是看代碼心裏踏實一點,我們再來用代碼說明一下上面的關係(注意代碼中的註釋,以下的代碼並不是很嚴謹,只是為了簡單說明上圖的代碼實現):

package top.dayarch.myjuc;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 自定義互斥鎖
 *
 * @author tanrgyb
 * @date 2020/5/23 9:33 PM
 */
public class MyMutex implements Lock {

	// 靜態內部類-自定義同步器
	private static class MySync extends AbstractQueuedSynchronizer{
		@Override
		protected boolean tryAcquire(int arg) {
			// 調用AQS提供的方法,通過CAS保證原子性
			if (compareAndSetState(0, arg)){
				// 我們實現的是互斥鎖,所以標記獲取到同步狀態(更新state成功)的線程,
				// 主要為了判斷是否可重入(一會兒會說明)
				setExclusiveOwnerThread(Thread.currentThread());
				//獲取同步狀態成功,返回 true
				return true;
			}
			// 獲取同步狀態失敗,返回 false
			return false;
		}

		@Override
		protected boolean tryRelease(int arg) {
			// 未擁有鎖卻讓釋放,會拋出IMSE
			if (getState() == 0){
				throw new IllegalMonitorStateException();
			}
			// 可以釋放,清空排它線程標記
			setExclusiveOwnerThread(null);
			// 設置同步狀態為0,表示釋放鎖
			setState(0);
			return true;
		}

		// 是否獨佔式持有
		@Override
		protected boolean isHeldExclusively() {
			return getState() == 1;
		}

		// 後續會用到,主要用於等待/通知機制,每個condition都有一個與之對應的條件等待隊列,在鎖模型中說明過
		Condition newCondition() {
			return new ConditionObject();
		}
	}

  // 聚合自定義同步器
	private final MySync sync = new MySync();


	@Override
	public void lock() {
		// 阻塞式的獲取鎖,調用同步器模版方法獨佔式,獲取同步狀態
		sync.acquire(1);
	}

	@Override
	public void lockInterruptibly() throws InterruptedException {
		// 調用同步器模版方法可中斷式獲取同步狀態
		sync.acquireInterruptibly(1);
	}

	@Override
	public boolean tryLock() {
		// 調用自己重寫的方法,非阻塞式的獲取同步狀態
		return sync.tryAcquire(1);
	}

	@Override
	public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
		// 調用同步器模版方法,可響應中斷和超時時間限制
		return sync.tryAcquireNanos(1, unit.toNanos(time));
	}

	@Override
	public void unlock() {
		// 釋放鎖
		sync.release(1);
	}

	@Override
	public Condition newCondition() {
		// 使用自定義的條件
		return sync.newCondition();
	}
}

如果你現在打開 IDE, 你會發現上文提到的 ReentrantLock ReentrantReadWriteLock Semaphore(信號量) CountDownLatch 都是按照這個結構實現,所以我們就來看一看 AQS 的模版方法到底是怎麼實現鎖

AQS實現分析

從上面的代碼中,你應該理解了lock.tryLock() 非阻塞式獲取鎖就是調用自定義同步器重寫的 tryAcquire() 方法,通過 CAS 設置state 狀態,不管成功與否都會馬上返回;那麼 lock.lock() 這種阻塞式的鎖是如何實現的呢?

有阻塞就需要排隊,實現排隊必然需要隊列

CLH:Craig、Landin and Hagersten 隊列,是一個單向鏈表,AQS中的隊列是CLH變體的虛擬雙向隊列(FIFO)——概念了解就好,不要記

隊列中每個排隊的個體就是一個 Node,所以我們來看一下 Node 的結構

Node 節點

AQS 內部維護了一個同步隊列,用於管理同步狀態。

  • 當線程獲取同步狀態失敗時,就會將當前線程以及等待狀態等信息構造成一個 Node 節點,將其加入到同步隊列中尾部,阻塞該線程
  • 當同步狀態被釋放時,會喚醒同步隊列中“首節點”的線程獲取同步狀態

為了將上述步驟弄清楚,我們需要來看一看 Node 結構 (如果你能打開 IDE 一起看那是極好的)

乍一看有點雜亂,我們還是將其歸類說明一下:

上面這幾個狀態說明有個印象就好,有了Node 的結構說明鋪墊,你也就能想象同步隊列的接本結構了:

前置知識基本鋪墊完畢,我們來看一看獨佔式獲取同步狀態的整個過程

獨佔式獲取同步狀態

故事要從範式lock.lock() 開始

public void lock() {
	// 阻塞式的獲取鎖,調用同步器模版方法,獲取同步狀態
	sync.acquire(1);
}

進入AQS的模版方法 acquire()

public final void acquire(int arg) {
  // 調用自定義同步器重寫的 tryAcquire 方法
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

首先,也會嘗試非阻塞的獲取同步狀態,如果獲取失敗(tryAcquire返回false),則會調用 addWaiter 方法構造 Node 節點(Node.EXCLUSIVE 獨佔式)並安全的(CAS)加入到同步隊列【尾部】

    private Node addWaiter(Node mode) {
      	// 構造Node節點,包含當前線程信息以及節點模式【獨佔/共享】
        Node node = new Node(Thread.currentThread(), mode);
      	// 新建變量 pred 將指針指向tail指向的節點
        Node pred = tail;
      	// 如果尾節點不為空
        if (pred != null) {
          	// 新加入的節點前驅節點指向尾節點
            node.prev = pred;

          	// 因為如果多個線程同時獲取同步狀態失敗都會執行這段代碼
            // 所以,通過 CAS 方式確保安全的設置當前節點為最新的尾節點
            if (compareAndSetTail(pred, node)) {
              	// 曾經的尾節點的後繼節點指向當前節點
                pred.next = node;
              	// 返回新構建的節點
                return node;
            }
        }
      	// 尾節點為空,說明當前節點是第一個被加入到同步隊列中的節點
      	// 需要一個入隊操作
        enq(node);
        return node;
    }

    private Node enq(final Node node) {
      	// 通過“死循環”確保節點被正確添加,最終將其設置為尾節點之後才會返回,這裏使用 CAS 的理由和上面一樣
        for (;;) {
            Node t = tail;
          	// 第一次循環,如果尾節點為 null
            if (t == null) { // Must initialize
              	// 構建一個哨兵節點,並將頭部指針指向它
                if (compareAndSetHead(new Node()))
                  	// 尾部指針同樣指向哨兵節點
                    tail = head;
            } else {
              	// 第二次循環,將新節點的前驅節點指向t
                node.prev = t;
              	// 將新節點加入到隊列尾節點
                if (compareAndSetTail(t, node)) {
                  	// 前驅節點的後繼節點指向當前新節點,完成雙向隊列
                    t.next = node;
                    return t;
                }
            }
        }
    }

你可能比較迷惑 enq() 的處理方式,進入該方法就是一個“死循環”,我們就用圖來描述它是怎樣跳出循環的

有些同學可能會有疑問,為什麼會有哨兵節點?

哨兵,顧名思義,是用來解決國家之間邊界問題的,不直接參与生產活動。同樣,計算機科學中提到的哨兵,也用來解決邊界問題,如果沒有邊界,指定環節,按照同樣算法可能會在邊界處發生異常,比如要繼續向下分析的 acquireQueued() 方法

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
          	// "死循環",嘗試獲取鎖,或者掛起
            for (;;) {
              	// 獲取當前節點的前驅節點
                final Node p = node.predecessor();
              	// 只有當前節點的前驅節點是頭節點,才會嘗試獲取鎖
              	// 看到這你應該理解添加哨兵節點的含義了吧
                if (p == head && tryAcquire(arg)) {
                  	// 獲取同步狀態成功,將自己設置為頭
                    setHead(node);
                  	// 將哨兵節點的後繼節點置為空,方便GC
                    p.next = null; // help GC
                    failed = false;
                  	// 返回中斷標識
                    return interrupted;
                }
              	// 當前節點的前驅節點不是頭節點
              	//【或者】當前節點的前驅節點是頭節點但獲取同步狀態失敗
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

獲取同步狀態成功會返回可以理解了,但是如果失敗就會一直陷入到“死循環”中浪費資源嗎?很顯然不是,shouldParkAfterFailedAcquire(p, node)parkAndCheckInterrupt() 就會將線程獲取同步狀態失敗的線程掛起,我們繼續向下看

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
      	// 獲取前驅節點的狀態
        int ws = pred.waitStatus;
      	// 如果是 SIGNAL 狀態,即等待被佔用的資源釋放,直接返回 true
      	// 準備繼續調用 parkAndCheckInterrupt 方法
        if (ws == Node.SIGNAL)
            return true;
      	// ws 大於0說明是CANCELLED狀態,
        if (ws > 0) {
            // 循環判斷前驅節點的前驅節點是否也為CANCELLED狀態,忽略該狀態的節點,重新連接隊列
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
          	// 將當前節點的前驅節點設置為設置為 SIGNAL 狀態,用於後續喚醒操作
          	// 程序第一次執行到這返回為false,還會進行外層第二次循環,最終從代碼第7行返回
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

到這裏你也許有個問題:

這個地方設置前驅節點為 SIGNAL 狀態到底有什麼作用?

保留這個問題,我們陸續揭曉

如果前驅節點的 waitStatus 是 SIGNAL狀態,即 shouldParkAfterFailedAcquire 方法會返回 true ,程序會繼續向下執行 parkAndCheckInterrupt 方法,用於將當前線程掛起

    private final boolean parkAndCheckInterrupt() {
      	// 線程掛起,程序不會繼續向下執行
        LockSupport.park(this);
      	// 根據 park 方法 API描述,程序在下述三種情況會繼續向下執行
      	// 	1. 被 unpark 
      	// 	2. 被中斷(interrupt)
      	// 	3. 其他不合邏輯的返回才會繼續向下執行
      	
      	// 因上述三種情況程序執行至此,返回當前線程的中斷狀態,並清空中斷狀態
      	// 如果由於被中斷,該方法會返回 true
        return Thread.interrupted();
    }

被喚醒的程序會繼續執行 acquireQueued 方法里的循環,如果獲取同步狀態成功,則會返回 interrupted = true 的結果

程序繼續向調用棧上層返回,最終回到 AQS 的模版方法 acquire

public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

你也許會有疑惑:

程序已經成功獲取到同步狀態並返回了,怎麼會有個自我中斷呢?

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

如果你不能理解中斷,強烈建議你回看 Java多線程中斷機制

到這裏關於獲取同步狀態我們還遺漏了一條線,acquireQueued 的 finally 代碼塊如果你仔細看你也許馬上就會有疑惑:

到底什麼情況才會執行 if(failed) 裏面的代碼 ?

if (failed)
  cancelAcquire(node);

這段代碼被執行的條件是 failed 為 true,正常情況下,如果跳出循環,failed 的值為false,如果不能跳出循環貌似怎麼也不能執行到這裏,所以只有不正常的情況才會執行到這裏,也就是會發生異常,才會執行到此處

查看 try 代碼塊,只有兩個方法會拋出異常:

  • node.processor() 方法

    台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

    還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

  • 自己重寫的 tryAcquire() 方法

先看前者:

很顯然,這裏拋出的異常不是重點,那就以 ReentrantLock 重寫的 tryAcquire() 方法為例

另外,上面分析 shouldParkAfterFailedAcquire 方法還對 CANCELLED 的狀態進行了判斷,那麼

什麼時候會生成取消狀態的節點呢?

答案就在 cancelAcquire 方法中, 我們來看看 cancelAcquire到底怎麼設置/處理 CANNELLED 的

	private void cancelAcquire(Node node) {
        // 忽略無效節點
        if (node == null)
            return;
				// 將關聯的線程信息清空
        node.thread = null;

        // 跳過同樣是取消狀態的前驅節點
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // 跳出上面循環后找到前驅有效節點,並獲取該有效節點的後繼節點
        Node predNext = pred.next;

        // 將當前節點的狀態置為 CANCELLED
        node.waitStatus = Node.CANCELLED;

        // 如果當前節點處在尾節點,直接從隊列中刪除自己就好
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            int ws;
          	// 1. 如果當前節點的有效前驅節點不是頭節點,也就是說當前節點不是頭節點的後繼節點
            if (pred != head &&
                // 2. 判斷當前節點有效前驅節點的狀態是否為 SIGNAL
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 // 3. 如果不是,嘗試將前驅節點的狀態置為 SIGNAL
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                // 判斷當前節點有效前驅節點的線程信息是否為空
                pred.thread != null) {
              	// 上述條件滿足
                Node next = node.next;
              	// 將當前節點有效前驅節點的後繼節點指針指向當前節點的後繼節點
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
              	// 如果當前節點的前驅節點是頭節點,或者上述其他條件不滿足,就喚醒當前節點的後繼節點
                unparkSuccessor(node);
            }
						
            node.next = node; // help GC
        }

看到這個註釋你可能有些亂了,其核心目的就是從等待隊列中移除 CANCELLED 的節點,並重新拼接整個隊列,總結來看,其實設置 CANCELLED 狀態節點只是有三種情況,我們通過畫圖來分析一下:

至此,獲取同步狀態的過程就結束了,我們簡單的用流程圖說明一下整個過程

獲取鎖的過程就這樣的結束了,先暫停幾分鐘整理一下自己的思路。我們上面還沒有說明 SIGNAL 的作用, SIGNAL 狀態信號到底是干什麼用的?這就涉及到鎖的釋放了,我們來繼續了解,整體思路和鎖的獲取是一樣的, 但是釋放過程就相對簡單很多了

獨佔式釋放同步狀態

故事要從 unlock() 方法說起

	public void unlock() {
		// 釋放鎖
		sync.release(1);
	}

調用 AQS 模版方法 release,進入該方法

    public final boolean release(int arg) {
      	// 調用自定義同步器重寫的 tryRelease 方法嘗試釋放同步狀態
        if (tryRelease(arg)) {
          	// 釋放成功,獲取頭節點
            Node h = head;
          	// 存在頭節點,並且waitStatus不是初始狀態
          	// 通過獲取的過程我們已經分析了,在獲取的過程中會將 waitStatus的值從初始狀態更新成 SIGNAL 狀態
            if (h != null && h.waitStatus != 0)
              	// 解除線程掛起狀態
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

查看 unparkSuccessor 方法,實際是要喚醒頭節點的後繼節點

    private void unparkSuccessor(Node node) {      
      	// 獲取頭節點的waitStatus
        int ws = node.waitStatus;
        if (ws < 0)
          	// 清空頭節點的waitStatus值,即置為0
            compareAndSetWaitStatus(node, ws, 0);
      
      	// 獲取頭節點的後繼節點
        Node s = node.next;
      	// 判斷當前節點的後繼節點是否是取消狀態,如果是,需要移除,重新連接隊列
        if (s == null || s.waitStatus > 0) {
            s = null;
          	// 從尾節點向前查找,找到隊列第一個waitStatus狀態小於0的節點
            for (Node t = tail; t != null && t != node; t = t.prev)
              	// 如果是獨佔式,這裏小於0,其實就是 SIGNAL
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
          	// 解除線程掛起狀態
            LockSupport.unpark(s.thread);
    }

有同學可能有疑問:

為什麼這個地方是從隊列尾部向前查找不是 CANCELLED 的節點?

原因有兩個:

第一,先回看節點加入隊列的情景:

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

節點入隊並不是原子操作,代碼第6、7行

node.prev = pred; 
compareAndSetTail(pred, node) 

這兩個地方可以看作是尾節點入隊的原子操作,如果此時代碼還沒執行到 pred.next = node; 這時又恰巧執行了unparkSuccessor方法,就沒辦法從前往後找了,因為後繼指針還沒有連接起來,所以需要從后往前找

第二點原因,在上面圖解產生 CANCELLED 狀態節點的時候,先斷開的是 Next 指針,Prev指針並未斷開,因此這也是必須要從后往前遍歷才能夠遍歷完全部的Node

同步狀態至此就已經成功釋放了,之前獲取同步狀態被掛起的線程就會被喚醒,繼續從下面代碼第 3 行返回執行:

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

繼續返回上層調用棧, 從下面代碼15行開始執行,重新執行循環,再次嘗試獲取同步狀態

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

到這裏,關於獨佔式獲取/釋放鎖的流程已經閉環了,但是關於 AQS 的另外兩個模版方法還沒有介紹

  • 響應中斷
  • 超時限制

獨佔式響應中斷獲取同步狀態

故事要從lock.lockInterruptibly() 方法說起

	public void lockInterruptibly() throws InterruptedException {
		// 調用同步器模版方法可中斷式獲取同步狀態
		sync.acquireInterruptibly(1);
	}

有了前面的理解,理解獨佔式可響應中斷的獲取同步狀態方式,真是一眼就能明白了:

    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
      	// 嘗試非阻塞式獲取同步狀態失敗,如果沒有獲取到同步狀態,執行代碼7行
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

繼續查看 doAcquireInterruptibly 方法:

    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                  	// 獲取中斷信號后,不再返回 interrupted = true 的值,而是直接拋出 InterruptedException 
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

沒想到 JDK 內部也有如此相近的代碼,可響應中斷獲取鎖沒什麼深奧的,就是被中斷拋出 InterruptedException 異常(代碼第17行),這樣就逐層返回上層調用棧捕獲該異常進行下一步操作了

趁熱打鐵,來看看另外一個模版方法:

獨佔式超時限制獲取同步狀態

這個很好理解,就是給定一個時限,在該時間段內獲取到同步狀態,就返回 true, 否則,返回 false。好比線程給自己定了一個鬧鐘,鬧鈴一響,線程就自己返回了,這就不會使自己是阻塞狀態了

既然涉及到超時限制,其核心邏輯肯定是計算時間間隔,因為在超時時間內,肯定是多次嘗試獲取鎖的,每次獲取鎖肯定有時間消耗,所以計算時間間隔的邏輯就像我們在程序打印程序耗時 log 那麼簡單

nanosTimeout = deadline – System.nanoTime()

故事要從 lock.tryLock(time, unit) 方法說起

	public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
		// 調用同步器模版方法,可響應中斷和超時時間限制
		return sync.tryAcquireNanos(1, unit.toNanos(time));
	}

來看 tryAcquireNanos 方法

    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

是不是和上面 acquireInterruptibly 方法長相很詳細了,繼續查看來 doAcquireNanos 方法,看程序, 該方法也是 throws InterruptedException,我們在中斷文章中說過,方法標記上有 throws InterruptedException 說明該方法也是可以響應中斷的,所以你可以理解超時限制是 acquireInterruptibly 方法的加強版,具有超時和非阻塞控制的雙保險

    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
      	// 超時時間內,為獲取到同步狀態,直接返回false
        if (nanosTimeout <= 0L)
            return false;
      	// 計算超時截止時間
        final long deadline = System.nanoTime() + nanosTimeout;
      	// 以獨佔方式加入到同步隊列中
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
              	// 計算新的超時時間
                nanosTimeout = deadline - System.nanoTime();
              	// 如果超時,直接返回 false
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                		// 判斷是最新超時時間是否大於閾值 1000    
                    nanosTimeout > spinForTimeoutThreshold)
                  	// 掛起線程 nanosTimeout 長時間,時間到,自動返回
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

上面的方法應該不是很難懂,但是又同學可能在第 27 行上有所困惑

為什麼 nanosTimeout 和 自旋超時閾值1000進行比較?

    /**
     * The number of nanoseconds for which it is faster to spin
     * rather than to use timed park. A rough estimate suffices
     * to improve responsiveness with very short timeouts.
     */
    static final long spinForTimeoutThreshold = 1000L;

其實 doc 說的很清楚,說白了,1000 nanoseconds 時間已經非常非常短暫了,沒必要再執行掛起和喚醒操作了,不如直接當前線程直接進入下一次循環

到這裏,我們自定義的 MyMutex 只差 Condition 沒有說明了,不知道你累了嗎?我還在堅持

Condition

如果你看過之前寫的 併發編程之等待通知機制 ,你應該對下面這個圖是有印象的:

如果當時你理解了這個模型,再看 Condition 的實現,根本就不是問題了,首先 Condition 還是一個接口,肯定也是需要有實現類的

那故事就從 lock.newnewCondition 說起吧

	public Condition newCondition() {
		// 使用自定義的條件
		return sync.newCondition();
	}

自定義同步器重封裝了該方法:

		Condition newCondition() {
			return new ConditionObject();
		}

ConditionObject 就是 Condition 的實現類,該類就定義在了 AQS 中,只有兩個成員變量:

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

所以,我們只需要來看一下 ConditionObject 實現的 await / signal 方法來使用這兩個成員變量就可以了

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
          	// 同樣構建 Node 節點,並加入到等待隊列中
            Node node = addConditionWaiter();
          	// 釋放同步狀態
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
              	// 掛起當前線程
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

這裏注意用詞,在介紹獲取同步狀態時,addWaiter 是加入到【同步隊列】,就是上圖說的入口等待隊列,這裏說的是【等待隊列】,所以 addConditionWaiter 肯定是構建了一個自己的隊列:

        private Node addConditionWaiter() {
            Node t = lastWaiter;
            
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
          	// 新構建的節點的 waitStatus 是 CONDITION,注意不是 0 或 SIGNAL 了
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
          	// 構建單向同步隊列
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

這裡有朋友可能會有疑問:

為什麼這裡是單向隊列,也沒有使用CAS 來保證加入隊列的安全性呢?

因為 await 是 Lock 範式 try 中使用的,說明已經獲取到鎖了,所以就沒必要使用 CAS 了,至於是單向,因為這裏還不涉及到競爭鎖,只是做一個條件等待隊列

在 Lock 中可以定義多個條件,每個條件都會對應一個 條件等待隊列,所以將上圖豐富說明一下就變成了這個樣子:

線程已經按相應的條件加入到了條件等待隊列中,那如何再嘗試獲取鎖呢?signal / signalAll 方法就已經排上用場了

        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

Signal 方法通過調用 doSignal 方法,只喚醒條件等待隊列中的第一個節點

        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
              	// 調用該方法,將條件等待隊列的線程節點移動到同步隊列中
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

繼續看 transferForSignal 方法

    final boolean transferForSignal(Node node) {       
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

       	// 重新進行入隊操作
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
          	// 喚醒同步隊列中該線程
            LockSupport.unpark(node.thread);
        return true;
    }

所以我們再用圖解一下喚醒的整個過程

到這裏,理解 signalAll 就非常簡單了,只不過循環判斷是否還有 nextWaiter,如果有就像 signal 操作一樣,將其從條件等待隊列中移到同步隊列中

        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }

不知你還是否記得,我在併發編程之等待通知機制 中還說過一句話

沒有特殊原因盡量用 signalAll 方法

什麼時候可以用 signal 方法也在其中做了說明,請大家自行查看吧

這裏我還要多說一個細節,從條件等待隊列移到同步隊列是有時間差的,所以使用 await() 方法也是範式的, 同樣在該文章中做了解釋

有時間差,就會有公平和不公平的問題,想要全面了解這個問題,我們就要走近 ReentrantLock 中來看了,除了了解公平/不公平問題,查看 ReentrantLock 的應用還是要反過來驗證它使用的AQS的,我們繼續吧

ReentrantLock 是如何應用的AQS

獨佔式的典型應用就是 ReentrantLock 了,我們來看看它是如何重寫這個方法的

乍一看挺奇怪的,怎麼裏面自定義了三個同步器:其實 NonfairSync,FairSync 只是對 Sync 做了進一步劃分:

從名稱上你應該也知道了,這就是你聽到過的 公平鎖/非公平鎖

何為公平鎖/非公平鎖?

生活中,排隊講求先來後到視為公平。程序中的公平性也是符合請求鎖的絕對時間的,其實就是 FIFO,否則視為不公平

我們來對比一下 ReentrantLock 是如何實現公平鎖和非公平鎖的

其實沒什麼大不了,公平鎖就是判斷同步隊列是否還有先驅節點的存在,只有沒有先驅節點才能獲取鎖;而非公平鎖是不管這個事的,能獲取到同步狀態就可以,就這麼簡單,那問題來了:

為什麼會有公平鎖/非公平鎖的設計?

考慮這個問題,我們需重新回憶上面的鎖獲取實現圖了,其實上面我已經透露了一點

主要有兩點原因:

原因一:

恢復掛起的線程到真正鎖的獲取還是有時間差的,從人類的角度來看這個時間微乎其微,但是從CPU的角度來看,這個時間差存在的還是很明顯的。所以非公平鎖能更充分的利用 CPU 的時間片,盡量減少 CPU 空閑狀態時間

原因二:

不知你是否還記得我在 面試問,創建多少個線程合適? 文章中反覆提到過,使用多線程很重要的考量點是線程切換的開銷,想象一下,如果採用非公平鎖,當一個線程請求鎖獲取同步狀態,然後釋放同步狀態,因為不需要考慮是否還有前驅節點,所以剛釋放鎖的線程在此刻再次獲取同步狀態的幾率就變得非常大,所以就減少了線程的開銷

相信到這裏,你也就明白了,為什麼 ReentrantLock 默認構造器用的是非公平鎖同步器

    public ReentrantLock() {
        sync = new NonfairSync();
    }

看到這裏,感覺非公平鎖 perfect,非也,有得必有失

使用公平鎖會有什麼問題?

公平鎖保證了排隊的公平性,非公平鎖霸氣的忽視這個規則,所以就有可能導致排隊的長時間在排隊,也沒有機會獲取到鎖,這就是傳說中的 “飢餓”

如何選擇公平鎖/非公平鎖?

相信到這裏,答案已經在你心中了,如果為了更高的吞吐量,很顯然非公平鎖是比較合適的,因為節省很多線程切換時間,吞吐量自然就上去了,否則那就用公平鎖還大家一個公平

我們還差最後一個環節,真的要挺住

可重入鎖

到這裏,我們還沒分析 ReentrantLock 的名字,JDK 起名這麼有講究,肯定有其含義,直譯過來【可重入鎖】

為什麼要支持鎖的重入?

試想,如果是一個有 synchronized 修飾的遞歸調用方法,程序第二次進入被自己阻塞了豈不是很大的笑話,所以 synchronized 是支持鎖的重入的

Lock 是新輪子,自然也要支持這個功能,其實現也很簡單,請查看公平鎖和非公平鎖對比圖,其中有一段代碼:

// 判斷當前線程是否和已佔用鎖的線程是同一個
else if (current == getExclusiveOwnerThread())

仔細看代碼, 你也許發現,我前面的一個說明是錯誤的,我要重新解釋一下

重入的線程會一直將 state + 1, 釋放鎖會 state – 1直至等於0,上面這樣寫也是想幫助大家快速的區分

總結

本文是一個長文,說明了為什麼要造 Lock 新輪子,如何標準的使用 Lock,AQS 是什麼,是如何實現鎖的,結合 ReentrantLock 反推 AQS 中的一些應用以及其獨有的一些特性

獨佔式獲取鎖就這樣介紹完了,我們還差 AQS 共享式 xxxShared 沒有分析,結合共享式,接下來我們來閱讀一下 Semaphore,ReentrantReadWriteLock 和 CountLatch 等

最後,也歡迎大家的留言,如有錯誤之處還請指出。我的手酸了,眼睛幹了,我去準備擼下一篇…..

靈魂追問

  1. 為什麼更改 state 有 setState() , compareAndSetState() 兩種方式,感覺後者更安全,但是鎖的視線中有好多地方都使用了 setState(),安全嗎?

  2. 下面代碼是一個轉賬程序,是否存在死鎖或者鎖的其他問題呢?

    
    class Account {
      private int balance;
      private final Lock lock
              = new ReentrantLock();
      // 轉賬
      void transfer(Account tar, int amt){
        while (true) {
          if(this.lock.tryLock()) {
            try {
              if (tar.lock.tryLock()) {
                try {
                  this.balance -= amt;
                  tar.balance += amt;
                } finally {
                  tar.lock.unlock();
                }
              }//if
            } finally {
              this.lock.unlock();
            }
          }//if
        }//while
      }//transfer
    }
    

參考

  1. Java 併發實戰
  2. Java 併發編程的藝術
  3. https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

國外零售商洩漏 Intel 第 11 代 Rocket Lake-S 處理器的售價,i9-11900K 價格比上一代便宜一些_網頁設計公司

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

台中景泰電動車行只是一個單純的理由,將來台灣的環境,出門可以自由放心的深呼吸,讓空氣回歸自然的乾淨,減少污染,留給我們下一代有好品質無空污的優質環境

先前 CES 2021 發表會上,Intel 正式透露第 11 代 Rocket Lake-S 處理器(i9-11900K)官方效能跑分之後,相信不少玩家都非常期待,不僅比上一代還強,也勝過對手的 AMD 12 核心 Ryzen 5900X,不過當時並沒有公布售價,也因此想買的人究竟該準備多少,只能參考第 10 代的價格。最近國外就有一間零售商,疑似搶先放上第 11 代 Rocket Lake-S 處理器的價格列表,從 i9、i7、一直到 i5 都有,有些比較貴,有些便宜。

國外零售商洩漏 Intel 第 11 代 Rocket Lake-S 處理器的售價

近日比利時一間 2Compute 電腦硬體零售商在它的官網上,洩漏第 11 代 Rocket Lake-S 處理器的價格列表,而且現在還沒刪除,感覺就像是故意放上去一樣。

根據列表顯示,新一代遊戲旗艦處理器 8 核心 16 執行緒的 i9-11900K 單顆未稅價為 499.70 歐元,含稅之後變成 605 歐元,代表說比上一代 i9-10900K 還便宜 9.8% 左右。

下圖是 i9-11900K 的價格:

i9-10900K 的價格,單顆未稅價 549 歐元,含稅之後 665 歐元:

下方為第 11 代 Rocket Lake-S 處理器與第 10 代 Comet Lake-S 處理器的比較圖,i9-11900KF 也比上一代便宜,但 i9-11900 與 i9-11900F 就沒有,其餘 i7、i5 大多數也比上一代貴:

2Compute 網站洩漏的 Rocket Lake-S 處理器價格清單(含稅價):

  • i5-11400:205 歐元
  • i5-11400F:175 歐元
  • i5-11500:227 歐元
  • i5-11600:250 歐元
  • i5-11600K:294 歐元
  • i5-11600KF:265 歐元
  • i7-11700:371 歐元
  • i7-11700F:342 歐元
  • i7-11700K:456 歐元
  • i7-11700KF:427 歐元
  • i9-11900:494 歐元
  • i9-11900F:465 歐元
  • i9-11900K:605 歐元
  • i9-11900KF:576 歐元

如果比較整體價格,第 11 代 Rocket Lake-S 處理器平均大約比第 10 代 Comet Lake-S 處理器貴 14% 左右。當然,這是零售商搶先洩漏的清單,也因此一切還是要以 Intel 官方為主。

Intel 第 11 代 Rocket Lake-S 處理器預計會在 3 月正式推出,下個月應該會有更確切的消息出現。

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

網站的第一印象網頁設計,決定了客戶是否繼續瀏覽的意願。台北網動廣告製作的RWD網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上它。

有趣的是,除了 2Compute,國外其實還有其他零售商也洩漏這價格清單,不過大家都不太一樣,讓人看起來有點亂:

Intel 11th Gen Price

10th Gen sheet is for comparison pic.twitter.com/kAvMbIQjKm

— 포시포시 (@harukaze5719) January 17, 2021

資料來源:Video Cardz

Intel 第 11 代「Rocket Lake」與第 12 代「Alder Lake」搶先看

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

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

以設計的實用美學觀點,規劃出舒適、美觀的視覺畫面,有效提昇使用者的心理期待,營造出輕鬆、愉悅的網站瀏覽體驗。