環境資訊中心綜合外電;姜唯 編譯;林大利 審校
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※南投搬家公司費用需注意的眉眉角角,別等搬了再說!
※教你寫出一流的銷售文案?
摘錄自2018年11月12日民視報導
印尼第二大城泗水為了鼓勵民眾回收塑膠廢棄物,推動了「用塑膠空瓶當車票」的活動,只要用塑膠空瓶就能換公車車票,免費在市區搭一趟車。這不僅鼓勵民眾回收塑膠廢棄物,也增加大眾運輸的使用率,印尼政府希望藉這項活動,在2020年前讓印尼免受塑膠塑廢棄物之苦。
泗水衛生局官員迪居尼亞托羅表示:「為避免塑膠瓶造成環境污染問題,我們呼籲居民回收塑膠空瓶,用這些空瓶來換公車車票。」
這項活動每天回收平均200公斤塑膠空瓶,周末回收數更高,從活動開始到現在,回收量已累積高達39公噸。
印尼是僅次於中國全球最大塑膠垃圾製造國之一,其中泗水的日常垃圾中,就有15%是塑膠廢棄物。當地政府也表示,這項活動讓塑膠廢棄物具有實用價值,讓民眾更關注這項問題。
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※想知道最厲害的網頁設計公司"嚨底家"!
※別再煩惱如何寫文案,掌握八大原則!
※產品缺大量曝光嗎?你需要的是一流包裝設計!
在前面的文章中,我們學習了如何使用@Import註解向Spring容器中導入bean,可以使用@Import註解快速向容器中導入bean,小夥伴們可以參見《【Spring註解驅動開發】使用@Import註解給容器中快速導入一個組件》。可以在@Import註解中使用ImportSelector接口導入bean,小夥伴們可以參見《【Spring註解驅動開發】在@Import註解中使用ImportSelector接口導入bean》一文。今天,我們就來說說,如何在@Import註解中使用ImportBeanDefinitionRegistrar向容器中註冊bean。
項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation
我們先來看看ImportBeanDefinitionRegistrar是個什麼鬼,點擊進入ImportBeanDefinitionRegistrar源碼,如下所示。
package org.springframework.context.annotation;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.core.type.AnnotationMetadata;
public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
BeanNameGenerator importBeanNameGenerator) {
registerBeanDefinitions(importingClassMetadata, registry);
}
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}
}
由源碼可以看出,ImportBeanDefinitionRegistrar本質上是一個接口。在ImportBeanDefinitionRegistrar接口中,有一個registerBeanDefinitions()方法,通過registerBeanDefinitions()方法,我們可以向Spring容器中註冊bean實例。
Spring官方在動態註冊bean時,大部分套路其實是使用ImportBeanDefinitionRegistrar接口。
所有實現了該接口的類都會被ConfigurationClassPostProcessor處理,ConfigurationClassPostProcessor實現了BeanFactoryPostProcessor接口,所以ImportBeanDefinitionRegistrar中動態註冊的bean是優先於依賴其的bean初始化的,也能被aop、validator等機制處理。
ImportBeanDefinitionRegistrar需要配合@Configuration和@Import註解,@Configuration定義Java格式的Spring配置文件,@Import註解導入實現了ImportBeanDefinitionRegistrar接口的類。
既然ImportBeanDefinitionRegistrar是一個接口,那我們就創建一個MyImportBeanDefinitionRegistrar類,實現ImportBeanDefinitionRegistrar接口,如下所示。
package io.mykit.spring.plugins.register.condition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
/**
* @author binghe
* @version 1.0.0
* @description ImportBeanDefinitionRegistrar的實現類
*/
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
/**
* AnnotationMetadata: 當前類的註解信息
* BeanDefinitionRegistry:BeanDefinition註冊類
* 通過調用BeanDefinitionRegistry接口的registerBeanDefinition()方法,可以將所有需要添加到容器中的bean注入到容器中。
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry){
}
}
可以看到,這裏,我們先創建了MyImportBeanDefinitionRegistrar類的大體框架。接下來,我們在PersonConfig2類上的@Import註解中,添加MyImportBeanDefinitionRegistrar類,如下所示。
@Configuration
@Import({Department.class, Employee.class, MyImportSelector.class, MyImportBeanDefinitionRegistrar.class})
public class PersonConfig2 {
接下來,創建一個Company類,作為測試測試ImportBeanDefinitionRegistrar接口的bean,如下所示。
package io.mykit.spring.plugins.register.bean;
/**
* @author binghe
* @version 1.0.0
* @description 測試ImportBeanDefinitionRegistrar接口的使用
*/
public class Company {
}
接下來,就要實現MyImportBeanDefinitionRegistrar類中的registerBeanDefinitions()方法的邏輯了,添加邏輯后的registerBeanDefinitions()方法如下所示。
/**
* AnnotationMetadata: 當前類的註解信息
* BeanDefinitionRegistry:BeanDefinition註冊類
* 通過調用BeanDefinitionRegistry接口的registerBeanDefinition()方法,可以將所有需要添加到容器中的bean注入到容器中。
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry){
boolean employee = registry.containsBeanDefinition("employee");
boolean department = registry.containsBeanDefinition("department");
if (employee && department){
BeanDefinition beanDefinition = new RootBeanDefinition(Company.class);
registry.registerBeanDefinition("company", beanDefinition);
}
}
registerBeanDefinitions()方法的實現邏輯很簡單,就是判斷Spring容器中是否同時存在以employee命名的bean和以department命名的bean,如果同時存在以employee命名的bean和以department命名的bean,則向Spring容器中注入一個以company命名的bean。
接下來,我們就運行SpringBeanTest類中的testAnnotationConfig7()方法來進行測試,輸出結果信息如下所示。
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig2
io.mykit.spring.plugins.register.bean.Department
io.mykit.spring.plugins.register.bean.Employee
io.mykit.spring.plugins.register.bean.User
io.mykit.spring.plugins.register.bean.Role
person
binghe001
可以看到,在輸出結果中,並沒有看到“company”,這是因為輸出結果中存在io.mykit.spring.plugins.register.bean.Department和io.mykit.spring.plugins.register.bean.Employee,並不存在我們代碼邏輯中的department和employee。所以,我們將registerBeanDefinitions()方法的邏輯稍微修改下,修改后的代碼如下所示。
/**
* AnnotationMetadata: 當前類的註解信息
* BeanDefinitionRegistry:BeanDefinition註冊類
* 通過調用BeanDefinitionRegistry接口的registerBeanDefinition()方法,可以將所有需要添加到容器中的bean注入到容器中。
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry){
boolean employee = registry.containsBeanDefinition(Employee.class.getName());
boolean department = registry.containsBeanDefinition(Department.class.getName());
if (employee && department){
BeanDefinition beanDefinition = new RootBeanDefinition(Company.class);
registry.registerBeanDefinition("company", beanDefinition);
}
}
接下來,我們再次運行SpringBeanTest類中的testAnnotationConfig7()方法來進行測試,輸出結果信息如下所示。
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig2
io.mykit.spring.plugins.register.bean.Department
io.mykit.spring.plugins.register.bean.Employee
io.mykit.spring.plugins.register.bean.User
io.mykit.spring.plugins.register.bean.Role
person
binghe001
company
可以看到,此時輸出了company,說明Spring容器中已經成功註冊了以company命名的bean。
好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!
項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation
如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※帶您來了解什麼是 USB CONNECTOR ?
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!
※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※教你寫出一流的銷售文案?
看到這個標題,你肯定以為我又要講這道面試題了
// 這行代碼創建了幾個對象?
String s3 = new String("1");
是的,沒錯,我確實要從這裏開始
這道題就算你沒做過也肯定看到,總所周知,它創建了兩個對象,一個位於堆上,一個位於常量池中。
這個答案粗看起來是沒有任何問題的,但是仔細思考確經不起推敲。
如果你覺得我說的不對的話,那麼可以思考下面這兩個問題
你說它創建了兩個對象,那麼這兩個對象分別是怎樣創建的呢?我們回顧下Java創建對象的方式,一共就這麼幾種
newInstance方法,以及Constructor類的newInstance方法)你說它創建了兩個對象,那你告訴我除了new出來那個對象外,另外一個對象怎麼創建出來的?
堆跟常量池到底什麼關係?不是說在JDK1.7之後(含1.7版本)常量池已經移到了堆中了嗎?如果說常量池本身就位於堆中的話,那麼這種一個對象在堆中,一個對象在常量池的說法還準確嗎?
如果你也產生過這些疑問的話,那麼請耐心看完這篇文章!要解釋上面的問題首先我們得對常量池有個準確的認知。
通常來說,我們提到的常量池分為三種
對於這三種常量池,我們需要搞懂下面幾個問題?
接下來,我們帶着這些問題往下看
顧名思義,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,標誌方法執行完成,將方法對應棧幀從棧中彈出
下面我用畫圖的方式來畫出整個流程,主要分為四步
解析ldc指令的符號引用(#2)
將#2對應的常量的引用壓入到操作數棧頂
將操作數棧的元素彈出並存儲到局部變量表中
執行return指令,方法執行結束,彈出棧區該方法對應的棧幀
第一步:
在解析#2這個符號引用時,會先到字符串常量池中查找是否存在對應字符串實例的引用,如果有的話,那麼直接返回這個字符串實例的引用,如果沒有的話,會創建一個字符串實例,那麼將其添加到字符串常量池中(實際上是將其引用放入到一個哈希表中),之後再返回這個字符串實例對象的引用。
到這裏也能回答我們之前提出的那個問題了,一個對象是new出來的,另外一個是在解析常量池的時候JVM自動創建的
第二步:
將第一步得到的引用壓入到操作數棧,此時這個字符串實例同時被操作數棧以及字符串常量池引用。
第三步:
操作數棧中的引用彈出,並賦值給局部變量表中的1號位置元素,到這一步其實執行完了String name = "dmz"這行代碼。此時局部變量表中儲存着一個指向堆中字符串實例的引用,並且這個字符串實例同時也被字符串常量池引用。
第四步:
這一步我就不畫圖了,就是方法執行完成,棧幀彈出,非常簡單。
在上文中,我多次提到了字符串常量池,它到底是個什麼東西呢?我們還是分為兩部分討論
字符串常量池比較特殊,在JDK1.7之前,其存在於永久代中,到JDK1.7及之後,已經中永久代移到了堆中。當然,如果你非要說永久代也是堆的一部分那我也沒辦法。
另外還要說明一點,經常有同學會將方法區,元空間,永久代(permgen space)的概念混淆。請注意
方法區是JVM在內存分配時需要遵守的規範,是一個理論,具體的實現可以因人而異永久代是hotspot 的jdk1.8以前對方法區的實現,使用jdk1.7的老司機肯定以前經常遇到過java.lang.OutOfMemoryError: PremGen space異常。這裏的PermGen space其實指的就是方法區。不過方法區和PermGen space又有着本質的區別。前者是JVM的規範,而後者則是JVM規範的一種實現,並且只有HotSpot才有PermGen space。元空間是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大博客
從上面我們可以知道
為了更好理解上面的內容,我們需要去分析String中的一個方法—–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() == 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™ 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);
}
打印結果是
false falsefalse 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:複製棧頂數值並將複製值壓入棧頂,這是因為invokespecial跟astore_1各需要消耗一個引用
ldc:解析常量池符號引用,將實際的直接引用壓入操作數棧頂
invokespecial:彈出此時棧頂的常量引用及對象引用,執行invokespecial指令,調用構造函數
astore_1:將此時操作數棧頂的元素彈出,賦值給局部變量表中1號元素(0號元素存的是main函數的參數)
我們可以將上面整個過程分為兩個階段
在解析常量的過程中,因為該字符串常量是第一次解析,所以會先在永久代中創建一個字符串實例對象,並將其引用添加到字符串常量池中。此時內存狀態如下:
當真正通過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方法來轉換成一個字符串,StringBuilder的toString方法其實就是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維修中心
| 好看請贊,養成習慣
你有一個思想,我有一個思想,我們交換后,一個人就有兩個思想
If you can NOT explain it simply, you do NOT understand it well enough
現陸續將Demo代碼和技術文章整理在一起 Github實踐精選 ,方便大家閱讀查看,本文同樣收錄在此,覺得不錯,還請Star
看到本期內容這麼少,是不是心動了呢?
上一篇萬字長文 Java AQS隊列同步器以及ReentrantLock的應用 為我們讀 JUC 源碼以及其設計思想做了足夠多的鋪墊,接下來的內容我將重點說明差異化,如果有些童鞋不是能很好的理解文中的一些內容,強烈建議回看上一篇文章,搞懂基礎內容,接下來的閱讀真會輕鬆加愉快
AQS 中我們介紹了獨佔式獲取同步狀態的多種情形:
AQS 提供的模版方法裏面還差共享式獲取同步狀態沒有介紹,所以我們今天來揭開這個看似神秘的面紗
獨佔式是你中沒我,我中沒你的的一種互斥形式,共享式顯然就不是這樣了,所以他們的唯一區別就是:
同一時刻能否有多個線程同時獲取到同步狀態
簡單來說,就是這樣滴:
我們知道同步狀態 state 是維護在 AQS 中的,拋開可重入鎖的概念,我在上篇文章中也提到了,獨佔式和共享式控制同步狀態 state 的區別僅僅是這樣:
所以說想了解 AQS 的 xxxShared 的模版方法,只需要知道它是怎麼控制 state 的就好了
為了幫助大家更好的回憶內容,我將上一篇文章的兩個關鍵內容粘貼在此處,幫助大家快速回憶,關於共享式,大家只需要關注【騷紫色】就可以了
故事就從這裏說起吧 (你會發現和獨佔式驚人的相似),關鍵代碼都加了註釋
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 中文多翻譯為 【信號量】,我還特意查了一下劍橋辭典的英文解釋:
其實就是信號標誌(two flags),比如紅綠燈,每個交通燈產生兩種不同行為
在 Semaphore 裏面,什麼時候是紅燈,什麼時候是綠燈,其實就是靠 tryAcquireShared(arg) 的結果來表示的
所以我們走近 Semaphore ,來看看它到底是怎麼應用 AQS 的,又是怎樣重寫 tryAcquireShared(arg) 方法的
先看一下類結構
看到這裏你是否有點跌眼鏡,和 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 日常應用還是非常廣泛的,我們接下來就陸續聊聊它們吧
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整
※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!
※教你寫出一流的銷售文案?
※超省錢租車方案
目錄
首先,我們要知道併發要解決的是什麼問題?併發要解決的是單進程情況下硬件資源無法充分利用的問題。而造成這一問題的主要原因是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();這一行代碼上。這行代碼,我們理解的執行順序應該是這樣:
但在實際編譯的過程中,編譯器有可能會幫我們進行優化,優化完它的順序可能變成如下:
按照優化完的順序,當併發訪問的時候,可能會出現這樣的情況
最終線程B拿到的instance 是一個沒有實例化對象的空內存地址,所以導致instance使用的過程中造成程序錯誤。解決辦法很簡單,可以給sInstance對象加上一個關鍵字,volatile,這樣編譯器就不會亂優化,有關volatile的具體內容後續再細說。
通過上面的介紹,其實可以歸納無論是CPU緩存,線程切換還是編譯器優化亂序,出現問題的核心都是因為多個線程要併發讀寫某個變量或併發執行某段代碼。那麼我們可以控制,一次只讓一個線程執行變量讀寫就可以了,這就是互斥。
而在某些時候,互斥還不夠,還需要一定的條件。比如一個生產者一個消費者併發,生產者向隊列存東西,消費者向隊列拿東西。那麼生產者寫的時候要保證存的時候隊列不是滿的,消費者要保證拿的時候隊列非空。這種線程與線程間需要通信協作的情況,稱為同步,同步可以說是更複雜的互斥。
既然知道了併發編程的根源以及同步和互斥,那我們來看看有哪些解決的思路。其實一共也就三種:
下面我們分別說說這三種方案的優缺點
我們先來說說避免共享,其實避免共享說是線程本地存儲技術,在java中指的一般就是Threadlocal。ThreadLocal會為每個線程提供一個本地副本,每個線程都只會修改自己的ThreadLocal變量。這樣一來就不會出現共享變量,也就不會出現衝突了。
其實現原理是在ThreadLocal內部維護一個ThreadLocalMap,每次有線程要獲取對應變量的時候,先獲取當前線程,然後根據不同線程取不同的值,典型的以空間換時間。
所以ThreadLocal還是比較適用於需要共享資源,且資源佔用空間不大的情況。比如一些連接的session啊等等。但是這種模式應用場景也較為有限,比如需要同步情況就難以勝任。
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地圖已可更新顯示潭子電動車充電站設置地點!!
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※別再煩惱如何寫文案,掌握八大原則!
※網頁設計最專業,超強功能平台可客製化
上一篇【.Net Core微服務入門全紀錄(三)——Consul-服務註冊與發現(下)】已經使用Consul完成了服務的註冊與發現,實際中光有服務註冊與發現往往是不夠的,我們需要一個統一的入口來連接客戶端與服務。
官網:https://ocelot.readthedocs.io/
Ocelot正是為.Net微服務體系提供一個統一的入口點,稱為:Gateway(網關)。
首先創建一個空的asp.net core web項目。
注意ocelot.json是我們添加的Ocelot的配置文件,記得設置生成時複製到輸出目錄。ocelot.json的文件名不是固定的,可以自己定義。
NuGet安裝一下Ocelot:
只需簡單的修改幾處默認代碼:
Program.cs:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddJsonFile("ocelot.json");
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
Startup.cs:
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
//添加ocelot服務
services.AddOcelot();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//設置Ocelot中間件
app.UseOcelot().Wait();
}
}
ocelot.json:
{
"Routes": [
{
"DownstreamPathTemplate": "/products",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 9050
},
{
"Host": "localhost",
"Port": 9051
},
{
"Host": "localhost",
"Port": 9052
}
],
"UpstreamPathTemplate": "/products",
"UpstreamHttpMethod": [
"Get"
],
"LoadBalancerOptions": {
"Type": "RoundRobin" //負載均衡,輪詢機制 LeastConnection/RoundRobin/NoLoadBalancer/CookieStickySessions
}
},
{
"DownstreamPathTemplate": "/orders",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 9060
},
{
"Host": "localhost",
"Port": 9061
},
{
"Host": "localhost",
"Port": 9062
}
],
"UpstreamPathTemplate": "/orders",
"UpstreamHttpMethod": [
"Get"
],
"LoadBalancerOptions": {
"Type": "RoundRobin" //負載均衡,輪詢機制 LeastConnection/RoundRobin/NoLoadBalancer/CookieStickySessions
}
}
],
"GlobalConfiguration": {
"BaseUrl": "http://localhost:9070"
}
}
我們先暫時忽略Consul,將服務實例的地址都寫在配置文件中。要知道Consul、Ocelot等組件都是可以獨立存在的。
配置文件中的Routes節點用來配置路由,Downstream代表下游,也就是服務實例,Upstream代表上游,也就是客戶端。我們的路徑比較簡單,只有/products、/orders,路徑中如果有不固定參數則使用{}匹配。我們這個配置的意思呢就是客戶端訪問網關的/orders、/products,網關會轉發給服務實例的/orders、/products,注意這個上游的路徑不一定要和下游一致,比如上游路徑可以配置成/api/orders,/xxx都可以。
LoadBalancerOptions節點用來配置負載均衡,Ocelot內置了 LeastConnection、RoundRobin、NoLoadBalancer、CookieStickySessions 4種負載均衡策略。
BaseUrl節點就是配置我們ocelot網關將要運行的地址。
目前不考慮網關集群,就不放在docker里了。直接控制台執行:`dotnet Ocelot.APIGateway.dll –urls=”http://*:9070″
用瀏覽器測試一下:
測試正常,我們通過網關可以正常的訪問到服務實例。
因為改動太多就直接新建一個GatewayServiceHelper來做。
GatewayServiceHelper:
/// <summary>
/// 通過gateway調用服務
/// </summary>
public class GatewayServiceHelper : IServiceHelper
{
public async Task<string> GetOrder()
{
var Client = new RestClient("http://localhost:9070");
var request = new RestRequest("/orders", Method.GET);
var response = await Client.ExecuteAsync(request);
return response.Content;
}
public async Task<string> GetProduct()
{
var Client = new RestClient("http://localhost:9070");
var request = new RestRequest("/products", Method.GET);
var response = await Client.ExecuteAsync(request);
return response.Content;
}
public void GetServices()
{
throw new NotImplementedException();
}
}
然後在Startup中修改一下注入的類型,別的就不用改了,這就是依賴注入的好處之一。。。
Startup.ConfigureServices():
//注入IServiceHelper
//services.AddSingleton<IServiceHelper, ServiceHelper>();
//注入IServiceHelper
services.AddSingleton<IServiceHelper, GatewayServiceHelper>();
Startup.Configure():
//程序啟動時 獲取服務列表
//serviceHelper.GetServices();
運行客戶端測試:
好了,現在客戶端對服務的調用都通過網關進行中轉,客戶端再也不用去關心那一堆服務實例的地址,只需要知道網關地址就可以了。另外,服務端也避免了服務地址直接暴露給客戶端。這樣做對客戶端,服務都非常友好。
至於我們的api網關呢,又要說到服務發現的問題了。目前我們的服務地址是寫在ocelot.json配置文件里的,當然這種做法在服務實例不經常變化的情況下是沒有問題的,一旦服務變化,需要人為的修改配置文件,這又顯得不太合理了。
當然,強大的Ocelot為我們提供了服務發現的方案。
代碼放在:https://github.com/xiajingren/NetCoreMicroserviceDemo
未完待續…
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※帶您來了解什麼是 USB CONNECTOR ?
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!
※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※教你寫出一流的銷售文案?
閱讀本文大概需要 11 分鐘。
原文:https://bit.ly/2UMiDLb
作者:Jon P Smith
翻譯:王亮
聲明:我翻譯技術文章不是逐句翻譯的,而是根據我自己的理解來表述的。其中可能會去除一些本人實在不知道如何組織但又不影響理解的句子。
本文將為你詳細描繪 EF Core 從數據庫中讀取數據的“幕後”視圖。我將揭開兩種數據庫讀取方式的面紗:一個是普通的查詢,另一個是使用 AsNoTracking 方法的非跟蹤查詢。我還將通過一個實驗來演示我是如何解決我的一個客戶遇到的性能問題。
我假設你對 EF Core 已經有了一定的認識,但在深入學習之前,我們先來了解一下如何使用 EF Core,以確保我們已經掌握了一些基本知識。這是一個“深入研究”的課題,所以我準備大量的技術細節,希望我的描述方式你能理解。
本文是“深入理解 EF Core”系列中的第一篇。以下是本系列文章列表:
提示:如果你已經對 EF Core 有一定的認識,那麼你可以跳過這一節,這部分只是一個如何讀取數據庫的例子。
為了能讓你更好地理解,我先描述一個數據庫結構,然後再給出一個簡單的數據庫讀取示例。下面是一些基本表的結構和它們之間的關係。
這些表被映射到具有類似名稱的類,例如 Book、BookAuthor、Author,這些類的屬性名稱與表的字段名稱相同。由於篇幅有限,我不打算展開來講這些類,但您可以在我的 GitHub 倉庫[1]中查看這些類。
EF Core 讀取數據庫需要下面五部分:
下面的單元測試代碼來自我的 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 部分,結果是這樣的:
SqliteInMemory.CreateOptions 方法,它使用我的一個 NuGet 包 EfCore.TestSupport 創建了一個內存數據庫(內存中的數據庫對於單元測試非常有用,因為你可以為這個測試建立一個新的空數據庫)。.Books 表示您希望訪問 Books 表。所有這一切查詢出來是一個結果集,其中有普通屬性,像 Books 的 Title 屬性;有關聯實體類的導航屬性,像 Books 的 AuthorsLink 屬性。
這個示例稱為查詢或讀取,也是四種數據庫訪問類型之一,即 CRUD(新增、讀取、更新和刪除)。我將在下一篇文章中介紹新增和更新。
當你查詢數據庫時,EF Core 會將數據庫返回的數據轉換為實體類並填充導航屬性的值。在本節中,我們將研究兩種類型的查詢步驟——普通查詢(即沒有 AsNoTracking 方法,也稱為讀寫查詢)和添加了 AsNoTracking 方法的非跟蹤查詢(稱為只讀查詢)。
我們先來看一下最初 LINQ 語句是如何轉換成數據庫相應的查詢命令然後返回數據的。對於我們將要看到的兩種類型的查詢來說,這是很常見的操作。關於查詢的第一部分,請參見下圖。
有一些非常複雜的代碼將你的 LINQ 轉換為數據庫查詢命令,但這些內部細節我們不必關心。如果你的 LINQ 不能被翻譯,你會從 EF Core 得到一個異常消息,其中包含類似“不能被翻譯”的描述詞語。此外,當數據返回時,像 Value Converters[4] 這樣的特性可能會調整數據。
本節展示了查詢的第一部分,其中 LINQ 被轉換為數據庫命令並返回所有正確的值。現在我們來看查詢的第二部分,在這裏 EF Core 獲取返回值並將它們轉換為實體類的實例,並填充導航屬性。我們將分別看看兩種類型的查詢。
普通查詢讀取數據的方式可以修改數據並更新到數據庫,這就是我將其稱為讀寫查詢的原因。它不會自動更新數據(請參閱下一篇文章,了解如何寫入數據庫)。如果你要更新數據,你的查詢必須是讀寫查詢。
我在介紹中給出的示例執行的是一個普通讀寫查詢,讀取帶有 AuthorsLink 實例的示例。下面是該示例的查詢部分的代碼:
var books = context.Books
.Include(r => r.AuthorsLink)
.ThenInclude(r => r.Author)
.ToList();
然後 EF Core 通過三個步驟將這些值轉換並填充含有導航屬性的實體類。下圖显示了這三個步驟以及生成的實體類及其導航屬性的實體類。
讓我們來分析一下這三個步驟:
非跟蹤查詢,即使用 AsNoTracking 方法的查詢,是一個只讀查詢。這意味着,當 SaveChanges 方法被調用時,你讀取的任何內容都不會被寫入數據庫。非跟蹤查詢的查詢效率更高,在下一節中,我將介紹非跟蹤查詢以及與普通查詢的其他區別。
在前文的示例之後,我修改了查詢代碼,添加了下面的 AsNoTracking 方法(請看第 2 行):
var books = context.Books
.AsNoTracking()
.Include(r => r.AuthorsLink)
.ThenInclude(r => r.Author)
.ToList();
這裏的 LINQ 查詢只有上面的普通查詢的前兩個步驟(沒有第三個步驟)。下圖显示了 AsNoTracking 查詢的步驟。
步驟如下:
現在讓我們比較這兩種查詢比較明顯的區別。
非跟蹤查詢查詢的性能更好。使用非跟蹤查詢查詢的主要原因是性能。非跟蹤查詢查詢表現為:
非跟蹤查詢修補關聯關係時只鏈接查詢中的實體。在普通查詢中,我已經說過修補關聯關係時連接的是查詢中的實體和當前跟蹤的實體,但是非跟蹤查詢只修補查詢中的實體關係。
非跟蹤查詢並不總是代表數據庫關係。這兩種類型查詢之間的關係修補的另一個區別是,非跟蹤查詢關係修補更快,它不需要標識的解析。這可以為數據庫中的同一行生成多個實例——見上圖右下角藍色的 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維修中心
自從這個純內存項目進了大客戶之後,搞得我現在對內存和CPU特別敏感,跑一點數據內存幾個G的上下,特別沒有安全感,總想用windbg抓幾個dump看看到底是哪一塊導致的,是我的代碼還是同事的代碼? 很多看過我博客的老朋友總是留言讓我出一套windbg的系列或者視頻,我也不會呀,沒辦法,人在江湖飄,遲早得挨上幾刀,逼着也得會幾個花架子,廢話不多說,這一篇就來看看 HashSet 是如何擴容的。
了解如何擴容,最好的辦法就是翻看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);
}
}
當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,如果你非要看演示,我就寫一小段代碼證明一下吧,如下圖:
為了方便演示,我把 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中實現的那樣漸進式擴容,時間開銷也是大家值得注意的。
這個有什麼風險呢?可以看一下: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_slots 和 int[] 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,但願我沒有算錯。。。
在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_slots 和 m_buckets,每個Slot還有三個元素: struct Slot { int hashCode;internal int next;internal T value; },所以了解完原理之後謹慎着用吧。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整
※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!
※教你寫出一流的銷售文案?
※超省錢租車方案
上一篇完成了標籤模塊和友情鏈接模塊的所有功能,本篇來繼續完成博客最後的模塊,文章的管理。
先將分頁查詢的列表給整出來,這塊和首頁的分頁列表是類似的,就是多了個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地圖已可更新顯示潭子電動車充電站設置地點!!
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※別再煩惱如何寫文案,掌握八大原則!
※網頁設計最專業,超強功能平台可客製化