【Spring註解驅動開發】在@Import註解中使用ImportBeanDefinitionRegistrar向容器中註冊bean

寫在前面

在前面的文章中,我們學習了如何使用@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是個什麼鬼,點擊進入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實例

既然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類設計

從一道面試題開始

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

//  這行代碼創建了幾個對象?
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維修中心

小師妹學JVM之:深入理解JIT和編譯優化-你看不懂系列

目錄

  • 簡介
  • JIT編譯器
  • Tiered Compilation分層編譯
  • OSR(On-Stack Replacement)
  • Deoptimization
  • 常見的編譯優化舉例
    • Inlining內聯
    • Branch Prediction分支預測
    • Loop unswitching
    • Loop unrolling展開
    • Escape analysis逃逸分析
  • 總結

簡介

小師妹已經學完JVM的簡單部分了,接下來要進入的是JVM中比較晦澀難懂的概念,這些概念是那麼的枯燥乏味,甚至還有點惹人討厭,但是要想深入理解JVM,這些概念是必須的,我將會盡量嘗試用簡單的例子來解釋它們,但一定會有人看不懂,沒關係,這個系列本不是給所有人看的。

更多精彩內容且看:

  • 區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新
  • Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
  • Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
  • java程序員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程

JIT編譯器

小師妹:F師兄,我的基礎已經打牢了嗎?可以進入這麼複雜的內容環節了嗎?

小師妹不試試怎麼知道不行呢?了解點深入內容可以幫助你更好的理解之前的知識。現在我們開始吧。

上次我們在講java程序的處理流程的時候,還記得那通用的幾步吧。

小師妹:當然記得了,編寫源代碼,javac編譯成字節碼,加載到JVM中執行。

對,其實在JVM的執行引擎中,有三個部分:解釋器,JIT編譯器和垃圾回收器。

解釋器會將前面編譯生成的字節碼翻譯成機器語言,因為每次都要翻譯,相當於比直接編譯成機器碼要多了一步,所以java執行起來會比較慢。

為了解決這個問題,JVM引入了JIT(Just-in-Time)編譯器,將熱點代碼編譯成為機器碼。

Tiered Compilation分層編譯

小師妹你知道嗎?在JDK8之前,HotSpot VM又分為三種。分別是 client VM, server VM, 和 minimal VM,分別用在客戶端,服務器,和嵌入式系統。

但是隨着硬件技術的發展,這些硬件上面的限制都不是什麼大事了。所以從JDK8之後,已經不再區分這些VM了,現在統一使用VM的實現來替代他們。

小師妹,你覺得Client VM和Server VM的本質區別在哪一部分呢?

小師妹,編譯成字節碼應該都是使用javac,都是同樣的命令,字節碼上面肯定是一樣的。難點是在執行引擎上面的不同?

說的對,因為Client VM和Server VM的出現,所以在JIT中出現了兩種不同的編譯器,C1 for Client VM, C2 for Server VM。

因為javac的編譯只能做少量的優化,其實大量的動態優化是在JIT中做的。C2相對於C1,其優化的程度更深,更加激進。

為了更好的提升編譯效率,JVM在JDK7中引入了分層編譯Tiered compilation的概念。

對於JIT本身來說,動態編譯是需要佔用用戶內存空間的,有可能會造成較高的延遲。

對於Server服務器來說,因為代碼要服務很多個client,所以磨刀不誤砍柴工,短暫的延遲帶來永久的收益,聽起來是可以接受的。

Server端的JIT編譯也不是立馬進行的,它可能需要收集到足夠多的信息之後,才進行編譯。

而對於Client來說,延遲帶來的性能影響就需要進行考慮了。和Server相比,它只進行了簡單的機器碼的編譯。

為了滿足不同層次的編譯需求,於是引入了分層編譯的概念。

大概來說分層編譯可以分為三層:

  1. 第一層就是禁用C1和C2編譯器,這個時候沒有JIT進行。
  2. 第二層就是只開啟C1編譯器,因為C1編譯器只會進行一些簡單的JIT優化,所以這個可以應對常規情況。
  3. 第三層就是同時開啟C1和C2編譯器。

在JDK7中,你可以使用下面的命令來開啟分層編譯:

-XX:+TieredCompilation

而在JDK8之後,恭喜你,分層編譯已經是默認的選項了,不用再手動開啟。

OSR(On-Stack Replacement)

小師妹:F師兄,你剛剛講到Server的JIT不是立馬就進行編譯的,它會等待一定的時間來搜集所需的信息,那麼代碼不是要從字節碼轉換成機器碼?

對的,這個過程就叫做OSR(On-Stack Replacement)。為什麼叫OSR呢?我們知道JVM的底層實現是一個棧的虛擬機,所以這個替換實際上是一系列的Stack操作。

上圖所示,m1方法從最初的解釋frame變成了後面的compiled frame。

Deoptimization

這個世界是平衡的,有陰就有陽,有優化就有反優化。

小師妹:F師兄,為什麼優化了之後還要反優化呢?這樣對性能不是下降了嗎?

通常來說是這樣的,但是有些特殊的情況下面,確實是需要進行反優化的。

下面是比較常見的情況:

  1. 需要調試的情況

如果代碼正在進行單個步驟的調試,那麼之前被編譯成為機器碼的代碼需要反優化回來,從而能夠調試。

  1. 代碼廢棄的情況

當一個被編譯過的方法,因為種種原因不可用了,這個時候就需要將其反優化。

  1. 優化之前編譯的代碼

有可能出現之前優化過的代碼可能不夠完美,需要重新優化的情況,這種情況下同樣也需要進行反優化。

常見的編譯優化舉例

除了JIT編譯成機器碼之外,JIT還有一下常見的代碼優化方式,我們來一一介紹。

Inlining內聯

舉個例子:

int a = 1;
int b = 2;
int result = add(a, b);
...
public int add(int x, int y) { return x + y; }
int result = a + b; //內聯替換

上面的add方法可以簡單的被替換成為內聯表達式。

Branch Prediction分支預測

通常來說對於條件分支,因為需要有一個if的判斷條件,JVM需要在執行完畢判斷條件,得到返回結果之後,才能夠繼續準備後面的執行代碼,如果有了分支預測,那麼JVM可以提前準備相應的執行代碼,如果分支檢查成功就直接執行,省去了代碼準備的步驟。

比如下面的代碼:

// make an array of random doubles 0..1
double[] bigArray = makeBigArray();
for (int i = 0; i < bigArray.length; i++)
{
 double cur = bigArray[i];
 if (cur > 0.5) { doThis();} else { doThat();}
}

Loop unswitching

如果我們在循環語句裏面添加了if語句,為了提升併發的執行效率,可以將if語句從循環中提取出來:

  int i, w, x[1000], y[1000];
  for (i = 0; i < 1000; i++) {
    x[i] += y[i];
    if (w)
      y[i] = 0;
  }

可以改為下面的方式:

  int i, w, x[1000], y[1000];
  if (w) {
    for (i = 0; i < 1000; i++) {
      x[i] += y[i];
      y[i] = 0;
    }
  } else {
    for (i = 0; i < 1000; i++) {
      x[i] += y[i];
    }
  }

Loop unrolling展開

在循環語句中,因為要不斷的進行跳轉,所以限制了執行的速度,我們可以對循環語句中的邏輯進行適當的展開:

 int x;
 for (x = 0; x < 100; x++)
 {
     delete(x);
 }

轉變為:

 int x; 
 for (x = 0; x < 100; x += 5 )
 {
     delete(x);
     delete(x + 1);
     delete(x + 2);
     delete(x + 3);
     delete(x + 4);
 }

雖然循環體變長了,但是跳轉次數變少了,其實是可以提升執行速度的。

Escape analysis逃逸分析

什麼叫逃逸分析呢?簡單點講就是分析這個線程中的對象,有沒有可能會被其他對象或者線程所訪問,如果有的話,那麼這個對象應該在Heap中分配,這樣才能讓對其他的對象可見。

如果沒有其他的對象訪問,那麼完全可以在stack中分配這個對象,棧上分配肯定比堆上分配要快,因為不用考慮同步的問題。

我們舉個例子:

  public static void main(String[] args) {
    example();
  }
  public static void example() {
    Foo foo = new Foo(); //alloc
    Bar bar = new Bar(); //alloc
    bar.setFoo(foo);
  }
}

class Foo {}

class Bar {
  private Foo foo;
  public void setFoo(Foo foo) {
    this.foo = foo;
  }
}

上面的例子中,setFoo引用了foo對象,如果bar對象是在heap中分配的話,那麼引用的foo對象就逃逸了,也需要被分配在heap空間中。

但是因為bar和foo對象都只是在example方法中調用的,所以,JVM可以分析出來沒有其他的對象需要引用他們,那麼直接在example的方法棧中分配這兩個對象即可。

逃逸分析還有一個作用就是lock coarsening。

為了在多線程環境中保證資源的有序訪問,JVM引入了鎖的概念,雖然鎖可以保證多線程的有序執行,但是如果實在單線程環境中呢?是不是還需要一直使用鎖呢?

比如下面的例子:

public String getNames() {
     Vector<String> v = new Vector<>();
     v.add("Me");
     v.add("You");
     v.add("Her");
     return v.toString();
}

Vector是一個同步對象,如果是在單線程環境中,這個同步鎖是沒有意義的,因此在JDK6之後,鎖只在被需要的時候才會使用。

這樣就能提升程序的執行效率。

總結

本文介紹了JIT的原理和一些基本的優化方式。後面我們會繼續探索JIT和JVM的秘密,敬請期待。

本文作者:flydean程序那些事

本文鏈接:http://www.flydean.com/jvm-jit-in-detail/

本文來源:flydean的博客

歡迎關注我的公眾號:程序那些事,更多精彩等着您!

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

精美圖文講解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地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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

特斯拉將調整全球超級充電站充電價格,車主錯愕

根據國外專門報導電動車產業消息的《Electrek》網站報導,電動車大廠特斯拉(Tesla)正式終止任何形式的免費充電計畫之後,準備將全球超級充電站(Supercharger)充電價格平均提高 33%,這舉動令車主錯愕。

自 2018 年 11 月以來,特斯拉所有新款電動車都必須遵守新超級充電站充電付費計畫,雖然沒有擴及 2018 年 11 月前購買特斯拉電動車的車主,特斯拉仍舊對這些車主提供有限度的免費充電服務。不過,這項優惠措施到 2019 年 1 月底為止,也就是之後再也不會有任何特斯拉車主有免費充電服務;新付費方式將以每度(小時千瓦;1kWh),或是部分地區每分鐘來計算充電費用。

觀察特斯拉的新充電費率,將以不同地區、甚至每個充電站的使用需求計價。特斯拉還希望根據當地電價,訂定更合理、更全面的價格。換句話說,這會造成大多數地區的超級充電樁價格大幅上漲。

在 2018 年,特斯拉已提高美國超級充電站的充電價格。調漲後多數地區的充電價格漲幅為 20%~40%,部分地區漲幅甚至高達 100%。以紐約市為例,過去是每度 0.24 美元,現在則是 0.32 美元,上漲 33%。加州地區,過去每度為 0.26 美元,調整之後是 0.32 到 0.36 美元不等。這次特斯拉全球充電價格調漲,美國市場已是第 2 次漲價。

歐洲方面,雖然大多數市場充電價格仍維持每度 0.28~0.32 歐元,以特斯拉在歐洲最重要的市場和超級充電站最密集的挪威來說,充電價格預計從每小時千瓦 1.4 挪威克朗,上升到 1.86 挪威克朗,幾乎漲了 33%。相信未來其他地區也會是類似漲幅。

特斯拉一直聲稱超級充電站「永遠不會成為利潤中心」。漲價計畫決定後,面對記者的詢問,特斯拉還是重申這點,並表示正在調整超級充電站的充電價格,希望更能反映當地電力成本,以及場地使用情況的差異。隨著特斯拉電動車增多,未來也繼續每週開設新超級充電站,讓更多消費者可長途行駛,並享受到比汽油價格低的充電價格,達到零排碳量的目標。未來,還希望利用超級充電站獲得的收入,建立更多充電站。

目前特斯拉全世界共有 1,422 個超級充電站,共有 12,011 個充電樁,而特斯拉的目標,是在 2019 年將這個數字倍增。

(合作媒體:。首圖來源: CC BY 2.0)

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

【其他文章推薦】

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

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

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

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

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

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

電動機車今年累計掛牌數已突破 4 萬輛,年成長達 2 倍

在政府鼓勵使用電動機車、車廠推出新車款及民眾環保意識抬頭等多方因素推波助瀾之下,近幾年電動機車掛牌數量逐年倍增;據經濟部工業局統計,105 年掛牌數量約 2 萬輛,106 年掛牌數量約 4.4 萬輛,107 年截至 8 月份已突破 4 萬輛,較去年同期大幅成長 2 倍。

工業局也預期,由於光陽、中華及三陽等下半年均有新車發表計畫,預期今年全年整體掛牌數仍會大幅成長。而工業局樂見民眾對電動機車產品的響應支持,也同步調整補助預算規則,經費將會優先補助民眾購買電動機車,並輔以補助設置能源補充設施。

隨著暑假到來,各車廠下半年將陸續推出新車款,也針對年輕學子與機車首購族祭出購車優惠。例如光陽推出兩款 New Many 110 EV,配合電池月租 99 元預購方案積極搶市,並將廣布 Ionex 充換電站;而睿能則推出 10 款 Gogoro 2 系列,搭配平均日付約 66 元銅板購車方案,並於 8 月 10 日前進宜蘭設置換電站以服務當地使用者;另中華汽車也於 8 月 3 日在宜蘭羅東開幕全新 emoving 專賣店;配合中央與地方政府購車補助,電動機車已成為民眾購買機車的優先考慮選項。

工業局統計,今年補助數量與去年同期相比,成長近 2.5 倍,其中重型等級佔比約 86.9%、輕型等級約 8.2%,小型輕型等級則約 4.9%。另據統計分析,目前電動機車的消費族群,男女比例各半,36-40 歲年齡層族群為購買主力,其次為 31-35 歲族群。而 40 歲以下的電動機車消費者,則呈現男性多於女性的現象。數量多集中於六都,銷售量依序為桃園市、新北市、高雄市、台中市、台南市與台北市等,數量總計超過全台之 86%。

此外,工業局也指出,未來更將積極與車廠透過建置能源補充設施及行銷方案,持續推動其他縣市消費市場,進一步落實電動機車推動政策。

(本文內容由 授權使用。首圖來源:)

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

【其他文章推薦】

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

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

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

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

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

新型液流電池可發電與供氫,電動車與氫燃料車都適用

電池電動車與氫燃料電池車都有望成為新一代交通主力,但這兩種車款各採用不同的「充電方式」,一種是電力、一種是補充氫氣,各國得針對兩種不同的車系分別打造管線與「加油站」,而近日英國格拉斯哥大學打造新型液流電池,不僅可讓汽車在幾秒內完成燃料補充,還可以釋放電力與氫氣、完美解決兩種電動車系統不相容的狀況。

液流電池(flow battery)由兩個電解質槽組成,充放電時電解質會被幫補到中間的發電室,而發電室也會以薄膜隔開兩種溶液、形成兩個電極,最後產生離子交換來發電,而由於兩種電解質是分開存放,不會有電解質相互滲漏與自身放電等安全性問題,是一種良好的儲能生力軍。

只是液流電池體積龐大,即使該電池具有安全性與穩定性高等優點,仍不適合用於 3C 產品與電動車,目前大多研究團隊都是想把液流電池用在再生能源儲能系統。

而格拉斯哥大學這次想將突破以往液流電池無法用在汽車的印象,並成功透過奈米粒子溶液打造新型液流電池,其中團隊所用的電解質是一種奈米懸浮液(suspension),每個奈米粒子都可以當成一顆小型電池,儲存能量更是一般液流電池的 10 倍,還能以電力或氫氣的形式釋放。

且由於液流電池主要以液態電解質驅動,團隊更指出,新型液流電池可在幾秒內完成移除舊液體、補充新電解質,因此該系統的「充電」時間可跟一般汽油車一樣,大大縮短電動車的充電速度。

格拉斯哥大學 Regius Chair of Chemistry 教授 Leroy(Lee)Cronin 表示,若再生能源想要有效運作,還需要儲存容量、靈活性高的儲能系統來幫忙解決間歇性能源與電力尖峰等問題,而團隊提出的新型化學充電方式,除了可用在儲能系統,還可應用在電動車中。

該研究將有助於未來液流電池的應用,有望縮短液流電池與大規模商業化的距離。團隊也指出,新型液流電池的能量密度相當高,可提升將來電動車的續航里程與儲能系統的儲存容量,目前研究已發表在《》。

(首圖來源:。文/DaisyChuang)

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

工研院新創公司菲國參展,大推電動車充電自主技術

電動車輛取代燃油車已是不可逆的趨勢,工研院表示,旗下新創公司起而行綠能(eTreego)為搶進廣大東南亞市場,16 日於菲律賓馬尼拉舉行的「菲律賓國際汽機車零配件展」(Automechanic Philippines),發表以先進充電控制模組打造出的汽機車充電樁,符合歐美日等先進國家 4 種不同規格,溫度運作範圍從 -40℃~70℃,加上全方位充電解決方案,獲得到場參觀廠商與買家的高詢問度,成為本次展會亮點之一。

歐洲主要國家與台灣已設定 2040 年起停售燃油車為目標,大舉刺激全球電動車的銷售。工研院 IEK 統計,2017 年全球電動車市場銷售量約 266 萬輛,今年銷售量可望達到 317 萬輛,成長率達 19%。電動機車預估今年全球銷售約 54.9 萬輛,較去年約 33.3 萬輛規模,成長 64% 左右。而全球電動機車市場,亞洲地區佔比就高達 76.3%,以機車為主要代步工具的東南亞地區,發展潛力更是驚人。

為響應政府新南向政策,積極推動產業創新的新藍海,起而行綠能總經理簡金品表示,此次進軍菲律賓參展,除了拓展起而行綠能在國際市場的知名度,也是看好東南亞電動車市場潛力,希望能以公司創新研發能量,協助當地電動車發展。

起而行綠能為工研院新創公司,掌握電動車充電關鍵技術的充電模組,產品包括充電控制模組、充電機、充電站、電能管理,提供電動汽車與機車全方位充電方案。其產品優勢包括可對應美國汽車工程師學會 SAE、國際電工委員會 IEC、日本 CHAdeMO、中國 GB 等 4 項國際主要標準;運作溫度範圍彈性大,可在 -40℃~70℃ 之間正常使用,遠優於市面其他產品僅 0℃~60℃ 區間。

簡金品指出,起而行綠能團隊在工研院時期,曾參與制定台灣電動汽機車充電標準。2010 年台中市政府與裕日車的電動車合作案,其中所打造的台灣第一支汽車充電椿,就是來自起而行團隊。目前台灣的汽車充電椿則有一半以上是來自起而行。

起而行綠能表示,目前公司客戶包括台灣國內的裕隆、中華汽車等,海外市場包含大中華區等國際車廠與相關充電設備業者也都正在進行合作與供貨洽談。同時,起而行也與台灣多家機車廠合作發展符合國內電動機車共通充電產業標準之充電產品。除這些既有充電系統,起而行亦積極投入開發小型化、雙向充電、無線充電、快速充電與全功能的聯網系統,因應即將快速爆發的電動車商機,為市場提供全方面的充電解決方案。

據了解,菲律賓國際汽機車零配件展 2017 年有來自 11 國、280 廠商參展,參觀人數逾 7,000 人。參觀者與買家包括汽車品牌廠、零組件製造商、通路商、零售商與運輸服務商。

(本文內容由 授權使用。首圖來源:)

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

中國研擬汽車投資管理等新規,新能源智慧車料續受惠

經濟參考報報導,中國 2018 年前 7 個月新能源汽車年增 68.6%,保持持續增長態勢,並成為拉動整體汽車市場成長的重要力量。據中國國家發改委等部門瞭解,目前包括新的汽車投資管理規定等多項政策正在加緊推進,鼓勵技術、模式等創新,未來新能源智慧化汽車將迎來更多利多。

中國發改委表示,目前新的汽車產業投資管理規定已完成向社會公開徵求意見,正在加緊對徵求意見稿進行進一步完善,以期儘快發布。

據了解,中國此次準備發佈的新汽車投資管理辦法被稱為「最嚴燃油車產業政策」,未來新建獨立燃油車項目將被禁止,而現有汽車企業擴大燃油汽車生產能力也要同時滿足上兩個年度汽車產能利用率均高於全行業平均水準、上兩個年度新能源汽車產量佔比均高於全行業平均水準等四個條件。

在此同時,中國官方對於新能源汽車的准入門檻也大幅提高,要求新建的獨立純電動汽車企業專案要有純電動汽車持續開發能力,純電動乘用車建設規模不低於 10 萬輛,以及純電動商用車不低於 5,000 輛;此外,對新建新能源汽車企業的股東也提出了要求。

多位專家和業內人士表示,這將大大推動未來新能源汽車市場的發展,並提高了廠商、投資者,以及消費者對新能源汽車市場的信心。同時,對新能源汽車市場准入門檻的提高也將進一步提升新能源汽車市場的發展品質,推進企業加大在產品和技術方面的投入,實現優勝汰劣。

中國發改委產業協調司副司長蔡榮華表示,未來將進一步推動新能源智慧化汽車產業發展,發改委也將積極推動新能源智慧化汽車創新發展戰略儘快推出,鼓勵技術創新和模式創新,努力打造有利於新能源智慧汽車發展的生態系統和環境。

(本文內容由 授權使用。首圖來源:)

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

【其他文章推薦】

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

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

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

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

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

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