鼓勵廢塑膠回收!印尼泗水「空瓶當車票」

摘錄自2018年11月12日民視報導

印尼第二大城泗水為了鼓勵民眾回收塑膠廢棄物,推動了「用塑膠空瓶當車票」的活動,只要用塑膠空瓶就能換公車車票,免費在市區搭一趟車。這不僅鼓勵民眾回收塑膠廢棄物,也增加大眾運輸的使用率,印尼政府希望藉這項活動,在2020年前讓印尼免受塑膠塑廢棄物之苦。

泗水衛生局官員迪居尼亞托羅表示:「為避免塑膠瓶造成環境污染問題,我們呼籲居民回收塑膠空瓶,用這些空瓶來換公車車票。」

這項活動每天回收平均200公斤塑膠空瓶,周末回收數更高,從活動開始到現在,回收量已累積高達39公噸。

印尼是僅次於中國全球最大塑膠垃圾製造國之一,其中泗水的日常垃圾中,就有15%是塑膠廢棄物。當地政府也表示,這項活動讓塑膠廢棄物具有實用價值,讓民眾更關注這項問題。

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

【其他文章推薦】

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

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

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

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

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

幼獸淪名流自拍炫富工具後棄養

摘錄自2018年11月15日公視報導

法國警方19號在香榭大道攔下一輛名車藍寶堅尼,發現裡面有一隻不到兩個月大的獅子寶寶,動保團體表示有越來越多人,會買一隻幼獸自拍炫富,然後棄養,形成歪風。

動保團體「300萬之友」主席胡庭說:「問題就出在馬戲團,他們可以利用野生動物(表演),法國馬戲團大約有1500隻野生動物,可怕的是這些馬戲團有繁殖的權利。」

胡庭認為,買幼獸自拍並上傳這群媒體的歪風,是來自波斯灣阿拉伯國家,這些小動物長大就會被丟棄;他們在約旦撿到三、四十隻被丟棄的小獅子,但更令人擔心的是,現在法國似乎也開始流行,過去半年,他們陸續撿到四隻獅子幼獸。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

從字符串到常量池,一文看懂String類設計

從一道面試題開始

看到這個標題,你肯定以為我又要講這道面試題了

//  這行代碼創建了幾個對象?
String s3 = new String("1");

是的,沒錯,我確實要從這裏開始

這道題就算你沒做過也肯定看到,總所周知,它創建了兩個對象,一個位於堆上,一個位於常量池中。

這個答案粗看起來是沒有任何問題的,但是仔細思考確經不起推敲。

如果你覺得我說的不對的話,那麼可以思考下面這兩個問題

  1. 你說它創建了兩個對象,那麼這兩個對象分別是怎樣創建的呢?我們回顧下Java創建對象的方式,一共就這麼幾種

    • 使用new關鍵字創建對象
    • 使用反射創建對象(包括Class類的newInstance方法,以及Constructor類的newInstance方法)
    • 使用clone複製一個對象
    • 反序列化得到一個對象

    你說它創建了兩個對象,那你告訴我除了new出來那個對象外,另外一個對象怎麼創建出來的?

  2. 堆跟常量池到底什麼關係?不是說在JDK1.7之後(含1.7版本)常量池已經移到了堆中了嗎?如果說常量池本身就位於堆中的話,那麼這種一個對象在堆中,一個對象在常量池的說法還準確嗎?

如果你也產生過這些疑問的話,那麼請耐心看完這篇文章!要解釋上面的問題首先我們得對常量池有個準確的認知。

常量池

通常來說,我們提到的常量池分為三種

  • class文件中的常量池
  • 運行時常量池
  • 字符串常量池

對於這三種常量池,我們需要搞懂下面幾個問題?

  1. 這個常量池在哪裡?
  2. 這個常量池用來干什麼呢?
  3. 這三者有什麼關係?

接下來,我們帶着這些問題往下看

class文件中的常量池

位置在哪?

顧名思義,class文件中的常量池當然是位於class文件中,而class文件又是位於磁盤上

用來干什麼的?

在學習class文件中的常量池前,我們首選需要對class文件的結構有一定了解

Class文件是一組以8個字節為基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在文

件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部是程序運行的必要數

據,沒有空隙存在。

​ ————《深入理解Java虛擬機》

整個class文件的組成可以用下圖來表示

對本文而言,我們只關注其中的常量池部分,常量池可以理解為class文件中資源倉庫,它是class文件結構中與其它項目關聯最多的數據類型,主要用於存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References)
字面量就是我們所說的常量概念,如文本字符串、被聲明為final的常量值等
符號引用是一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可(它與直接引用區分一下,直接引用一般是指向方法區的本地指針,相對偏移量或是一個能間接定位到目標的句柄)。一般包括下面三類常量:

  • 類和接口的全限定名
  • 字段的名稱和描述符
  • 方法的名稱和描述符

現在我們知道了class文件中常量池的作用:存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References)。很多時候知道了一個東西的概念並不能說你會了,對於程序員而言,如果你說你已經會了,那麼最好的證明是你能夠通過代碼將其描述出來,所以,接下來,我想以一種直觀的方式讓大家感受到常量池的存在。通過分析一段簡單代碼的字節碼,讓大家能更好感知常量池的作用。

talk is cheap ,show me code

我們以下面這段代碼為例,通過javap來查看class文件中的具體內容,代碼如下:

/**
 * @author 程序員DMZ
 * @Date Create in 22:59 2020/6/15
 * @公眾號 微信搜索:程序員DMZ
 */
public class Main {
    public static void main(String[] args) {
        String name = "dmz";
    }
}

進入Main.java文件所在目錄,執行命令:javac Main.java ,那麼此時會在當前目錄下生成對應的Main.class文件。再執行命令:javap -v -c Main.class,此時會得到如下的解析后的字節碼信息

public class com.dmz.jvm.Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
// 這裏就是常量池了
Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = String             #21            // dmz
   #3 = Class              #22            // com/dmz/jvm/Main
   #4 = Class              #23            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               LocalVariableTable
  #10 = Utf8               this
  #11 = Utf8               Lcom/dmz/jvm/Main;
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               args
  #15 = Utf8               [Ljava/lang/String;
  #16 = Utf8               name
  #17 = Utf8               Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               Main.java
  #20 = NameAndType        #5:#6          // "<init>":()V
  #21 = Utf8               dmz
  #22 = Utf8               com/dmz/jvm/Main
  #23 = Utf8               java/lang/Object
 // 下面是方法表                           
{
  public com.dmz.jvm.Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/dmz/jvm/Main;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         // 可以看到方法表中的指令引用了常量池中的常量,這也是為什麼說常量池是資源倉庫的原因
         // 因為它會被class文件中的其它結構引用         
         0: ldc           #2                  // String dmz
         2: astore_1
         3: return
      LineNumberTable:
        line 9: 0
        line 10: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  args   [Ljava/lang/String;
            3       1     1  name   Ljava/lang/String;
}
SourceFile: "Main.java"

在上面的字節碼中,我們暫且關注常量池中的內容即可。主要看這兩行

#2 = String             #14            // dmz
#14 = Utf8               dmz

如果要看懂這兩行代碼,我們需要對常量池中String類型常量的結構有一定了解,其結構如下:

CONSTANT_String_info tag 標誌常量類型的標籤
index 指向字符串字面量的索引

對應到我們上面的字節碼中,tag=String,index=#14,所以我們可以知道,#2是一個字面量為#14的字符串類型常量。而#14對應的字面量信息(一個Utf8類型的常量)就是dmz

常量池作為資源倉庫,最大的用處在於被class文件中的其它結構所引用,這個時候我們再將注意力放到main方法上來,對應的就是這三條指令

0: ldc           #2                  // String dmz
2: astore_1
3: return

ldc:這個指令的作用是將對應的常量的引用壓入操作數棧,在執行ldc指令時會觸發對它的符號引用進行解析,在上面例子中對應的符號引用就是#2,也就是常量池中的第二個元素(這裏就能看出方法表中就引用了常量池中的資源)

astore_1:將操作數棧底元素彈出,存儲到局部變量表中的1號元素

return:方法返回值為void,標誌方法執行完成,將方法對應棧幀從棧中彈出

下面我用畫圖的方式來畫出整個流程,主要分為四步

  1. 解析ldc指令的符號引用(#2

  2. #2對應的常量的引用壓入到操作數棧頂

  3. 將操作數棧的元素彈出並存儲到局部變量表中

  4. 執行return指令,方法執行結束,彈出棧區該方法對應的棧幀

第一步:

在解析#2這個符號引用時,會先到字符串常量池中查找是否存在對應字符串實例的引用,如果有的話,那麼直接返回這個字符串實例的引用,如果沒有的話,會創建一個字符串實例,那麼將其添加到字符串常量池中(實際上是將其引用放入到一個哈希表中),之後再返回這個字符串實例對象的引用。

到這裏也能回答我們之前提出的那個問題了,一個對象是new出來的,另外一個是在解析常量池的時候JVM自動創建的

第二步:

將第一步得到的引用壓入到操作數棧,此時這個字符串實例同時被操作數棧以及字符串常量池引用。

第三步:

操作數棧中的引用彈出,並賦值給局部變量表中的1號位置元素,到這一步其實執行完了String name = "dmz"這行代碼。此時局部變量表中儲存着一個指向堆中字符串實例的引用,並且這個字符串實例同時也被字符串常量池引用。

第四步:

這一步我就不畫圖了,就是方法執行完成,棧幀彈出,非常簡單。

在上文中,我多次提到了字符串常量池,它到底是個什麼東西呢?我們還是分為兩部分討論

  1. 位置在哪?
  2. 用來干什麼的?

字符串常量池

位置在哪?

字符串常量池比較特殊,在JDK1.7之前,其存在於永久代中,到JDK1.7及之後,已經中永久代移到了堆中。當然,如果你非要說永久代也是堆的一部分那我也沒辦法。

另外還要說明一點,經常有同學會將方法區元空間永久代(permgen space)的概念混淆。請注意

  1. 方法區JVM在內存分配時需要遵守的規範,是一個理論,具體的實現可以因人而異
  2. 永久代hotspot jdk1.8以前對方法區的實現,使用jdk1.7的老司機肯定以前經常遇到過java.lang.OutOfMemoryError: PremGen space異常。這裏的PermGen space其實指的就是方法區。不過方法區和PermGen space又有着本質的區別。前者是JVM的規範,而後者則是JVM規範的一種實現,並且只有HotSpot才有PermGen space
  3. 元空間jdk1.8對方法區的實現,jdk1.8徹底移除了永久代,其實,移除永久代的工作從JDK 1.7就開始了。JDK 1.7中,存儲在永久代的部分數據就已經轉移到Java Heap或者Native Heap。但永久代仍存在於JDK 1.7中,並沒有完全移除,譬如符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了Java heap;類的靜態變量(class statics)轉移到了Java heap。到jdk1.8徹底移除了永久代,將JDK7中還剩餘的永久代信息全部移到元空間,元空間相比對永久代最大的差別是,元空間使用的是本地內存(Native Memory)

用來干什麼的?

字符串常量池,顧名思義,肯定就是用來存儲字符串的嘛,準確來說存儲的是字符串實例對象的引用。我查閱了很多博客、資料,它們都會說,字符串常量池中存儲的就是字符串對象。其實我們可以類比下面這段代碼:

HashSet<Person> persons = new HashSet<Person>;

persons這個集合中,存儲的是Person對象還是Person對象對應的引用呢?

所以,請大聲跟我念三遍

字符串常量池存儲的是字符串實例對象的引用!

字符串常量池存儲的是字符串實例對象的引用!

字符串常量池存儲的是字符串實例對象的引用!

下面我們來看R大博文下評論的一段話:

簡單來說,HotSpot VM里StringTable是個哈希表,裏面存的是駐留字符串的引用(而不是駐留字符串實例自身)。也就是說某些普通的字符串實例被這個StringTable引用之後就等同被賦予了“駐留字符串”的身份。這個StringTable在每個HotSpot VM的實例里只有一份,被所有的類共享。類的運行時常量池裡的CONSTANT_String類型的常量,經過解析(resolve)之後,同樣存的是字符串的引用;解析的過程會去查詢StringTable,以保證運行時常量池所引用的字符串與StringTable所引用的是一致的。

​ ——R大博客

從上面我們可以知道

  1. 字符串常量池本質就是一個哈希表
  2. 字符串常量池中存儲的是字符串實例的引用
  3. 字符串常量池在被整個JVM共享
  4. 在解析運行時常量池中的符號引用時,會去查詢字符串常量池,確保運行時常量池中解析后的直接引用跟字符串常量池中的引用是一致的

為了更好理解上面的內容,我們需要去分析String中的一個方法—–intern()

intern方法分析

/** 
 * Returns a canonical representation for the string object. 
 * <p> 
 * A pool of strings, initially empty, is maintained privately by the 
 * class <code>String</code>. 
 * <p> 
 * When the intern method is invoked, if the pool already contains a 
 * string equal to this <code>String</code> object as determined by 
 * the {@link #equals(Object)} method, then the string from the pool is 
 * returned. Otherwise, this <code>String</code> object is added to the 
 * pool and a reference to this <code>String</code> object is returned. 
 * <p> 
 * It follows that for any two strings <code>s</code> and <code>t</code>, 
 * <code>s.intern()&nbsp;==&nbsp;t.intern()</code> is <code>true</code> 
 * if and only if <code>s.equals(t)</code> is <code>true</code>. 
 * <p> 
 * All literal strings and string-valued constant expressions are 
 * interned. String literals are defined in section 3.10.5 of the 
 * <cite>The Java&trade; Language Specification</cite>. 
 * 
 * @return  a string that has the same contents as this string, but is 
 *          guaranteed to be from a pool of unique strings. 
 */  
public native String intern();  

String#intern方法中看到,這個方法是一個 native 的方法,但註釋寫的非常明了。“如果常量池中存在當前字符串, 就會直接返回當前字符串. 如果常量池中沒有此字符串, 會將此字符串放入常量池中后, 再返回”。

關於其詳細的分析可以參考:美團:深入解析String#intern

珠玉在前,所以本文着重就分析下intern方法在JDK不同版本下的差異,首先我們要知道引起差異的原因是因為JDK1.7及之後將字符串常量池從永久代挪到了堆中。

我這裏就以美團文章中的示例代碼來進行分析,代碼如下:

public static void main(String[] args) {
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
}

打印結果是

  • jdk6 下false false
  • jdk7 下false true

在美團的文章中已經對這個結果做了詳細的解釋,接下來我就用我的圖解方式再分析一波這個過程

jdk6 執行流程

第一步:執行 String s = new String("1"),要清楚這行代碼的執行過程,我們還是得從字節碼入手,這行代碼對應的字節碼如下:

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/String
       3: dup
       4: ldc           #3                  // String 1
       6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
       9: astore_1
      10: return

new :創建了一個類的實例(還沒有調用構造器函數),並將其引用壓入操作數棧頂

dup:複製棧頂數值並將複製值壓入棧頂,這是因為invokespecialastore_1各需要消耗一個引用

ldc:解析常量池符號引用,將實際的直接引用壓入操作數棧頂

invokespecial:彈出此時棧頂的常量引用及對象引用,執行invokespecial指令,調用構造函數

astore_1:將此時操作數棧頂的元素彈出,賦值給局部變量表中1號元素(0號元素存的是main函數的參數)

我們可以將上面整個過程分為兩個階段

  1. 解析常量
  2. 調用構造函數創建對象並返回引用

在解析常量的過程中,因為該字符串常量是第一次解析,所以會先在永久代中創建一個字符串實例對象,並將其引用添加到字符串常量池中。此時內存狀態如下:

當真正通過new方式創建對象完成后,對應的內存狀態如下,因為在分析class文件中的常量池的時候已經對棧區做了詳細的分析,所以這裏就省略一些細節了,在執行完這行代碼后,棧區存在一個引用,指向 了堆區的一個字符串實例內存狀態對應如下:

第二步:緊接着,我們調用了s的intern方法,對應代碼就是 s.intern()

當intern方法執行時,因為此時字符串常量池中已經存在了一個字面量信息跟s相同的字符串的引用,所以此時內存狀態不會發生任何改變。

第三步:執行String s2 = "1",此時因為常量池中已經存在了字面量1的對應字符串實例的引用,所以,這裏就直接返回了這個引用並且賦值給了局部變量s2。對應的內存狀態如下:

到這裏就很清晰了,s跟s2指向兩個不同的對象,所以s==s2肯定是false嘛~

如果看過美團那篇文章的同學可能會有些疑惑,我在圖中對常量池的描述跟美團文章圖中略有差異,在美團那篇文章中,直接將具體的字符串實例放到了字符串常量池中,而在我上面的圖中,字符串常量池存的永遠時引用,它的圖是這樣畫的

就我查閱的資料而言,我個人不贊同這種說法,常量池中應該保存的僅僅是引用。關於這個問題,我已經向美團的團隊進行了留言,也請大佬出來糾錯!

接着我們分析s3跟s4,對應的就是這幾行代碼:

String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);

我們一行行分析,看看執行完后,內存的狀態是什麼樣的

第一步:String s3 = new String("1") + new String("1"),執行完成后,堆區多了兩個匿名對象,這個我們不用多關注,另外堆區還多了一個字面量為11的字符串實例,並且棧中存在一個引用指向這個實例

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-NVeeWKoO-1592334452491)(upload\image-20200617020742618.png)]

實際上上圖中還少了一個匿名的StringBuilder的對象,這是因為當我們在進行字符串拼接時,編譯器默認會創建一個StringBuilder對象並調用其append方法來進行拼接,最後再調用其toString方法來轉換成一個字符串,StringBuildertoString方法其實就是new一個字符串

public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

這也是為什麼在圖中會說在堆上多了一個字面量為11的字符串實例的原因,因為實際上就是new出來的嘛!

第二步:s3.intern()

調用intern方法后,因為字符串常量池中目前沒有11這個字面量對應的字符串實例的應用,所以JVM會先從堆區複製一個字符串實例到永久代中,再將其引用添加到字符串常量池中,最終的內存狀態就如下所示

第三步:String s4 = "11"

這應該沒啥好說的了吧,常量池中有了,直接指向對應的字符串實例

到這裏可以發現,s3跟s4指向的根本就是兩個不同的對象,所以也返回false

jdk7 執行流程

在jdk1.7中,s跟s2的執行結果還是一樣的,這是因為 String s = new String("1")這行代碼本身就創建了兩個字符串對象,一個屬於被常量池引用的駐留字符串,而另外一個只是堆上的一個普通字符串對象。跟1.6的區別在於,1.7中的駐留字符串位於堆上,而1.6中的位於方法區中,但是本質上它們還是兩個不同的對象,在下面代碼執行完后

    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);

內存狀態為:

但是對於s3跟s4確不同了,因為在jdk1.7中不會再去複製字符串實例了,在intern方法執行時在發現堆上有對應的對象之後,直接將這個對應的引用添加到字符串常量池中,所以代碼執行完,內存狀態對應如下:

看到了吧,s3跟s4指向的同一個對象,這是因為intern方法執行時,直接s3這個引用複製到了常量池,之後執行String s4= "11"的時候,直接再將常量池中的引用複製給了s4,所以s3==s4肯定為true啦。

在理解了它們之間的差異之後,我們再來思考一個問題,假設我現在將代碼改成這個樣子,那麼運行結果是什麼樣的呢?

public static void main(String[] args) {
    String s = new String("1");
    String sintern = s.intern();
    String s2 = "1";
    System.out.println(sintern == s2);

    String s3 = new String("1") + new String("1");
    String s3intern = s3.intern();
    String s4 = "11";
    System.out.println(s3intern == s4);
}

上面這段代碼運行起來結果會有差異嗎?大家可以自行思考~

在我們對字符串常量池有了一定理解之後會發現,其實通過String name = "dmz"這行代碼申明一個字符串,實際的執行邏輯就像下面這段偽代碼所示

/**
  * 這段代碼邏輯類比於
  * <code>String s = "字面量"</code>;這種方式申明一個字符串
  * 其中字面量就是在""中的值
  *
  */
public String declareString(字面量) {
    String s;
    // 這是一個偽方法,標明會根據字面量的值到字符串值中查找是否存在對應String實例的引用
    s = findInStringTable(字面量);
    // 說明字符串池中已經存在了這個引用,那麼直接返回
    if (s != null) {
        return s;
    }
    // 不存在這個引用,需要新建一個字符串實例,然後調用其intern方法將其拘留到字符串池中,
    // 最後返回這個新建字符串的引用
    s = new String(字面量);
    // 調用intern方法,將創建好的字符串放入到StringTable中,
    // 類似就是調用StringTable.add(s)這也的一個偽方法
    s.intern();
    return s;
}

按照這個邏輯,我們將我們將上面思考題中的所有字面量進行替換,會發現不管在哪個版本中結果都應該返回true。

運行時常量池

位置在哪?

位於方法區中,1.6在永久代,1.7在元空間中,永久代跟元空間都是對方法區的實現

用來干什麼?

jvm在執行某個類的時候,必須經過加載、連接、初始化,而連接又包括驗證# 位置在哪?

位於方法區中,1.6在永久代,1.7在元空間中,永久代跟元空間都是對方法區的實現

用來干什麼?

jvm在執行某個類的時候,必須經過加載、連接、初始化,而連接又包括驗證、準備、解析三個階段。而當類加載到內存中后,jvm就會將class常量池中的內容存放到運行時常量池中,由此可知,運行時常量池也是每個類都有一個。在上面我也說了,class常量池中存的是字面量和符號引用,也就是說他們存的並不是對象的實例,而是對象的符號引用值。而經過解析(resolve)之後,也就是把符號引用替換為直接引用,解析的過程會去查詢全局字符串池,也就是我們上面所說的StringTable,以保證運行時常量池所引用的字符串與全局字符串池中所引用的是一致的

所以簡單來說,運行時常量池就是用來存放class常量池中的內容的。

總結

我們將三者進行一個比較

以一道測試題結束

// 環境1.7及以上
public class Clazz {
    public static void main(String[] args) {
        String s1 = new StringBuilder().append("ja").append("va1").toString();
        String s2 = s1.intern();
        System.out.println(s1==s2);
        
        String s5 = "dmz";
        String s3 = new StringBuilder().append("d").append("mz").toString();
        String s4 = s3.intern();
        System.out.println(s3 == s4);

        String s7 = new StringBuilder().append("s").append("pring").toString();
        String s8 = s7.intern();
        String s6 = "spring";
        System.out.println(s7 == s8);
    }
}

答案是true,false,true。大家可以仔細思考為什麼,如有疑惑可以給我留言,或者進群交流!

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

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

參考文章:

R大博文:請別再拿“String s = new String(“xyz”);創建了多少個String實例”來面試了吧

R大知乎回答:JVM 常量池中存儲的是對象還是引用呢?

Java中幾種常量池的區分

方法區,永久代和元空間

美團:深入解析String#intern

參考書籍:

《深入理解Java虛擬機》第二版

《深入理解Java虛擬機》第三版

《Java虛擬機規範》

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

【其他文章推薦】

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

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

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

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

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

精美圖文講解Java AQS 共享式獲取同步狀態以及Semaphore的應用

| 好看請贊,養成習慣

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

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

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

看到本期內容這麼少,是不是心動了呢?

前言

上一篇萬字長文 Java AQS隊列同步器以及ReentrantLock的應用 為我們讀 JUC 源碼以及其設計思想做了足夠多的鋪墊,接下來的內容我將重點說明差異化,如果有些童鞋不是能很好的理解文中的一些內容,強烈建議回看上一篇文章,搞懂基礎內容,接下來的閱讀真會輕鬆加愉快

AQS 中我們介紹了獨佔式獲取同步狀態的多種情形:

  • 獨佔式獲取鎖
  • 可響應中斷的獨佔式獲取鎖
  • 有超時限制的獨佔式獲取鎖

AQS 提供的模版方法裏面還差共享式獲取同步狀態沒有介紹,所以我們今天來揭開這個看似神秘的面紗

AQS 中的共享式獲取同步狀態

獨佔式是你中沒我,我中沒你的的一種互斥形式,共享式顯然就不是這樣了,所以他們的唯一區別就是:

同一時刻能否有多個線程同時獲取到同步狀態

簡單來說,就是這樣滴:

我們知道同步狀態 state 是維護在 AQS 中的,拋開可重入鎖的概念,我在上篇文章中也提到了,獨佔式和共享式控制同步狀態 state 的區別僅僅是這樣:

所以說想了解 AQS 的 xxxShared 的模版方法,只需要知道它是怎麼控制 state 的就好了

AQS共享式獲取同步狀態源碼分析

為了幫助大家更好的回憶內容,我將上一篇文章的兩個關鍵內容粘貼在此處,幫助大家快速回憶,關於共享式,大家只需要關注【騷紫色】就可以了

自定義同步器需要重寫的方法

AQS 提供的模版方法

故事就從這裏說起吧 (你會發現和獨佔式驚人的相似),關鍵代碼都加了註釋

    public final void acquireShared(int arg) {
      	// 同樣調用自定義同步器需要重寫的方法,非阻塞式的嘗試獲取同步狀態,如果結果小於零,則獲取同步狀態失敗
        if (tryAcquireShared(arg) < 0)
          	// 調用 AQS 提供的模版方法,進入等待隊列
            doAcquireShared(arg);
    }

進入 doAcquireShared 方法:

    private void doAcquireShared(int arg) {
      	// 創建共享節點「SHARED」,加到等待隊列中
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
          	// 進入“自旋”,這裏並不是純粹意義上的死循環,在獨佔式已經說明過
            for (;;) {
              	// 同樣嘗試獲取當前節點的前驅節點
                final Node p = node.predecessor();
              	// 如果前驅節點為頭節點,嘗試再次獲取同步狀態
                if (p == head) {
                  	// 在此以非阻塞式獲取同步狀態
                    int r = tryAcquireShared(arg);
                  	// 如果返回結果大於等於零,才能跳出外層循環返回
                    if (r >= 0) {
                      	// 這裡是和獨佔式的區別
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

上面代碼第 18 行我們提到和獨佔式獲取同步狀態的區別,貼心的給大家一個更直觀的對比:

差別只在這裏,所以我們就來看看 setHeadAndPropagate(node, r) 到底幹了什麼,我之前說過 JDK 源碼中的方法命名絕大多數還是非常直觀的,該方法直譯過來就是 【設置頭並且傳播/繁衍】。獨佔式只是設置了頭,共享式除了設置頭還多了一個傳播,你的疑問應該已經來了:

啥是傳播,為什麼會有傳播這個設置呢?

想了解這個問題,你需要先知道非阻塞共享式獲取同步狀態返回值的含義:

這裏說的傳播其實說的是 propagate > 0 的情況,道理也很簡單,當前線程獲取同步狀態成功了,還有剩餘的同步狀態可用於其他線程獲取,那就要通知在等待隊列的線程,讓他們嘗試獲取剩餘的同步狀態

如果要讓等待隊列中的線程獲取到通知,需要線程調用 release 方法實現的。接下來,我們走近 setHeadAndPropagate 一探究竟,驗證一下

  // 入參,node: 當前節點
	// 入參,propagate:獲取同步狀態的結果值,即上面方法中的變量 r
	private void setHeadAndPropagate(Node node, int propagate) {
    		// 記錄舊的頭部節點,用於下面的check
        Node h = head; 
    		// 將當前節點設置為頭節點
        setHead(node);
        
    		// 通過 propagate 的值和 waitStatus 的值來判斷是否可以調用 doReleaseShared 方法
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
          	// 如果後繼節點為空或者後繼節點為共享類型,則進行喚醒後繼節點
    				// 這裏後繼節點為空意思是只剩下當前頭節點了,另外這裏的 s == null 也是判斷空指針的標準寫法
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

上面方法的大方向作用我們了解了,但是代碼中何時調用 doReleaseShared 的判斷邏輯還是挺讓人費解的,為什麼會有這麼一大堆的判斷,我們來逐個分析一下:

這裏的空判斷有點讓人頭大,我們先挑出來說明一下:

排除了其他判斷條件的干擾,接下來我們就專註分析 propagate 和 waitStatus 兩個判斷條件就可以了,這裏再將 waitStatus 的幾種狀態展示在這裏,幫助大家理解,【騷粉色】是我們一會要用到的:

propagate > 0

上面已經說過了,如果成立,直接短路後續判斷,然後根據 doReleaseShared 的判斷條件進行釋放

propagate > 0 不成立, h.waitStatus < 0 成立 (注意這裏的h是舊的頭節點)

什麼時候 h.waitStatus < 0 呢?拋開 CONDITION 的使用,只剩下 SIGNAL 和 PROPAGATE,想知道這個答案,需要提前看一下 doReleaseShared() 方法了:

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                  	// CAS 將頭節點的狀態設置為0                
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    // 設置成功后才能跳出循環喚醒頭節點的下一個節點
                  	unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         // 將頭節點狀態CAS設置成 PROPAGATE 狀態
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

doReleaseShared() 方法中可以看出:

  • 如果讓 h.waitStatus < 0 成立,只能將其設置成 PROPAGATE = -3 的情況,設置成功的前提是 h 頭節點 expected 的狀態是 0;

  • 如果 h.waitStatus = 0,是上述代碼第 8 行 CAS 設置成功,然後喚醒等待中的線程

所以猜測,當前線程執行到 h.waitStatus < 0 的判斷前,有另外一個線程剛好執行了 doReleaseShared() 方法,將 waitStatus 又設置成PROPAGATE = -3

這個理解有點繞,我們還是來畫個圖理解一下吧:

可能有同學還是不太能理解這麼寫的道理,我們一直說 propagate <> = 0 的情況,propagate = 0 代表的是當時/當時/當時 嘗試獲取同步狀態沒成功,但是之後可能又有共享狀態被釋放了,所以上面的邏輯是以防這種萬一,你懂的,嚴謹的併發就是要防止一切萬一,現在結合這個情景再來理解上面的判斷你是否豁然開朗了呢?

繼續向下看,

前序條件不成立,(h = head) == null || h.waitStatus < 0 注意這裏的h是新的頭節點)

有了上面鋪墊,這個就直接畫個圖就更好理解啦,其實就是沒有那麼巧有另外一個線程摻合了

相信到這裏你應該理解共享式獲取同步狀態的全部過程了吧,至於非阻塞共享式獲取同步狀態帶有超時時間獲取同步狀態,結合本文講的 setHeadAndPropagate 邏輯和獨佔式獲取同步狀態的實現過程過程來看,真是一毛一樣,這裏就不再累述了,趕緊打開你的 IDE 去驗證一下吧

我們分析了AQS 的模版方法,還一直沒說 tryAcquireShared(arg) 這個方法是如何被重寫的,想要了解這個,我們就來看一看共享式獲取同步狀態的經典應用 Semaphore

Semaphore 的應用及源碼分析

Semaphore 概念

Semaphore 中文多翻譯為 【信號量】,我還特意查了一下劍橋辭典的英文解釋:

其實就是信號標誌(two flags),比如紅綠燈,每個交通燈產生兩種不同行為

  • Flag1-紅燈:停車
  • Flag2-綠燈:行車

在 Semaphore 裏面,什麼時候是紅燈,什麼時候是綠燈,其實就是靠 tryAcquireShared(arg) 的結果來表示的

  • 獲取不到共享狀態,即為紅燈
  • 獲取到共享狀態,即為綠燈

所以我們走近 Semaphore ,來看看它到底是怎麼應用 AQS 的,又是怎樣重寫 tryAcquireShared(arg) 方法的

Semaphore 源碼分析

先看一下類結構

看到這裏你是否有點跌眼鏡,和 ReentrantLock 相似的可怕吧,如果你有些陌生,再次強烈建議你回看上一篇文章 Java AQS隊列同步器以及ReentrantLock的應用 ,這裏直接提速對比看公平和非公平兩種重寫的 tryAcquireShared(arg) 方法,沒有意外,公平與否,就是判斷是否有前驅節點

方法內部只是計算 state 的剩餘值,那 state 的初始值是多少怎麼設置呢?當然也就是構造方法了:

		public Semaphore(int permits) {
      	// 默認仍是非公平的同步器,至於為什麼默認是非公平的,在上一篇文章中也特意說明過
        sync = new NonfairSync(permits);
    }
    
    NonfairSync(int permits) {
    		super(permits);
    }

super 方法,就會將初始值給到 AQS 中的 state

也許你發現了,當我們把 permits 設置為1 的時候,不就是 ReentrantLock 的互斥鎖了嘛,說的一點也沒錯,我們用 Semaphore 也能實現基本互斥鎖的效果


static int count;
//初始化信號量
static final Semaphore s 
    = new Semaphore(1);
//用信號量保證互斥    
static void addOne() {
  s.acquire();
  try {
    count+=1;
  } finally {
    s.release();
  }
}

But(英文聽力中的重點),Semaphore 肯定不是為這種特例存在的,它是共享式獲取同步狀態的一種實現。如果使用信號量,我們通常會將 permits 設置成大於1的值,不知道你是否還記得我曾在 為什麼要使用線程池? 一文中說到的池化概念,在同一時刻,允許多個線程使用連接池,每個連接被釋放之前,不允許其他線程使用。所以說 Semaphore 可以允許多個線程訪問一個臨界區,最終很好的做到一個限流/限流/限流 的作用

雖然 Semaphore 能很好的提供限流作用,說實話,Semaphore 的限流作用比較單一,我在實際工作中使用 Semaphore 並不是很多,如果真的要用高性能限流器,Guava RateLimiter 是一個非常不錯的選擇,我們後面會做分析,有興趣的可以提前了解一下

關於 Semaphore 源碼,就這麼三下五除二的結束了

總結

不知你有沒有感覺到,我們的節奏明顯加快了,好多原來分散的點在被瘋狂的串聯起來,如果按照這個方式來閱讀 JUC 源碼,相信你也不會一頭扎進去迷失方向,然後沮喪的退出 JUC 吧,然後面試背誦答案,然後忘記,然後再背誦?

跟上節奏,關於共享式獲取同步狀態,Semaphore 只不過是非常經典的應用,ReadWriteLock 和 CountDownLatch 日常應用還是非常廣泛的,我們接下來就陸續聊聊它們吧

靈魂追問

  1. Semaphore 的 permits 設置成1 “等同於” 簡單的互斥鎖實現,那它和 ReentrantLock 的區別還是挺大的,都有哪些區別呢?
  2. 你在項目中是如何使用 Semaphore 的呢?

參考

  1. Java 併發實戰
  2. Java 併發編程的藝術
  3. https://blog.csdn.net/anlian523/article/details/106319294

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

java併發編程 –併發問題的根源及主要解決方法

目錄

  • 併發問題的根源在哪
    • 緩存導致的可見性
    • 線程切換帶來的原子性
    • 編譯器優化帶來的有序性
  • 主要解決辦法
    • 避免共享
    • Immutability(不變性)
    • 管程及其他工具

併發問題的根源在哪

首先,我們要知道併發要解決的是什麼問題?併發要解決的是單進程情況下硬件資源無法充分利用的問題。而造成這一問題的主要原因是CPU-內存-磁盤三者之間速度差異實在太大。如果將CPU的速度比作火箭的速度,那麼內存的速度就像火車,而最慘的磁盤,基本上就相當於人雙腿走路。

這樣造成的一個問題,就是CPU快速執行完它的任務的時候,很長時間都會在等待磁盤或是內存的讀寫。

計算機的發展有一部分就是如何重複利用資源,解決硬件資源之間效率的不平衡,而後就有了多進程,多線程的發展。並且演化出了各種為多進程(線程)服務的東西:

  • CPU增加緩存機制,平衡與內存的速度差異
  • 增加了多個概念,CPU時間片,程序計數器,線程切換等,用以更好得服務併發場景
  • 編譯器的指令優化,希望在內部充分利用硬件資源

但是這樣一來,也會帶來新的併發問題,歸結起來主要有三個。

  • 由於緩存導致的可見性問題
  • 線程切換帶來的原子性問題
  • 編譯器優化帶來的有序性問題

我們分別介紹這幾個:

緩存導致的可見性

CPU為了平衡與內存之間的性能差異,引入了CPU緩存,這樣CPU執行指令修改數據的時候就可以批量直接讀寫CPU緩存的內存,一個階段后再將數據寫回到內存。

但由於現在多核CPU技術的發展,各個線程可能運行在不同CPU核上面,每個CPU核各有各自的CPU緩存。前面說到對變量的修改通常都會先寫入CPU緩存,再寫回內存。這就會出現這樣一種情況,線程1修改了變量A,但此時修改后的變量A只存儲在CPU緩存中。這時候線程B去內存中讀取變量A,依舊只讀取到舊的值,這就是可見性問題。

線程切換帶來的原子性

為了更充分得利用CPU,引入了CPU時間片時間片的概念。進程或線程通過爭用CPU時間片,讓CPU可以更加充分得利用。

比如在進行讀寫磁盤等耗時高的任務時,就可以將寶貴的CPU資源讓出來讓其他線程去獲取CPU並執行任務。

但這樣的切換也會導致問題,那就是會破壞線程某些任務的原子性。比如java中簡單的一條語句count += 1。

映射到CPU指令有三條,讀取count變量指令,變量加1指令,變量寫回指令。雖然在高級語言(java)看來它就是一條指令,但實際上確是三條CPU指令,並且這三條指令的原子性無法保證。也就是說,可能在執行到任意一條指令的時候被打斷,CPU被其他線程搶佔了。而這個期間變量值可能會被修改,這裏就會引發數據不一致的情況了。所以高併發場景下,很多時候都會通過鎖實現原子性。而這個問題也是很多併發問題的源頭。

編譯器優化帶來的有序性

因為現在程序員編寫的都是高級語言,編譯器需要將用戶的代碼轉成CPU可以執行的指令。

同時,由於計算機領域的不斷髮展,編譯器也越來越智能,它會自動對程序員編寫的代碼進行優化,而優化中就有可能出現實際執行代碼順序和編寫的代碼順序不一樣的情況。

而這種破壞程序有序性的行為,在有些時候會出現一些非常微妙且難以察覺的併發編程bug。

舉個簡單的例子,我們常見的單例模式是這樣的:

public class Singleton {
 
 private Singleton() {}

 private static Singleton sInstance;

 public static Singleton getInstance() {

    if (sInstance == null) {	//第一次驗證是否為null
      synchronized (Singleton.class) {   //加鎖
        if (sInstance == null) {	  //第二次驗證是否為null
          sInstance = new Singleton();  //創建對象
                 }
             }
         }
    return sInstance;
    }

}

即通過兩段判斷加鎖來保證單例的成功生成,但在極小的概率下,可能會出現異常情況。原因就出現在sInstance = new Singleton();這一行代碼上。這行代碼,我們理解的執行順序應該是這樣:

  1. 為Singleton象分配一個內存空間。
  2. 在分配的內存空間實例化對象。
  3. 把Instance 引用地址指向內存空間。

但在實際編譯的過程中,編譯器有可能會幫我們進行優化,優化完它的順序可能變成如下:

  1. 為Singleton對象分配一個內存空間。
  2. 把instance 引用地址指向內存空間。
  3. 在分配的內存空間實例化對象。

按照優化完的順序,當併發訪問的時候,可能會出現這樣的情況

  1. A線程進入方法進行第1次instance == null判斷。
  2. 此時A線程發現instance 為null 所以對Singleton.class加鎖。
  3. 然後A線程進入方法進行第2次instance == null判斷。
  4. 然後A線程發現instance 為null,開始進行對象實例化。
  5. 為對象分配一個內存空間。
    6.把Instance 引用地址指向內存空間(而就在這個指令完成后,線程B進入了方法)。
  6. B線程首先進入方法進行第1次instance == null判斷。
  7. B線程此時發現instance 不為null ,所以它會直接返回instance (而此時返回的instance 是A線程還沒有初始化完成的對象)

最終線程B拿到的instance 是一個沒有實例化對象的空內存地址,所以導致instance使用的過程中造成程序錯誤。解決辦法很簡單,可以給sInstance對象加上一個關鍵字,volatile,這樣編譯器就不會亂優化,有關volatile的具體內容後續再細說。

主要解決辦法

通過上面的介紹,其實可以歸納無論是CPU緩存,線程切換還是編譯器優化亂序,出現問題的核心都是因為多個線程要併發讀寫某個變量或併發執行某段代碼。那麼我們可以控制,一次只讓一個線程執行變量讀寫就可以了,這就是互斥

而在某些時候,互斥還不夠,還需要一定的條件。比如一個生產者一個消費者併發,生產者向隊列存東西,消費者向隊列拿東西。那麼生產者寫的時候要保證存的時候隊列不是滿的,消費者要保證拿的時候隊列非空。這種線程與線程間需要通信協作的情況,稱為同步同步可以說是更複雜的互斥

既然知道了併發編程的根源以及同步和互斥,那我們來看看有哪些解決的思路。其實一共也就三種:

  • 避免共享
  • Immutability(不變性)
  • 管程及其他工具

下面我們分別說說這三種方案的優缺點

避免共享

我們先來說說避免共享,其實避免共享說是線程本地存儲技術,在java中指的一般就是Threadlocal。ThreadLocal會為每個線程提供一個本地副本,每個線程都只會修改自己的ThreadLocal變量。這樣一來就不會出現共享變量,也就不會出現衝突了。

其實現原理是在ThreadLocal內部維護一個ThreadLocalMap,每次有線程要獲取對應變量的時候,先獲取當前線程,然後根據不同線程取不同的值,典型的以空間換時間。

所以ThreadLocal還是比較適用於需要共享資源,且資源佔用空間不大的情況。比如一些連接的session啊等等。但是這種模式應用場景也較為有限,比如需要同步情況就難以勝任。

Immutability(不變性)

Immutability在函數式中用得比較多,函數式編程的一個主要目的是要寫出無副作用的代碼,有關什麼是無副作用可以參考我以前的文章Scala函數式編程指南(一) 函數式思想介紹。而無副作用的一個主要特點就是變量都是Immutability即不可變的,即創建對象后不會再修改對象,比如scala默認的變量和數據結構都是不可變的。而在java中,不變性變量即通過final修飾的變量,如String,Long,Double等類型都是Immutability的,它們的內部實現都是基於final關鍵字的。

那這又和併發編程有什麼關係呢?其實啊,併發問題很大部分原因就是因為線程切換破壞了原子性,這又導致線程隨意對變量的讀寫破壞了數據的一致性。而不變性就不必擔心這個問題,因為變量都是不變,不可寫只能讀的。在這種編程模式下,你要修改一個變量,那麼只能新生成一個。這樣做的好處很明顯,但壞處也是顯而易見,那就是引入了額外的編程複雜度,喪失了代碼的可讀性和易用性。

因為如此,不變性的併發解決方案其實相對而已沒那麼廣泛,其中比較有代表性的算是Actor併發編程模型,我以前也有討論過,有興趣可以看看Actor模型淺析 一致性和隔離性,這種編程模型和常規併發解決方案有很顯著的差異。按我的了解,Acctor模式多用在分佈式系統的一些協調功能,比如維持集群中多個機器的心跳通信等等。如果在單機併發環境下,還是下面要介紹的管程類工具才是利器。

管程及其他工具

其實最早的操作系統中,解決併發問題用的是信號量,信號量通過兩個原子操作wait(S),和signal(S)(俗稱P,V操作)來實現訪問資源互斥和同步。比如下面這個小例子:

//整型信號量定義
int S;

//P操作
wait(S){
  while(S<=0);
  S--;
}

//V操作
signal(S){
  S++;
}

雖然信號量方便有效,但信號量要對每個共享資源都實現對應的P和V操作,這使得併發編程中可能要出現大量的P,V操作,並且這部分內容難以抽象出來。

為了更好地實現同步互斥,於是就產生了管程(即Monitor,也有翻譯為監視器),值得一提的是,管程也有幾種模型,分別是:Hasen模型,Hoare模型和MESA模型。其中MESA模型應用最廣泛,java也是參考自MESA模型。這裏簡單介紹下管程的理論知識,這部分內容參考自進程同步機制—–為進程併發執行保駕護航,希望了解更多管程理論知識的童鞋可以看看。

我們來通過一個經典的生產-消費隊列來解釋,如下圖

我們先解釋下圖中右半部分的內容,右上角有一個等待調用的線程隊列,管程中每次只能有一個線程在執行任務,所以多個任務需要等待。然後是各個名詞的意思,生產-消費需要往隊列寫入和取出東西,這裏的隊列就是共享變量對共享資源進行操作稱之為過程(入隊和出隊兩個過程)。而向隊列寫入和取出是有條件的,寫入的時候隊列必須是非滿的,取出的時候隊列必須是非空的,這兩個條件被稱為條件變量

然後再來看看左半部分的內容,假設線程T1讀取共享變量(即隊列),此時發現隊列為空(條件變量之一),那麼T1此時需要等待,去哪裡等呢?去條件變量隊列不能為空對應的隊列中去等待。此時另一個線程T2向共享變量隊列寫數據,通過了條件變量隊列不能滿,那麼寫完后就會通知線程T1。但因為管程的限制,管程中只能有一個線程在執行,所以T1線程不能立即執行,它會回到右上角的線程等待隊列等待(不同的管程模型在這裡是有分歧的,比如Hasen模型是立即中斷T2線程讓隊列中下一個線程執行)。

解釋完這個圖,管程的概念也就呼之欲出了,

hansen對管程的定義如下:一個管程定義了一個數據結構和能力為併發進程所執行(在該數據結構上)的一組操作,這組操作能同步進程和改變管程中的數據。

本質上,管程是對共享資源以及對共享資源的操作抽象成變量和方法,要操作共享變量僅能通過管程提供的方法(比如上面的入隊和出隊)間接訪問。所以你會發現管程其實和面向對象的理念是十分相近的,在java中,主要提供了低層次了synchronized關鍵字和wait(),notify()等方法。同時還提供了高層次的ReenTrantLock和Condition來實現管程模型。

以上~

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

【其他文章推薦】

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

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

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

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

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

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

深入理解 EF Core:EF Core 讀取數據時發生了什麼?

閱讀本文大概需要 11 分鐘。

原文:https://bit.ly/2UMiDLb
作者:Jon P Smith
翻譯:王亮
聲明:我翻譯技術文章不是逐句翻譯的,而是根據我自己的理解來表述的。其中可能會去除一些本人實在不知道如何組織但又不影響理解的句子。

本文將為你詳細描繪 EF Core 從數據庫中讀取數據的“幕後”視圖。我將揭開兩種數據庫讀取方式的面紗:一個是普通的查詢,另一個是使用 AsNoTracking 方法的非跟蹤查詢。我還將通過一個實驗來演示我是如何解決我的一個客戶遇到的性能問題。

我假設你對 EF Core 已經有了一定的認識,但在深入學習之前,我們先來了解一下如何使用 EF Core,以確保我們已經掌握了一些基本知識。這是一個“深入研究”的課題,所以我準備大量的技術細節,希望我的描述方式你能理解。

本文是“深入理解 EF Core”系列中的第一篇。以下是本系列文章列表:

  • 當 EF Core 從數據庫讀取數據時發生了什麼?(本文)
  • 當 EF Core 寫入數據到數據庫時發生了什麼?(敬請期待)

概要

  • EF Core 有兩種方法從數據庫中讀取數據(也稱為查詢):普通 LINQ 查詢和包含 AsNoTracking 方法的非跟蹤 LINQ 查詢。
  • 這兩種方法查詢的返回類(被稱為實體類),它連接的其它的實體類(即所謂的導航屬性)也被同時加載,但這兩種法如何連接及連接的內容是不一樣的。
  • 普通查詢接受的是 DbContext 執行讀取時所有數據的副本——此時的實體類稱為被跟蹤。這允許加載的實體類參与數據庫的更新操作。
  • 普通查詢還會有一些其它的複雜底層實現,稱為關係修補(fixup),用於描述讀入的實體類和其他被跟蹤實體之間的連接關係。
  • AsNoTracked 非跟蹤查詢沒有副本,所以它沒有被跟蹤——這意味着它比普通查詢更快。這也意味着它不會用於數據庫的寫操作。
  • 最後,我將展示 EF Core 普通查詢中一個鮮為人知的特性,以此作為示例,說明通過導航屬性連接實體類的關係是多麼智能。

EF Core 如何讀取數據庫數據

提示:如果你已經對 EF Core 有一定的認識,那麼你可以跳過這一節,這部分只是一個如何讀取數據庫的例子。

為了能讓你更好地理解,我先描述一個數據庫結構,然後再給出一個簡單的數據庫讀取示例。下面是一些基本表的結構和它們之間的關係。

這些表被映射到具有類似名稱的類,例如 Book、BookAuthor、Author,這些類的屬性名稱與表的字段名稱相同。由於篇幅有限,我不打算展開來講這些類,但您可以在我的 GitHub 倉庫[1]中查看這些類。

EF Core 讀取數據庫需要下面五部分:

  1. 數據庫服務器,如 SQL server, Sqlite, PostgreSQL 等。
  2. 具有數據的數據庫。
  3. 映射到數據表的類(稱為實體類)。
  4. 一個繼承 DbContext 的類,該類包含 EF Core 的配置。
  5. 最後,從數據庫讀取數據的命令。

下面的單元測試代碼來自我的 GitHub 創庫[2],展示了一個簡單的示例,它從現有數據庫中讀取 4 個 Book 實體及其關聯的 BookAuthor 和 Authors 實體。

倉庫地址:https://bit.ly/2Yza7QQ

[Fact]
public void TestBookCountAuthorsOk()
{
    //SETUP
    var options = SqliteInMemory.CreateOptions<EfCoreContext>();
    //code to set up the database with four books, two with the same Author
    using (var context = new EfCoreContext(options))
    {
        //ATTEMPT
        var books = context.Books
            .Include(r => r.AuthorsLink)
            .ThenInclude(r => r.Author)
            .ToList();

        //VERIFY
        books.Count.ShouldEqual(4);
        books.SelectMany(x => x.AuthorsLink.Select(y => y.Author))
            .Distinct().Count().ShouldEqual(3);
    }
}

現在,如果我們將單元測試代碼對應到上面的 5 部分,結果是這樣的:

  1. 數據庫服務器——第 5 行:我選擇了一個 Sqlite 數據庫服務器,在本例中是 SqliteInMemory.CreateOptions 方法,它使用我的一個 NuGet 包 EfCore.TestSupport 創建了一個內存數據庫(內存中的數據庫對於單元測試非常有用,因為你可以為這個測試建立一個新的空數據庫)。
  2. 具有數據的數據庫——第 6 行:我將在下一篇文章介紹數據是如何寫入數據庫的,現在假設有一個數據庫包含 4 本書信息,其中兩本書的作者是同一個人。
  3. 實體類——代碼里這裏沒有展示,但是你可以在這裏查看這些類[1]。其中有一個 Books 實體類,通過一個名為 BookAuhor 的實體類多對多關聯 Authors 實體類。
  4. 一個繼承 DbContext 的類——第 7 行:EfCoreContext 類繼承了 DbContext 類並配置了從類到數據庫的映射關係(你可以在我的 GitHub 倉庫[3] 中查看該類)。
  5. 從數據庫讀取數據的命令——第 10 到 13 行,這是一個查詢:
    • 第 10 行 — context 為 EfCoreContext 的實例,通過它訪問你的數據庫,.Books 表示您希望訪問 Books 表。
    • 第 11 行 — Include 被稱為貪婪加載,它告訴 EF Core 當它加載 Books 時,也應該加載關聯到的所有 BookAuthor 實體類。
    • 第 12 行 — ThenInclude 是繼續貪婪加載,它告訴 EF Core 當它加載一個 BookAuthor 時,它也應該加載關聯到該 BookAuthor 的 Author 實體類。

所有這一切查詢出來是一個結果集,其中有普通屬性,像 Books 的 Title 屬性;有關聯實體類的導航屬性,像 Books 的 AuthorsLink 屬性。

這個示例稱為查詢或讀取,也是四種數據庫訪問類型之一,即 CRUD(新增、讀取、更新和刪除)。我將在下一篇文章中介紹新增和更新。

EF Core 如何表示讀取的數據

當你查詢數據庫時,EF Core 會將數據庫返回的數據轉換為實體類並填充導航屬性的值。在本節中,我們將研究兩種類型的查詢步驟——普通查詢(即沒有 AsNoTracking 方法,也稱為讀寫查詢)和添加了 AsNoTracking 方法的非跟蹤查詢(稱為只讀查詢)。

我們先來看一下最初 LINQ 語句是如何轉換成數據庫相應的查詢命令然後返回數據的。對於我們將要看到的兩種類型的查詢來說,這是很常見的操作。關於查詢的第一部分,請參見下圖。

有一些非常複雜的代碼將你的 LINQ 轉換為數據庫查詢命令,但這些內部細節我們不必關心。如果你的 LINQ 不能被翻譯,你會從 EF Core 得到一個異常消息,其中包含類似“不能被翻譯”的描述詞語。此外,當數據返回時,像 Value Converters[4] 這樣的特性可能會調整數據。

本節展示了查詢的第一部分,其中 LINQ 被轉換為數據庫命令並返回所有正確的值。現在我們來看查詢的第二部分,在這裏 EF Core 獲取返回值並將它們轉換為實體類的實例,並填充導航屬性。我們將分別看看兩種類型的查詢。

1. 普通查詢(讀寫查詢)

普通查詢讀取數據的方式可以修改數據並更新到數據庫,這就是我將其稱為讀寫查詢的原因。它不會自動更新數據(請參閱下一篇文章,了解如何寫入數據庫)。如果你要更新數據,你的查詢必須是讀寫查詢。

我在介紹中給出的示例執行的是一個普通讀寫查詢,讀取帶有 AuthorsLink 實例的示例。下面是該示例的查詢部分的代碼:

var books = context.Books
    .Include(r => r.AuthorsLink)
    .ThenInclude(r => r.Author)
    .ToList();

然後 EF Core 通過三個步驟將這些值轉換並填充含有導航屬性的實體類。下圖显示了這三個步驟以及生成的實體類及其導航屬性的實體類。

讓我們來分析一下這三個步驟:

  1. 創建類並填充數據。它接受數據庫返回的值,並填充非導航(稱為標量)屬性、字段等。在 Book 實體類中,是 BookId(主鍵)、Title 等屬性——參見上圖左下角淺藍色矩形。
  2. 修補關聯關係。首先是填入主鍵和外鍵的信息,它們定義如何相互關聯數據。然後,EF Core 使用這些鍵設置實體類之間的導航屬性(如圖中藍色粗線所示)。這個關係的修補所需的信息不僅是查詢讀入的實體類,它還會查看 DbContext 中跟蹤的每個實體,並填充導航屬性。這是一個強大的功能,但你的被跟蹤實體越多,所需消耗時間也越多——這就是為什麼需要 AsNoTracking 來實現更快的查詢。
  3. 創建跟蹤快照。跟蹤快照是返回給用戶的實體類的一個副本,加上它所隱藏的與每個實體類的關聯關係——若一個實體處於被跟蹤狀態,這意味着它將會發生修改並會寫入到數據庫中。

2. 非跟蹤查詢(只讀查詢)

非跟蹤查詢,即使用 AsNoTracking 方法的查詢,是一個只讀查詢。這意味着,當 SaveChanges 方法被調用時,你讀取的任何內容都不會被寫入數據庫。非跟蹤查詢的查詢效率更高,在下一節中,我將介紹非跟蹤查詢以及與普通查詢的其他區別。

在前文的示例之後,我修改了查詢代碼,添加了下面的 AsNoTracking 方法(請看第 2 行):

var books = context.Books
    .AsNoTracking()
    .Include(r => r.AuthorsLink)
    .ThenInclude(r => r.Author)
    .ToList();

這裏的 LINQ 查詢只有上面的普通查詢的前兩個步驟(沒有第三個步驟)。下圖显示了 AsNoTracking 查詢的步驟。

步驟如下:

  1. 創建類並填充數據。它接受數據庫返回的值,並填充非導航(稱為標量)屬性、字段等。在 Book 實體類中,是 BookId(主鍵)、Title 等屬性——參見上圖左下角淺藍色矩形。
  2. 修補關聯關係。首先是填入主鍵和外鍵的信息,它們定義如何相互關聯數據。然後,EF Core 使用這些鍵設置實體類之間的導航屬性(如圖中藍色粗線所示)。這個關係的修補所需的信息不僅是查詢讀入的實體類,它還會查看 DbContext 中跟蹤的每個實體,並填充導航屬性。這是一個強大的功能,但你的被跟蹤實體越多,所需消耗時間也越多——這就是為什麼需要 AsNoTracking 來實現更快的查詢。

普通查詢和非跟蹤查詢的區別

現在讓我們比較這兩種查詢比較明顯的區別。

  1. 非跟蹤查詢查詢的性能更好。使用非跟蹤查詢查詢的主要原因是性能。非跟蹤查詢查詢表現為:

    • 稍微快一點,使用的內存稍微少一點,因為它不需要創建跟蹤快照。
    • 避免沒有必要的跟蹤快照可以提高 SaveChanges 的性能,因為它不必檢查跟蹤快照以查找更改。
    • 稍微快一點,因為修補關聯關係時沒有所謂的身份解析。這就是為什麼你會得到兩個具有相同數據的 Author 實例。
  2. 非跟蹤查詢修補關聯關係時只鏈接查詢中的實體。在普通查詢中,我已經說過修補關聯關係時連接的是查詢中的實體和當前跟蹤的實體,但是非跟蹤查詢只修補查詢中的實體關係。

  3. 非跟蹤查詢並不總是代表數據庫關係。這兩種類型查詢之間的關係修補的另一個區別是,非跟蹤查詢關係修補更快,它不需要標識的解析。這可以為數據庫中的同一行生成多個實例——見上圖右下角藍色的 Author 實體和註釋。如果只是向用戶显示數據,那麼這種差異並不重要,但是如果具有業務邏輯,那麼多個實例不能正確反映數據的結構,就可能會有問題。

對層級數據有用的關係修補特性

關聯關係修補的步驟是非常智能的,特別是在普通查詢中。下面我想向你展示我是如何利用關係修補的特性來解決一個客戶項目中的性能問題的。

我曾在一家公司工作,那裡的許多數據處理都是層次化結構的,即數據具有一系列深度不確定的關聯關係。問題是我必須先解析整個層次結構,然後才能呈現這些數據。我最初是通過貪婪的方式加載前兩個層級,然後顯式地加載更深的層級來實現這一點的。它可以工作,但是性能非常慢,並且數據庫因大量單數據庫訪問而超載。

這不得不讓我思考解決辦法,如果普通查詢的關係修補那麼智能的話,它能幫助我提高查詢的性能嗎?它可以!讓我給你舉一個公司員工的例子。下圖显示了我們想要加載的公司的層次結構。

你可以接龍式地使用 .Include(x => x.WorksForMe).ThenInclude(x => x.WorksForMe)… 等等來加載所需的層級信息,但結果是一個 .Include(x => x.WorksForMe) 就夠了。因為 EF Core 的關係修補為你做了剩下的事情,這一點很驚奇,但也很有用。

例如,如果我想查詢角色為 Development 的所有員工(每個員工都有一個名為 WhatTheyDo 的屬性和名為 Role 的屬性,該 Role 包含他們工作的部門),我可以這樣編寫代碼:

var devDept = context.Employees
    .Include(x => x.WorksFromMe)
    .Where(x => x.WhatTheyDo.HasFlag(Roles.Development))
    .ToList();

這將創建一個查詢,用於加載角色為 Development 的所有員工,並且在員工實體類上修補與 WorksFoMe 導航屬性(集合)和 Manager 導航屬性(單個)的關係。通過只執行一個查詢,既提高了查詢花費的時間,又減少了數據庫服務器上的負載。

總結

你已經看到了兩種類型的查詢,我稱之為 a)普通的讀寫查詢,和 b) 非跟蹤的只讀查詢。對於每一種查詢類型,我都向你展示了 EF Core “幕後”是如何讀取數據並展示的。他們工作方式的不同也表現出他們的優勢和劣勢。

非跟蹤查詢是只讀查詢的解決方案,因為它比普通讀寫查詢更快。但是您應該記住關係修補的機制,它可以在數據庫只有一個關係的情況下創建類的多個實例。

普通的讀寫查詢是查詢跟蹤實體的解決方案,這意味着你可以在創建、更新和刪除數據時使用它們。普通的讀寫查詢確實會佔用更多的時間和內存資源,但是有一些有用的特性,比如自動鏈接到其他被跟蹤的實體類實例。

我希望這篇文章對您有用。祝你編程快樂!

[1]. https://bit.ly/2MXK3ZY
[2]. https://bit.ly/2Yza7QQ
[3]. https://bit.ly/2Y0UORO
[4]. https://bit.ly/2YEyg8j

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

【其他文章推薦】

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

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

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

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

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

HashSet擴容機制在時間和空間上的浪費,遠大於你的想象

一:背景

1. 講故事

自從這個純內存項目進了大客戶之後,搞得我現在對內存和CPU特別敏感,跑一點數據內存幾個G的上下,特別沒有安全感,總想用windbg抓幾個dump看看到底是哪一塊導致的,是我的代碼還是同事的代碼? 很多看過我博客的老朋友總是留言讓我出一套windbg的系列或者視頻,我也不會呀,沒辦法,人在江湖飄,遲早得挨上幾刀,逼着也得會幾個花架子,廢話不多說,這一篇就來看看 HashSet 是如何擴容的。

二:HashSet的擴容機制

1. 如何查看

了解如何擴容,最好的辦法就是翻看HashSet底層源碼,最粗暴的入口點就是 HashSet.Add 方法。

從圖中可以看到最後的初始化是用 Initialize 的,而且裏面有這麼一句神奇的代碼: int prime = HashHelpers.GetPrime(capacity);,從字面意思看是獲取一個質數,哈哈,有點意思,什麼叫質數? 簡單說就是只能被 1 和 自身 整除的數就叫做質數,那好奇心就來了,一起看看質數是怎麼算的吧! 再次截圖。

從圖中看,HashSet底層為了加速默認定義好了 72 個質數,最大的一個質數是 719w,換句話就是說當元素個數大於 719w 的時候,就只能使用 IsPrime 方法動態計算質數,如下代碼:


public static bool IsPrime(int candidate)
{
	if ((candidate & 1) != 0)
	{
		int num = (int)Math.Sqrt(candidate);
		for (int i = 3; i <= num; i += 2)
		{
			if (candidate % i == 0)
			{
				return false;
			}
		}
		return true;
	}
	return candidate == 2;
}

看完了整個流程,我想你應該明白了,當你第一次Add的時候,默認的空間佔用是 72 個預定義中最小的一個質數 3,看過我之前文章的朋友知道List的默認大小是4,後面就是簡單粗暴的 * 2 處理,如下代碼。


private void EnsureCapacity(int min)
{
	if (_items.Length < min)
	{
		int num = (_items.Length == 0) ? 4 : (_items.Length * 2);
	}
}

2. HashSet 二次擴容探究

當HashSet的個數達到3之後,很顯然要進行二次擴容,這一點不像List用一個 EnsureCapacity 方法搞定就可以了,然後細看一下怎麼擴容。


public static int ExpandPrime(int oldSize)
{
	int num = 2 * oldSize;
	if ((uint)num > 2146435069u && 2146435069 > oldSize)
	{
		return 2146435069;
	}
	return GetPrime(num);
}

從圖中可以看到,最後的擴容是在 ExpandPrime 方法中完成的,流程就是先 * 2, 再取最接近上限的一個質數,也就是 7 ,然後將 7 作為 HashSet 新的Size,如果你非要看演示,我就寫一小段代碼證明一下吧,如下圖:

2. 您嗅出風險了嗎?

<1> 時間上的風險

為了方便演示,我把 72 個預定義的最後幾個質數显示出來。


public static readonly int[] primes = new int[72]
{
	2009191,
	2411033,
	2893249,
	3471899,
	4166287,
	4999559,
	5999471,
	7199369
};

也就是說,當HashSet的元素個數為 2893249 的時候觸發擴容變成了 2893249 * 2 => 5786498 最接近的一個質數為:5999471,也就是 289w 暴增到了 599w,一下子就是 599w -289w = 310w 的空間虛占,這可是增加了兩倍多哦,嚇人不? 下面寫個代碼驗證下。


        static void Main(string[] args)
        {
            var hashSet = new HashSet<int>(Enumerable.Range(0, 2893249));

            hashSet.Add(int.MaxValue);

            Console.Read();
        }

0:000> !clrstack -l

000000B8F4DBE500 00007ffaf00132ae ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 16]
    LOCALS:
        0x000000B8F4DBE538 = 0x0000020e0b8fcc08
0:000> !DumpObj /d 0000020e0b8fcc08
Name:        System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]]
Size:        64(0x40) bytes
File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.0-preview.5.20278.1\System.Collections.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffaf0096d10  4000017        8       System.Int32[]  0 instance 0000020e2025e9f8 _buckets
00007ffaf00f7ad0  4000018       10 ...ivate.CoreLib]][]  0 instance 0000020e2bea1020 _slots
00007ffaeffdf828  4000019       28         System.Int32  1 instance          2893250 _count
0:000> !DumpObj /d 0000020e2025e9f8
Name:        System.Int32[]
Size:        23997908(0x16e2dd4) bytes
Array:       Rank 1, Number of elements 5999471, Type Int32 (Print Array)
Fields:
None


而且最重要的是,這裡是一次性擴容的,而非像redis中實現的那樣漸進式擴容,時間開銷也是大家值得注意的。

<2> 空間上的風險

這個有什麼風險呢?可以看一下:289w 和 599w 兩個HashSet的佔用空間大小,這也是我最敏感的。


        static void Main(string[] args)
        {
            var hashSet1 = new HashSet<int>(Enumerable.Range(0, 2893249));

            var hashSet2 = new HashSet<int>(Enumerable.Range(0, 2893249));
            hashSet2.Add(int.MaxValue);

            Console.Read();
        }

0:000> !clrstack -l
OS Thread Id: 0x4a44 (0)
000000B1B4FEE460 00007ffaf00032ea ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 18]
    LOCALS:
        0x000000B1B4FEE4B8 = 0x000001d13363cc08
        0x000000B1B4FEE4B0 = 0x000001d13363d648

0:000> !objsize 0x000001d13363cc08
sizeof(000001D13363CC08) = 46292104 (0x2c25c88) bytes (System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]])
0:000> !objsize 0x000001d13363d648
sizeof(000001D13363D648) = 95991656 (0x5b8b768) bytes (System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]])

可以看到, hashSet1的佔用: 46292104 / 1024 / 1024 = 44.1M, hashSet2 的佔用 : 95991656 / 1024 / 1024 = 91.5M,一下子就浪費了: 91.5 - 44.1 = 47.4M

如果你真以為僅僅浪費了 47.4M 的話,那你就大錯特錯了,不要忘了底層在擴容的時候,使用新的 size 覆蓋了老的 size,而這個 老的 size 集合在GC還沒有回收的時候會一直佔用堆上空間的,這個能聽得懂嗎? 如下圖:

要驗證的話可以用 windbg 去託管堆上抓一下 Slot[] m_slotsint[] m_buckets 兩個數組,我把代碼修改如下:


    static void Main(string[] args)
    {
        var hashSet2 = new HashSet<int>(Enumerable.Range(0, 2893249));
        hashSet2.Add(int.MaxValue);
        Console.Read();
    }


0:011> !dumpheap -stat
00007ffaf84f7ad0        3    123455868 System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]

這裏就拿 Slot[] 說事,從上面代碼可以看到,託管堆上有三個 Slot[] 數組,這就有意思了,怎麼有三個哈,是不是有點懵逼,沒關係,我們將三個 Slot[] 的地址找出來,一個一個看。


0:011> !DumpHeap /d -mt 00007ffaf84f7ad0
         Address               MT     Size
0000016c91308048 00007ffaf84f7ad0 16743180     
0000016c928524b0 00007ffaf84f7ad0 34719012     
0000016ce9e61020 00007ffaf84f7ad0 71993676  

0:011> !gcroot 0000016c91308048
Found 0 unique roots (run '!gcroot -all' to see all roots).
0:011> !gcroot 0000016c928524b0
Found 0 unique roots (run '!gcroot -all' to see all roots).
0:011> !gcroot 0000016ce9e61020
Thread 2b0c:
    0000006AFAB7E5F0 00007FFAF84132AE ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 15]
        rbp-18: 0000006afab7e618
            ->  0000016C8000CC08 System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]]
            ->  0000016CE9E61020 System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]

從上面可以看到,我通過 gcroot 去找這三個地址的引用根,有兩個是沒有的,最後一個有的自然就是新的 599w 的size,對不對,接下來用 !do 打出這三個地址的值。


0:011> !do 0000016c91308048
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:        16743180(0xff7b0c) bytes
Array:       Rank 1, Number of elements 1395263, Type VALUETYPE (Print Array)
Fields:
None

0:011> !do 0000016c928524b0
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:        34719012(0x211c524) bytes
Array:       Rank 1, Number of elements 2893249, Type VALUETYPE (Print Array)
Fields:
None

0:011> !do 0000016ce9e61020
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:        71993676(0x44a894c) bytes
Array:       Rank 1, Number of elements 5999471, Type VALUETYPE (Print Array)
Fields:
None

從上面的 Rank 1, Number of elements 信息中可以看到,原來託管堆不僅有擴容前的Size :2893249,還有更前一次的擴容Size: 1395263,所以按這種情況算: 託管堆上的總大小近似為: 23.7M + 47.4M + 91.5M = 162.6M,我去,不簡單把。。。 也就是說:託管堆上有 162.6 - 91.5 =71.1M 的未回收垃圾 剛才的 47.4M 的空間虛佔用,總浪費為:118.5M,但願我沒有算錯。。。

3. 有解決方案嗎?

在List中大家可以通過 Capacity 去控制List的Size,但是很遺憾,在 HashSet 中並沒有類似的解決方案,只有一個很笨拙的裁剪方法: TrimExcess,用於將當前Size擴展到最接近的 質數 值, 如下代碼所示:


public void TrimExcess()
{
	int prime = HashHelpers.GetPrime(m_count);
	Slot[] array = new Slot[prime];
	int[] array2 = new int[prime];
	int num = 0;
	for (int i = 0; i < m_lastIndex; i++)
	{
		if (m_slots[i].hashCode >= 0)
		{
			array[num] = m_slots[i];
			int num2 = array[num].hashCode % prime;
			array[num].next = array2[num2] - 1;
			array2[num2] = num + 1;
			num++;
		}
	}
}

應用到本案例就是將 289w 限制到 347w,仍然有 58w的空間佔用。 如下圖:

三: 總結

HashSet的時間和空間上虛占遠比你想象的大很多,而且實占也不小,因為底層用到了雙數組 m_slotsm_buckets,每個Slot還有三個元素: struct Slot { int hashCode;internal int next;internal T value; },所以了解完原理之後謹慎着用吧。

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

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)

系列文章

  1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
  2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
  5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
  11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
  13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
  17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
  18. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)
  19. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)
  20. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(五)
  21. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(一)
  22. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(二)
  23. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(三)
  24. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)
  25. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(五)
  26. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(六)
  27. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(七)
  28. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)
  29. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(九)
  30. 基於 abp vNext 和 .NET Core 開發博客項目 – 終結篇之發布項目

上一篇完成了標籤模塊和友情鏈接模塊的所有功能,本篇來繼續完成博客最後的模塊,文章的管理。

文章列表 & 刪除

先將分頁查詢的列表給整出來,這塊和首頁的分頁列表是類似的,就是多了個Id字段。

先添加兩條路由規則。

@page "/admin/posts"
@page "/admin/posts/{page:int}"

新建返回數據默認QueryPostForAdminDto.cs

//QueryPostForAdminDto.cs
using System.Collections.Generic;

namespace Meowv.Blog.BlazorApp.Response.Blog
{
    public class QueryPostForAdminDto
    {
        /// <summary>
        /// 年份
        /// </summary>
        public int Year { get; set; }

        /// <summary>
        /// Posts
        /// </summary>
        public IEnumerable<PostBriefForAdminDto> Posts { get; set; }
    }
}

//PostBriefForAdminDto.cs
namespace Meowv.Blog.BlazorApp.Response.Blog
{
    public class PostBriefForAdminDto : PostBriefDto
    {
        /// <summary>
        /// 主鍵
        /// </summary>
        public int Id { get; set; }
    }
}

然後添加所需的參數:當前頁碼、限制條數、總頁碼、文章列表返回數據模型。

/// <summary>
/// 當前頁碼
/// </summary>
[Parameter]
public int? page { get; set; }

/// <summary>
/// 限制條數
/// </summary>
private int Limit = 15;

/// <summary>
/// 總頁碼
/// </summary>
private int TotalPage;

/// <summary>
/// 文章列表數據
/// </summary>
private ServiceResult<PagedList<QueryPostForAdminDto>> posts;

然後在初始化函數OnInitializedAsync()中調用API獲取文章數據.

/// <summary>
/// 初始化
/// </summary>
protected override async Task OnInitializedAsync()
{
    var token = await Common.GetStorageAsync("token");
    Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

    // 設置默認值
    page = page.HasValue ? page : 1;

    await RenderPage(page);
}

/// <summary>
/// 點擊頁碼重新渲染數據
/// </summary>
/// <param name="page"></param>
/// <returns></returns>
private async Task RenderPage(int? page)
{
    // 獲取數據
    posts = await Http.GetFromJsonAsync<ServiceResult<PagedList<QueryPostForAdminDto>>>($"/blog/admin/posts?page={page}&limit={Limit}");

    // 計算總頁碼
    TotalPage = (int)Math.Ceiling((posts.Result.Total / (double)Limit));
}

在初始化中判斷page參數,如果沒有值給他設置一個默認值1。RenderPage(int? page)方法是調用API返回數據,並計算出總頁碼值。

最後在頁面上進行數據綁定。

<AdminLayout>
    @if (posts == null)
    {
        <Loading />
    }
    else
    {
        <div class="post-wrap archive">
            <NavLink style="float:right" href="/admin/post"><h3>~~~ 新增文章 ~~~</h3></NavLink>
            @if (posts.Success && posts.Result.Item.Any())
            {
                @foreach (var item in posts.Result.Item)
                {
                    <h3>@item.Year</h3>
                    @foreach (var post in item.Posts)
                    {
                        <article class="archive-item">
                            <NavLink title="刪除" @onclick="@(async () => await DeleteAsync(post.Id))"></NavLink>
                            <NavLink title="編輯" @onclick="@(async () => await Common.NavigateTo($"/admin/post/{post.Id}"))"></NavLink>
                            <NavLink target="_blank" class="archive-item-link" href="@("/post" + post.Url)">@post.Title</NavLink>
                            <span class="archive-item-date">@post.CreationTime</span>
                        </article>
                    }
                }
                <nav class="pagination">
                    @for (int i = 1; i <= TotalPage; i++)
                    {
                        var _page = i;

                        if (page == _page)
                        {
                            <span class="page-number current">@_page</span>
                        }
                        else
                        {
                            <a class="page-number" @onclick="@(() => RenderPage(_page))" href="/admin/posts/@_page">@_page</a>
                        }
                    }
                </nav>
            }
            else
            {
                <ErrorTip />
            }
        </div>
    }
</AdminLayout>

HTML內容放在組件AdminLayout中,當 posts 沒加載完數據的時候显示加載組件<Loading />

在頁面上循環遍歷文章數據和翻頁頁碼,每篇文章標題前面添加兩個按鈕刪除和編輯,同時單獨加了一個新增文章的按鈕。

刪除文章調用DeleteAsync(int id)方法,需要傳遞參數,當前文章的id。

新增和編輯按鈕都跳轉到”/admin/post”頁面,當編輯的時候將id也傳過去即可,路由規則為:”/admin/post/{id}”。

刪除文章“方法如下:

/// <summary>
/// 刪除文章
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private async Task DeleteAsync(int id)
{
    // 彈窗確認
    bool confirmed = await Common.InvokeAsync<bool>("confirm", "\n真的要幹掉這篇該死的文章嗎");

    if (confirmed)
    {
        var response = await Http.DeleteAsync($"/blog/post?id={id}");

        var result = await response.Content.ReadFromJsonAsync<ServiceResult>();

        if (result.Success)
        {
            await RenderPage(page);
        }
    }
}

刪除之前進行二次確認,避免誤刪,當確認刪除之後調用刪除文章API,最後重新渲染數據即可。

新增 & 更新文章

完成了後台文章列表的查詢和刪除,現在整個博客模塊功能就差新增和更新文章了,勝利就在前方,沖啊。

這塊的開發工作耗費了我太多時間,因為想使用 markdown 來寫文章,找了一圈下來沒有一個合適的組件,所以退而求次只能選擇現有的markdown編輯器來實現了。

我這裏選擇了開源的編輯器Editor.md,有需要的可以去 Github 自己下載,https://github.com/pandao/editor.md 。

將下載的資源包解壓放在 wwwroot 文件夾下,默認是比較大的,而且還有很多示例文件,我已經將其精簡了一番,可以去我 Github 下載使用。

先來看下最終的成品效果吧。

是不是感覺還可以,廢話不多說,接下里告訴大家如何實現。

在 Admin 文件夾下添加post.razor組件,設置路由,並且引用一個樣式文件,在頁面中引用樣式文件好像不太符合標準,不過無所謂了,這個後台就自己用,而且還就這一個頁面用得到。

@page "/admin/post"
@page "/admin/post/{id:int}"

<link href="./editor.md/css/editormd.css" rel="stylesheet" />

<AdminLayout>
    ...
</AdminLayout>

把具體HTML內容放在組件AdminLayout中。

因為新增和編輯放在同一個頁面上,所以當id參數不為空的時候需要添加一個id參數,同時默認一進來就讓頁面显示加載中的組件,當頁面和數據加載完成后在显示具體的內容,所以在指定一個布爾類型的是否加載參數isLoading

我們的編輯器主要依賴JavaScript實現的,所以這裏不可避免要使用到JavaScript了。

app.js中添加幾個全局函數。

switchEditorTheme: function () {
    editor.setTheme(localStorage.editorTheme || 'default');
    editor.setEditorTheme(localStorage.editorTheme === 'dark' ? 'pastel-on-dark' : 'default');
    editor.setPreviewTheme(localStorage.editorTheme || 'default');
},
renderEditor: async function () {
	await this._loadScript('./editor.md/lib/zepto.min.js').then(function () {
	    func._loadScript('./editor.md/editormd.js').then(function () {
	        editor = editormd("editor", {
	            width: "100%",
	            height: 700,
	            path: './editor.md/lib/',
	            codeFold: true,
	            saveHTMLToTextarea: true,
	            emoji: true,
	            atLink: false,
	            emailLink: false,
	            theme: localStorage.editorTheme || 'default',
	            editorTheme: localStorage.editorTheme === 'dark' ? 'pastel-on-dark' : 'default',
	            previewTheme: localStorage.editorTheme || 'default',
	            toolbarIcons: function () {
	                return ["bold", "del", "italic", "quote", "ucwords", "uppercase", "lowercase", "h1", "h2", "h3", "h4", "h5", "h6", "list-ul", "list-ol", "hr", "link", "image", "code", "preformatted-text", "code-block", "table", "datetime", "html-entities", "emoji", "watch", "preview", "fullscreen", "clear", "||", "save"]
	            },
	            toolbarIconsClass: {
	                save: "fa-check"
	            },
	            toolbarHandlers: {
	                save: function () {
	                    func._shoowBox();
	                }
	            },
	            onload: function () {
	                this.addKeyMap({
	                    "Ctrl-S": function () {
	                        func._shoowBox();
	                    }
	                });
	            }
	        });
	    });
	});
},
_shoowBox: function () {
    DotNet.invokeMethodAsync('Meowv.Blog.BlazorApp', 'showbox');
},
_loadScript: async function (url) {
    let response = await fetch(url);
    var js = await response.text();
    eval(js);
}

renderEditor主要實現了動態加載JavaScript代碼,將markdown編輯器渲染出來。這裏不多說,都是Editor.md示例裏面的代碼。

為了兼容暗黑色主題,這裏還加了一個切換編輯器主題的JavaScript方法,switchEditorTheme

_shoowBox就厲害了,這個方法是調用的.NET組件中的方法,前面我們用過了在Blazor中調用JavaScript,這裏演示了JavaScript中調用Blazor中的組件方法。

現在將所需的幾個參數都添加到代碼中。

/// <summary>
/// 定義一個委託方法,用於組件實例方法調用
/// </summary>
private static Func<Task> action;

/// <summary>
/// 默認隱藏Box
/// </summary>
private bool Open { get; set; } = false;

/// <summary>
/// 修改時的文章Id
/// </summary>
[Parameter]
public int? Id { get; set; }

/// <summary>
/// 格式化的標籤
/// </summary>
private string tags { get; set; }

/// <summary>
/// 默認显示加載中
/// </summary>
private bool isLoading = true;

/// <summary>
/// 文章新增或者修改輸入參數
/// </summary>
private PostForAdminDto input;

/// <summary>
/// API返回的分類列表數據
/// </summary>
private ServiceResult<IEnumerable<QueryCategoryForAdminDto>> categories;

大家看看註釋就知道參數是做什麼的了。

現在我們在初始化函數中將所需的數據通過API獲取到。

/// <summary>
/// 初始化
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
    action = ChangeOpenStatus;

    var token = await Common.GetStorageAsync("token");
    Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

    if (Id.HasValue)
    {
        var post = await Http.GetFromJsonAsync<ServiceResult<PostForAdminDto>>($"/blog/admin/post?id={Id}");

        if (post.Success)
        {
            var _post = post.Result;
            input = new PostForAdminDto
            {
                Title = _post.Title,
                Author = _post.Author,
                Url = _post.Url,
                Html = _post.Html,
                Markdown = _post.Markdown,
                CategoryId = _post.CategoryId,
                Tags = _post.Tags,
                CreationTime = _post.CreationTime
            };

            tags = string.Join(",", input.Tags);
        }
    }
    else
    {
        input = new PostForAdminDto()
        {
            Author = "阿星Plus",
            CreationTime = DateTime.Now
        };
    }

    categories = await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryCategoryForAdminDto>>>("/blog/admin/categories");

    // 渲染編輯器
    await Common.InvokeAsync("window.func.renderEditor");

    // 關閉加載
    isLoading = !isLoading;
}

action是一個異步的委託,在初始化中執行了ChangeOpenStatus方法,這個方法等會說,然後獲取localStorage中token的值。

通過參數Id是否有值來判斷當前是新增文章還是更新文章,如果有值就是更新文章,這時候需要根據id去將文章的數據拿到賦值給PostForAdminDto對象展示在頁面上,如果沒有可以添加幾個默認值給PostForAdminDto對象。

因為文章需要分類和標籤的數據,同時這裏將分類的數據也查出來,標籤默認是List列表,將其轉換成字符串類型。

但完成上面操作后,調用JavaScript方法renderEditor渲染渲染編輯器,最後關閉加載,显示頁面。

現在來看看頁面。

<AdminLayout>
    @if (isLoading)
    {
        <Loading />
    }
    else
    {
        <div class="post-box">
            <div class="post-box-item">
                <input type="text" placeholder="標題" autocomplete="off" @bind="@input.Title" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
                <input type="text" placeholder="作者" autocomplete="off" @bind="@input.Author" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
            </div>
            <div class="post-box-item">
                <input type="text" placeholder="URL" autocomplete="off" @bind="@input.Url" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
                <input type="text" placeholder="時間" autocomplete="off" @bind="@input.CreationTime" @bind:format="yyyy-MM-dd HH:mm:sss" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
            </div>
            <div id="editor">
                <textarea style="display:none;">@input.Markdown</textarea>
            </div>

            <Box OnClickCallback="@SubmitAsync" Open="@Open" ButtonText="發布">
                <div class="box-item">
                    <b>分類:</b>
                    @if (categories.Success && categories.Result.Any())
                    {
                        @foreach (var item in categories.Result)
                        {
                            <label><input type="radio" name="category" value="@item.Id" @onchange="@(() => { input.CategoryId = item.Id; })" checked="@(item.Id == input.CategoryId)" />@item.CategoryName</label>
                        }
                    }
                </div>
                <div class="box-item"></div>
                <div class="box-item">
                    <b>標籤:</b>
                    <input type="text" @bind="@tags" @bind:event="oninput" />
                </div>
            </Box>
        </div>
    }
</AdminLayout>

添加了四個input框,分別用來綁定標題、作者、URL、時間,<div id="editor"></div>中為編輯器所需。

然後我這裏還是把之前的彈窗組件搞出來了,執行邏輯不介紹了,在彈窗組件中自定義显示分類和標籤的內容,將獲取到的分類和標籤綁定到具體位置。

每個分類都是一個radio標籤,並且對應一個點擊事件,點哪個就把當前分類的Id賦值給PostForAdminDto對象。

所有的input框都使用@bind@bind:event綁定數據和獲取數據。

Box彈窗組件這裏自定義了按鈕文字,ButtonText="發布"

/// <summary>
/// 改變Open狀態,通知組件渲染
/// </summary>
private async Task ChangeOpenStatus()
{
    Open = true;

    var markdown = await Common.InvokeAsync<string>("editor.getMarkdown");
    var html = await Common.InvokeAsync<string>("editor.getHTML");

    if (string.IsNullOrEmpty(input.Title) || string.IsNullOrEmpty(input.Url) ||
        string.IsNullOrEmpty(input.Author) || string.IsNullOrEmpty(markdown) ||
        string.IsNullOrEmpty(html))
    {
        await Alert();
    }

    input.Html = html;
    input.Markdown = markdown;

    StateHasChanged();
}

/// <summary>
/// 暴漏給JS執行,彈窗確認框
/// </summary>
[JSInvokable("showbox")]
public static void ShowBox()
{
    action.Invoke();
}
/// <summary>
/// alert提示
/// </summary>
/// <returns></returns>
private async Task Alert()
{
    Open = false;

    await Common.InvokeAsync("alert", "\n好像漏了點什麼吧");
    return;
}

現在可以來看看ChangeOpenStatus方法了,這個是改變當前彈窗狀態的一個方法。為什麼需要這個方法呢?

因為在Blazor中JavaScript想要調用組件內的方法,方法必須是靜態的,那麼只能通過這種方式去實現了,在靜態方法是不能夠直接改變彈窗的狀態值的。

其實也可以不用這麼麻煩,因為我在編輯器上自定義了一個按鈕,為了好看一些所以只能曲折一點,嫌麻煩的可以直接在頁面上搞個按鈕執行保存數據邏輯也是一樣的。

使用JSInvokableAttribute需要在_Imports.razor中添加命名空間@using Microsoft.JSInterop

ChangeOpenStatus中獲取到文章內容:HTML和markdown,賦值給PostForAdminDto對象,要先進行判斷頁面上的幾個參數是否有值,沒值的話給出提示執行Alert()方法,最後使用StateHasChanged()通知組件其狀態已更改。

Alert方法就是調用原生的JavaScriptalert方法,給出一個提示。

ShowBox就是暴漏給JavaScript的方法,使用DotNet.invokeMethodAsync('Meowv.Blog.BlazorApp', 'showbox');進行調用。

那麼現在一切都正常進行的情況下,點擊編輯器上自定義的保存按鈕,頁面上值不為空的情況下就會彈出我們的彈窗組件Box

最後在彈窗組件的回調方法中執行新增文章還是更新文章。

/// <summary>
/// 確認按鈕點擊事件
/// </summary>
/// <returns></returns>
private async Task SubmitAsync()
{
    if (string.IsNullOrEmpty(tags) || input.CategoryId == 0)
    {
        await Alert();
    }

    input.Tags = tags.Split(",");

    var responseMessage = new HttpResponseMessage();

    if (Id.HasValue)
        responseMessage = await Http.PutAsJsonAsync($"/blog/post?id={Id}", input);
    else
        responseMessage = await Http.PostAsJsonAsync("/blog/post", input);

    var result = await responseMessage.Content.ReadFromJsonAsync<ServiceResult>();
    if (result.Success)
    {
        await Common.NavigateTo("/admin/posts");
    }
}

打開彈窗后執行回調事件之前還是要判斷值是否為空,為空的情況下還是給出alert提示,此時將tags標籤還是轉換成List列表,根據Id是否有值去執行新增數據或者更新數據,最終成功后跳轉到文章列表頁。

本片到這裏就結束了,主要攻克了在Blazor中使用Markdown編輯器實現新增和更新文章,這個系列差不多就快結束了,預計還有2篇的樣子,感謝各位的支持。

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

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

【其他文章推薦】

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

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

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

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

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

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

重學 Java 設計模式:實戰代理模式「模擬mybatis-spring中定義DAO接口,使用代理類方式操作數據庫原理實現場景」

作者:小傅哥

博客:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!

一、前言

難以跨越的瓶頸期,把你拿捏滴死死的!

編程開發學習過程中遇到的瓶頸期,往往是由於看不到前進的方向。這個時候你特別希望能有人告訴你,你還欠缺些什麼朝着哪個方向努力。而導致這一問題的主要原因是由於日常的業務開發太過於複製過去,日復一日的重複。沒有太多的挑戰,也沒參与過較大體量的業務場景,除了這些開發場景因素外,還有缺少組內的技術氛圍和技術分享,沒有人做傳播和佈道者,也缺少自己對各項技術學習的熱情,從而導致一直遊盪在瓶頸之下,難以提升。

小公司與大公司,選擇哪個?

刨除掉薪資以外你會選擇什麼,是不有人建議小公司,因為可以接觸到各個環境,也有人建議大公司,因為正規體量大可以學習到更多。有些時候你的技術成長緩慢也是因為你的不同選擇而導致的,小公司確實要接觸各個環境,但往往如果你所做的業務體量不高,那麼你會用到的技術棧就會相對較少,同時也會技術棧研究的深度也會較淺。大公司中確實有時候你不需要去關心一個集群的部署和維護、一个中間件的開發、全套服務監控等等,但如果你願意了解這些技術在內部都是公開的,你會擁有無限的技術營養可以補充。而這最主要的是提升視野和事業。

除了業務中的CRUD開發,有些技術你真的很難接觸到!

可能很多小夥伴認為技術開發就是承接下產品需求,寫寫CRUD,不會的百度一下,就完事了,總覺得別人問的東西像再造火箭一樣。但在高體量、高併發的業務場景下,每一次的壓測優化,性能提升,都像在研究一道數學題一樣,反覆的錘鍊,壓榨性能。不斷的深究,找到最合適的設計。除了這些優化提升外,還有那麼廣闊的技術體系棧,都可能因為你只是注重CRUD而被忽略;字節碼編程、領域驅動設計架構、代理模式中間件開發、JVM虛擬機實現原理等等。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. Spring 4.3.24.RELEASE
  4. 涉及工程三個,可以通過關注公眾號bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)
工程 描述
itstack-demo-design-12-00 模擬MyBatis開發中間件代理類部分

三、代理模式介紹

代理模式有點像老大和小弟,也有點像分銷商。主要解決的是問題是為某些資源的訪問、對象的類的易用操作上提供方便使用的代理服務。而這種設計思想的模式經常會出現在我們的系統中,或者你用到過的組件中,它們都提供給你一種非常簡單易用的方式控制原本你需要編寫很多代碼的進行使用的服務類。

類似這樣的場景可以想到;

  1. 你的數據庫訪問層面經常會提供一個較為基礎的應用,以此來減少應用服務擴容時不至於數據庫連接數暴增。
  2. 使用過的一些中間件例如;RPC框架,在拿到jar包對接口的描述后,中間件會在服務啟動的時候生成對應的代理類,當調用接口的時候,實際是通過代理類發出的socket信息進行通過。
  3. 另外像我們常用的MyBatis,基本是定義接口但是不需要寫實現類,就可以對xml或者自定義註解里的sql語句進行增刪改查操作。

四、案例場景模擬

在本案例中我們模擬實現mybatis-spring中代理類生成部分

對於Mybatis的使用中只需要定義接口不需要寫實現類就可以完成增刪改查操作,有疑問的小夥伴,在本章節中就可以學習到這部分知識。解析下來我們會通過實現一個這樣的代理類交給spring管理的核心過程,來講述代理類模式。

這樣的案例場景在實際的業務開發中其實不多,因為這是將這種思想運用在中間件開發上,而很多小夥伴經常是做業務開發,所以對Spring的bean定義以及註冊和對代理以及反射調用的知識了解的相對較少。但可以通過本章節作為一個入門學習,逐步了解。

五、代理類模式實現過程

接下來會使用代理類模式來模擬實現一個Mybatis中對類的代理過程,也就是只需要定義接口,就可以關聯到方法註解中的sql語句完成對數據庫的操作。

這裏需要注意一些知識點;

  1. BeanDefinitionRegistryPostProcessor,spring的接口類用於處理對bean的定義註冊。
  2. GenericBeanDefinition,定義bean的信息,在mybatis-spring中使用到的是;ScannedGenericBeanDefinition 略有不同。
  3. FactoryBean,用於處理bean工廠的類,這個類非常見。

1. 工程結構

itstack-demo-design-12-00
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo.design
    │   │       ├── agent
    │   │       │	├── MapperFactoryBean.java
    │   │       │	├── RegisterBeanFactory.java
    │   │       │	└── Select.java
    │   │       └── IUserDao.java
    │   └── resources	
    │       └── spring-config.xml
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java

代理模式中間件模型結構

  • 此模型中涉及的類並不多,但都是抽離出來的核心處理類。主要的事情就是對類的代理和註冊到spring中。
  • 上圖中最上面是關於中間件的實現部分,下面對應的是功能的使用。

2. 代碼實現

2.1 自定義註解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Select {

    String value() default "";  // sql語句

}
  • 這裏我們定義了一個模擬mybatis-spring中的自定義註解,用於使用在方法層面。

2.2 Dao層接口

public interface IUserDao {

    @Select("select userName from user where id = #{uId}")
    String queryUserInfo(String uId);

}
  • 這裏定義一個Dao層接口,並把自定義註解添加上。這與你使用的mybatis組件是一樣的。
  • 2.1和2.2是我們的準備工作,後面開始實現中間件功能部分。

2.3 代理類定義

public class MapperFactoryBean<T> implements FactoryBean<T> {

    private Logger logger = LoggerFactory.getLogger(MapperFactoryBean.class);

    private Class<T> mapperInterface;

    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public T getObject() throws Exception {
        InvocationHandler handler = (proxy, method, args) -> {
            Select select = method.getAnnotation(Select.class);
            logger.info("SQL:{}", select.value().replace("#{uId}", args[0].toString()));
            return args[0] + ",小傅哥,bugstack.cn - 沉澱、分享、成長,讓自己和他人都能有所收穫!";
        };
        return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{mapperInterface}, handler);
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterface;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

}
  • 如果你有閱讀過mybatis源碼,是可以看到這樣的一個類;MapperFactoryBean,這裏我們也模擬一個這樣的類,在裏面實現我們對代理類的定義。
  • 通過繼承FactoryBean,提供bean對象,也就是方法;T getObject()
  • 在方法getObject()中提供類的代理以及模擬對sql語句的處理,這裏包含了用戶調用dao層方法時候的處理邏輯。
  • 還有最上面我們提供構造函數來透傳需要被代理類,Class<T> mapperInterface,在mybatis中也是使用這樣的方式進行透傳。
  • 另外getObjectType()提供對象類型反饋,以及isSingleton()返回類是單例的。

2.4 將Bean定義註冊到Spring容器

public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor {
    
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(MapperFactoryBean.class);
        beanDefinition.setScope("singleton");
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(IUserDao.class);

        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, "userDao");
        BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        // left intentionally blank
    }

}
  • 這裏我們將代理的bean交給spring容器管理,也就可以非常方便讓我們可以獲取到代理的bean。這部分是spring中關於一個bean註冊過程的源碼。
  • GenericBeanDefinition,用於定義一個bean的基本信息setBeanClass(MapperFactoryBean.class);,也包括可以透傳給構造函數信息addGenericArgumentValue(IUserDao.class);
  • 最後使用 BeanDefinitionReaderUtils.registerBeanDefinition,進行bean的註冊,也就是註冊到DefaultListableBeanFactory中。

2.5 配置文件spring-config

<?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-3.0.xsd"
       default-autowire="byName">

    <bean id="userDao" class="org.itstack.demo.design.agent.RegisterBeanFactory"/>

</beans>
  • 接下來在配置文件中添加我們的bean配置,在mybatis的使用中一般會配置掃描的dao層包,這樣就可以減少這部分的配置。

3. 測試驗證

3.1 編寫測試類

@Test
public void test_IUserDao() {
    BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
    IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);
    String res = userDao.queryUserInfo("100001");
    logger.info("測試結果:{}", res);
}
  • 測試的過程比較簡單,通過加載Bean工廠獲取我們的代理類的實例對象,之後調用方法返回結果。
  • 那麼這個過程你可以看到我們是沒有對接口先一個實現類的,而是使用代理的方式給接口生成一個實現類,並交給spring管理。

3.2 測試結果

23:21:57.551 [main] DEBUG o.s.core.env.StandardEnvironment - Adding PropertySource 'systemProperties' with lowest search precedence
...
23:21:57.858 [main] DEBUG o.s.c.s.ClassPathXmlApplicationContext - Unable to locate LifecycleProcessor with name 'lifecycleProcessor': using default [org.springframework.context.support.DefaultLifecycleProcessor@7bc1a03d]
23:21:57.859 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'lifecycleProcessor'
23:21:57.860 [main] DEBUG o.s.c.e.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source
23:21:57.861 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'userDao'
23:21:57.915 [main] INFO  o.i.d.design.agent.MapperFactoryBean - SQL:select userName from user where id = 100001
23:21:57.915 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:100001,小傅哥,bugstack.cn - 沉澱、分享、成長,讓自己和他人都能有所收穫!

Process finished with exit code 0
  • 從測試結果可以看到,我們打印了SQL語句,這部分語句是從自定義註解中獲取的;select userName from user where id = 100001,我們做了簡單的適配。在mybatis框架中會交給SqlSession的實現類進行邏輯處理返回操作數據庫數據
  • 而這裏我們的測試結果是一個固定的,如果你願意更加深入的研究可以嘗試與數據庫操作層進行關聯,讓這個框架可以更加完善。

六、總結

  • 關於這部分代理模式的講解我們採用了開發一個關於mybatis-spring中間件中部分核心功能來體現代理模式的強大之處,所以涉及到了一些關於代理類的創建以及spring中bean的註冊這些知識點,可能在平常的業務開發中都是很少用到的,但是在中間件開發中確實非常常見的操作。
  • 代理模式除了開發中間件外還可以是對服務的包裝,物聯網組件等等,讓複雜的各項服務變為輕量級調用、緩存使用。你可以理解為你家裡的電燈開關,我們不能操作220v電線的人肉連接,但是可以使用開關,避免觸電。
  • 代理模式的設計方式可以讓代碼更加整潔、乾淨易於維護,雖然在這部分開發中額外增加了很多類也包括了自己處理bean的註冊等,但是這樣的中間件復用性極高也更加智能,可以非常方便的擴展到各個服務應用中。

七、推薦閱讀

  • 1. 重學 Java 設計模式:實戰工廠方法模式(多種類型商品發獎場景)
  • 2. 重學 Java 設計模式:實戰建造者模式(裝修物料組合套餐選配場景)
  • 3. 重學 Java 設計模式:實戰原型模式(多套試每人題目和答案亂序場景)
  • 4. 重學 Java 設計模式:實戰橋接模式(多支付渠道「微信支付寶」與多支付模式「刷臉、指紋」場景)
  • 5. 重學 Java 設計模式:實戰組合模式(營銷差異化人群發券決策樹引擎搭建場景)
  • 6. 重學 Java 設計模式:實戰外觀模式「基於SpringBoot開發門面模式中間件,統一控制接口白名單場景」

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

【其他文章推薦】

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

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

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

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

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

Spring系列.AOP原理簡析

Spring AOP使用簡介

Spring的兩大核心功能是IOC和AOP。當我們使用Spring的AOP功能時是很方便的。只需要進行下面的配置即可。

@Component
@Aspect
public class MyAspect {

//PointCut匹配的方法必須是Spring中bean的方法
//Pointcut可以有下列方式來定義或者通過&& || 和!的方式進行組合.
//下面定義的這些切入點就可以通過&& ||組合

private static Logger logger = LoggerFactory.getLogger(MyAspect.class);

//*:代表方法的返回值可以是任何類型
//整個表達式匹配controller包下面任何的的echo方法,方法入參樂意是任意
@Pointcut("execution(* com.csx.demo.spring.boot.controller.*.echo(..))")
public void pointCut1(){}

@Before("pointCut1()")
public void befor(){
    logger.info("前置通知vvvv...");
    logger.info("我要做些事情...");
}
}

然後再開啟註解

//自動選擇合適的AOP代理
//傳統xml這樣配置:<aop:aspectj-autoproxy/>

//exposeProxy = true屬性設置成true,意思是將動態生成的代理類expose到AopContext的ThreadLocal線程
//可以通過AopContext.currentProxy();獲取到生成的動態代理類。

//proxyTargetClass屬性設置動態代理使用JDK動態代理還是使用CGlib代理,設置成true是使用CGlib代理,false的話是使用JDK動態代理

//注意:如果使用Spring Boot的話,下面的配置可以不需要。AopAutoConfiguration這個自動配置類中已經自動開啟了AOP
//默認使用CGLIB動態代理,Spring Boot配置的優先級高於下面的配置

@Configuration
@EnableAspectJAutoProxy(exposeProxy = true,proxyTargetClass = false)
public class AopConfig {

}

通上面的配置,當我們調用controller包下面的任何類的echo方法時就會觸發前置通知。其實這個說法不是很準確。因為我們調用的類已經不是我們自己寫的類了。而是Spring框架通過動態代理生成的類。

稍微了解一點Spring AOP的同學都會知道Spring的AOP是通過動態代理實現的。那Spring是怎麼生成動態代理類,並將Advice織入代理類的呢?整個流程是怎樣的呢?下面就分析下Spring生成動態代理類的過程。

需要說明下的是,本博客旨在梳理整個AOP動態代理的過程,細節方面需要大家自己去看。

@EnableAspectJAutoProxy幹了些啥

如果讓你從頭開始研究下AOP的原理,你是不是一頭霧水,根本不知道從何入手。但其實看Spring的代碼有個小技巧:如果你要研究一個功能,可以從開啟這個功能的Enable註解開始看。Spring的很多功能都是通過Enable註解開啟的,所以這些註解肯定和這些功能相關。

那麼這邊我們可以從@EnableAspectJAutoProxy這個註解開始着手,看下這個註解做了些什麼操作。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
    //設置為true的話就一直使用cglib動態代理
    //設置為false的話,對於接口使用jdk動態代理,對於類使用cglib代理
	boolean proxyTargetClass() default false;
	boolean exposeProxy() default false;
}

看到上面的@Impoer註解,我們很自然就會想到去看AspectJAutoProxyRegistrar這個類。

//AspectJAutoProxyRegistrar源代碼
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

    /**
     * 主要作用也就是註冊AnnotationAwareAspectJAutoProxyCreator
     */
    @Override
    public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //註冊AnnotationAwareAspectJAutoProxyCreator的BeanDefinition
        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

        AnnotationAttributes enableAspectJAutoProxy =
                AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
        if (enableAspectJAutoProxy != null) {
            //給上面註冊的BeanDefinition中添加兩個屬相proxyTargetClass和exposeProxy
            if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
                AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
            }
            if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
                AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
            }
        }
    }

}

我們可以看到上面的類中也沒幹什麼特別的事情,就註冊了一個BeanDefinition。如果我們點進去看下AnnotationAwareAspectJAutoProxyCreator這個類的源代碼會發現這個類竟然實現了InstantiationAwareBeanPostProcessor這個接口。熟悉Spring尿性的朋友會敏銳的感覺到Spring可能是在postProcessBeforeInstantiation或者postProcessAfterInstantiation這些方法中對Bean進行動態代理的。

“大膽假設,小心求證”,讓我們帶着這個猜想去看看AnnotationAwareAspectJAutoProxyCreator到底幹了些什麼?

AnnotationAwareAspectJAutoProxyCreator生成動態代理類

@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
    Object cacheKey = getCacheKey(beanClass, beanName);

    if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {
        if (this.advisedBeans.containsKey(cacheKey)) {
            return null;
        }
        if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return null;
        }
    }
    // 一般不指定CustomTargetSource,所以不會進入這段代碼,所以關鍵代碼在
    // postProcessAfterInitialization中
    // Create proxy here if we have a custom TargetSource.
    // Suppresses unnecessary default instantiation of the target bean:
    // The TargetSource will handle target instances in a custom fashion.
    TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
    if (targetSource != null) {
        if (StringUtils.hasLength(beanName)) {
            this.targetSourcedBeans.add(beanName);
        }
        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
        Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }
    return null;
}

下面是創建動態代理類的關鍵代碼。

@Override
    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
        if (bean != null) {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (this.earlyProxyReferences.remove(cacheKey) != bean) {
                //這邊是創建代碼類的關鍵代碼
                return wrapIfNecessary(bean, beanName, cacheKey);
            }
        }
        return bean;
    }
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
        if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
            return bean;
        }
        if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
            return bean;
        }
        if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
        }

        // Create proxy if we have advice.
        //獲取當前Bean配置的advice,這步是關鍵
        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
        if (specificInterceptors != DO_NOT_PROXY) {
            this.advisedBeans.put(cacheKey, Boolean.TRUE);
            //創建代理
            Object proxy = createProxy(
                    bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
            this.proxyTypes.put(cacheKey, proxy.getClass());
            return proxy;
        }

        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }

層層dedug進去我們能看到下面這段代碼,我們口中常說的JDK動態代理和Cglib動態代理就是在這邊生成的。

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

    @Override
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) {
                throw new AopConfigException("TargetSource cannot determine target class: " +
                        "Either an interface or a target is required for proxy creation.");
            }
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                //生成JDK動態代理
                return new JdkDynamicAopProxy(config);
            }
            //生成Cglib動態代理
            return new ObjenesisCglibAopProxy(config);
        }
        else {
            return new JdkDynamicAopProxy(config);
        }
    }
    /**
     * Determine whether the supplied {@link AdvisedSupport} has only the
     * {@link org.springframework.aop.SpringProxy} interface specified
     * (or no proxy interfaces specified at all).
     */
    private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
        Class<?>[] ifcs = config.getProxiedInterfaces();
        return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0])));
    }

到此,我們已經簡單分析了Spring動態代理類的生成流程。

PS:關於InstantiationAwareBeanPostProcessor接口和BeanPostProcessor接口大家可以自行了解下,這兩個接口是Spring中非常重要的接口。看懂了這兩個接口,Spring很多“神秘”的功能你就能理解了。

簡單總結

通過上面分析,其實我們發現如果不去看AOP動態代理類生成的細節的話,整個Spring AOP的流程還是挺簡單的:

  • @EnableAspectJAutoProxy註解通過AopConfigUtils這個工具類註冊AnnotationAwareAspectJAutoProxyCreator這個類,這個類實現了InstantiationAwareBeanPostProcessor接口,所以會在Bean實例化前後對Bean做一系列額外的操作;
  • AnnotationAwareAspectJAutoProxyCreator的postProcessAfterInitialization中會找出所有和當前Bean相關的Advice,如果找到就創建相應的動態代理類,如果找不到就不生成,返回原始類。

所以整個大流程就這麼簡單。

一些重要類:

  • @EnableAspectJAutoProxy;
  • AspectJAutoProxyRegistrar:註冊AnnotationAwareAspectJAutoProxyCreator
  • AnnotationAwareAspectJAutoProxyCreator:AOP動態代理自動生成的處理類,其他類似的類有AspectJAwareAdvisorAutoProxyCreator和InfrastructureAdvisorAutoProxyCreator等;
  • AopConfigUtils:AOP配置工具類
  • ProxyFactory:代理工廠
  • AopProxy接口:常見實現類ObjenesisCglibAopProxy、JdkDynamicAopProxy

參考

  • https://blog.csdn.net/zhoushimiao1990/article/details/89853368

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

【其他文章推薦】

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

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

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

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

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