Java——內部類詳解

說起內部類,大家肯定感覺熟悉又陌生,因為一定在很多框架源碼中有看到別人使用過,但又感覺自己使用的比較少,今天我就帶你具體來看看內部類。

內部類基礎

所謂內部類就是在類的內部繼續定義其他內部結構類。

在 Java 中,廣泛意義上的內部類一般來說包括這四種:成員內部類、局部內部類、匿名內部類和靜態內部類。下面就先來了解一下這四種內部類的用法。

成員內部類

成員內部類是最普通的內部類,它的定義為位於另一個類的內部,具體使用如下:

class Circle {
    double radius = 0;

    public Circle(double radius) {
        this.radius = radius;
    }

    /**
     * 內部類
     */
    class Draw {
        public void drawSahpe() {
            System.out.println("drawshape");
        }
    }
}

這樣看起來,類 Draw 像是類 Circle 的一個成員, Circle 稱為外部類。成員內部類可以無條件訪問外部類的所有成員屬性和成員方法(包括 private 成員和靜態成員),例如:

class Circle {
    private double radius = 0;
    public static int count =1;
    public Circle(double radius) {
        this.radius = radius;
    }

    /**
     * 內部類
     */
    class Draw {
        public void drawSahpe() {
            // 外部類的private成員
            System.out.println(radius);
            // 外部類的靜態成員
            System.out.println(count);
        }
    }
}

不過要注意的是,當成員內部類擁有和外部類同名的成員變量或者方法時,會發生隱藏現象,即默認情況下訪問的是成員內部類的成員。如果要訪問外部類的同名成員,需要採取以下形式進行訪問:

外部類.this.成員變量
外部類.this.成員方法

雖然成員內部類可以無條件地訪問外部類的成員,而外部類想訪問成員內部類的成員卻不是這麼隨心所欲了。在外部類中如果要訪問成員內部類的成員,必須先創建一個成員內部類的對象,再通過指向這個對象的引用來訪問,其具體形式為:

class Circle {
    private double radius = 0;

    public Circle(double radius) {
        this.radius = radius;
        // 必須先創建成員內部類的對象,再進行訪問
        getDrawInstance().drawSahpe();
    }

    private Draw getDrawInstance() {
        return new Draw();
    }

    /**
     * 內部類
     */
    class Draw {
        public void drawSahpe() {
            // 外部類的private成員
            System.out.println(radius);
        }
    }
}

成員內部類是依附外部類而存在的,也就是說,如果要創建成員內部類的對象,前提是必須存在一個外部類的對象。創建成員內部類對象的一般方式如下:

public class Test {
    public static void main(String[] args)  {
        // 第一種方式
        Outter outter = new Outter();
        // 必須通過Outter對象來創建
        Outter.Inner inner = outter.new Inner();

        // 第二種方式
        Outter.Inner inner1 = outter.getInnerInstance();
    }
}

class Outter {
    private Inner inner = null;
    public Outter() {
    }

    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }

    class Inner {
        public Inner() {
        }
    }
}

內部類可以擁有 private 訪問權限、 protected 訪問權限、 public 訪問權限及包訪問權限。

比如上面的例子,如果成員內部類 Inner 用 private 修飾,則只能在外部類的內部訪問;如果用 public 修飾,則任何地方都能訪問;如果用 protected 修飾,則只能在同一個包下或者繼承外部類的情況下訪問;如果是默認訪問權限,則只能在同一個包下訪問。

這一點和外部類有一點不一樣,外部類只能被 public 和包訪問兩種權限修飾。

我個人是這麼理解的,由於成員內部類看起來像是外部類的一個成員,所以可以像類的成員一樣擁有多種權限修飾。

局部內部類

局部內部類是定義在一個方法或者一個作用域裏面的類,它和成員內部類的區別在於局部內部類的訪問僅限於方法內或者該作用域內。

class People{
    public People() {
    }
}

class Man{
    public Man(){
    }

    public People getWoman(){
        /**
         * 局部內部類
         */
        class Woman extends People{
            int age =0;
        }
        return new Woman();
    }
}

注意,局部內部類就像是方法裏面的一個局部變量一樣,是不能用 public 、 protected 、 private 以及 static 修飾的。

匿名內部類

匿名內部類應該是平時我們編寫代碼時用得最多的,比如創建一個線程的時候:

class Test {

    public static void main(String[] args) {
        Thread thread = new Thread(
                // 匿名內部類
                new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("Thread run");
                    }
                }
        );
    }
}

同樣的,匿名內部類也是不能有訪問修飾符和 static 修飾符的。

匿名內部類是唯一一種沒有構造器的類。正因為其沒有構造器,所以匿名內部類的使用範圍非常有限,大部分匿名內部類用於接口回調。

匿名內部類在編譯的時候由系統自動起名為Outter$1.class。一般來說,匿名內部類用於繼承其他類或是實現接口,並不需要增加額外的方法,只是對繼承方法的實現或是重寫。

靜態內部類

靜態內部類也是定義在另一個類裏面的類,只不過在類的前面多了一個關鍵字 static 。

靜態內部類是不需要依賴於外部類的,這點和類的靜態成員屬性有點類似,並且它不能使用外部類的非 static 成員變量或者方法,這點很好理解,因為在沒有外部類的對象的情況下,可以創建靜態內部類的對象,如果允許訪問外部類的非 static 成員就會產生矛盾,因為外部類的非 static 成員必須依附於具體的對象。

例如:

public class Test {
    public static void main(String[] args)  {
        Outter.Inner inner = new Outter.Inner();
    }
}

class Outter {
    public Outter() {
    }

    /**
     * 靜態
     */
    static class Inner {
        public Inner() {
        }
    }
}

深入理解內部類

通過上面的介紹,相比你已經大致了解的內部類的使用,那麼你的心裏想必會有一個疑惑:

為什麼成員內部類可以無條件訪問外部類的成員?

首先我們先定義一個內部類:

public class Outter {
    private Inner inner = null;

    public Outter() {
    }

    public Inner getInnerInstance() {
        if (inner == null)
            inner = new Inner();
        return inner;
    }

    protected class Inner {
        public Inner() {
        }
    }
}

先用 javac 進行編譯,你可以發現會生成兩個文件: Outter$Inner.class 和 Outter.class 。接下來利用javap -p反編譯 Outter$Inner.class ,其結果如下:

Classfile /D:/project/Test/src/test/java/test/Outter$Inner.class
  Last modified 2019-11-25; size 408 bytes
  MD5 checksum b936e37bc77059b83951429e28f3f225
  Compiled from "Outter.java"
public class Outter$Inner
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Fieldref           #3.#13         // test/Outter$Inner.this$0:Ltest/Outter;
   #2 = Methodref          #4.#14         // java/lang/Object."<init>":()V
   #3 = Class              #16            // test/Outter$Inner
   #4 = Class              #19            // java/lang/Object
   #5 = Utf8               this$0
   #6 = Utf8               Ltest/Outter;
   #7 = Utf8               <init>
   #8 = Utf8               (Ltest/Outter;)V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               SourceFile
  #12 = Utf8               Outter.java
  #13 = NameAndType        #5:#6          // this$0:Ltest/Outter;
  #14 = NameAndType        #7:#20         // "<init>":()V
  #15 = Class              #21            // test/Outter
  #16 = Utf8               test/Outter$Inner
  #17 = Utf8               Inner
  #18 = Utf8               InnerClasses
  #19 = Utf8               java/lang/Object
  #20 = Utf8               ()V
  #21 = Utf8               test/Outter
{
  final Outter this$0;
    descriptor: Ltest/Outter;
    flags: ACC_FINAL, ACC_SYNTHETIC

  public Outter$Inner(Outter);
    descriptor: (Ltest/Outter;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:Ltest/Outter;
         5: aload_0
         6: invokespecial #2                  // Method java/lang/Object."<init>":()V
         9: return
      LineNumberTable:
        line 16: 0
        line 17: 9
}
SourceFile: "Outter.java"
InnerClasses:
     protected #17= #3 of #15; //Inner=class test/Outter$Inner of class test/Outter

32行的內容為:final Outter this$0;

學過 C 的朋友應該能知道,這是一個指向外部類 Outter 對象的指針,也就是說編譯器會默認為成員內部類添加一個指向外部類對象的引用,這樣也就解釋了為什麼成員內部類能夠無條件訪問外部類了。

那麼這個引用是如何賦初值的呢?下面接着看內部類的構造器:public Outter$Inner(Outter);

從這裏可以看出,雖然我們在定義的內部類的構造器是無參構造器,但編譯器還是會默認添加一個參數,該參數的類型為指向外部類對象的一個引用,所以成員內部類中的 Outter this&0 指針便指向了外部類對象,因此可以在成員內部類中隨意訪問外部類的成員。

從這裏也間接說明了成員內部類是依賴於外部類的,如果沒有創建外部類的對象,則無法對 Outter this&0 引用進行初始化賦值,也就無法創建成員內部類的對象了。

為什麼局部內部類和匿名內部類只能訪問局部final變量?

我們還是採用和之前一樣的解答方式,先定義一個類:

public class Outter {

    public static void main(String[] args)  {
        Outter outter = new Outter();
        int b = 10;
        outter.test(b);
    }

    public void test(final int b) {
        final int a = 10;
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
    }
}

通過 javac 編譯 Outter,也會生成兩個文件: Outter.class 和 Outter1.class。默認情況下,編譯器會為匿名內部類和局部內部類起名為 Outter$x.class( x 為正整數)。

根據我提供的類,可以思考一個問題:

當 test 方法執行完畢之後,變量 a 的生命周期就結束了,而此時 Thread 對象的生命周期很可能還沒有結束,那麼在 Thread 的 run 方法中繼續訪問變量 a 就變成不可能了,但是又要實現這樣的效果,怎麼辦呢?

Java 採用了複製的手段來解決這個問題。將 Outter$1.class 反編譯可以得到下面的內容:

Classfile /D:/project/Test/src/test/java/test/Outter$1.class
  Last modified 2019-11-25; size 653 bytes
  MD5 checksum 2e238dafbd73356eba22d473c6469082
  Compiled from "Outter.java"
class test.Outter$1 extends java.lang.Thread
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Fieldref           #6.#23         // test/Outter$1.this$0:Ltest/Outter;
   #2 = Fieldref           #6.#24         // test/Outter$1.val$b:I
   #3 = Methodref          #7.#25         // java/lang/Thread."<init>":()V
   #4 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #28.#29        // java/io/PrintStream.println:(I)V
   #6 = Class              #30            // test/Outter$1
   #7 = Class              #32            // java/lang/Thread
   #8 = Utf8               val$b
   #9 = Utf8               I
  #10 = Utf8               this$0
  #11 = Utf8               Ltest/Outter;
  #12 = Utf8               <init>
  #13 = Utf8               (Ltest/Outter;I)V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               run
  #17 = Utf8               ()V
  #18 = Utf8               SourceFile
  #19 = Utf8               Outter.java
  #20 = Utf8               EnclosingMethod
  #21 = Class              #33            // test/Outter
  #22 = NameAndType        #34:#35        // test:(I)V
  #23 = NameAndType        #10:#11        // this$0:Ltest/Outter;
  #24 = NameAndType        #8:#9          // val$b:I
  #25 = NameAndType        #12:#17        // "<init>":()V
  #26 = Class              #36            // java/lang/System
  #27 = NameAndType        #37:#38        // out:Ljava/io/PrintStream;
  #28 = Class              #39            // java/io/PrintStream
  #29 = NameAndType        #40:#35        // println:(I)V
  #30 = Utf8               test/Outter$1
  #31 = Utf8               InnerClasses
  #32 = Utf8               java/lang/Thread
  #33 = Utf8               test/Outter
  #34 = Utf8               test
  #35 = Utf8               (I)V
  #36 = Utf8               java/lang/System
  #37 = Utf8               out
  #38 = Utf8               Ljava/io/PrintStream;
  #39 = Utf8               java/io/PrintStream
  #40 = Utf8               println
{
  final int val$b;
    descriptor: I
    flags: ACC_FINAL, ACC_SYNTHETIC

  final test.Outter this$0;
    descriptor: Ltest/Outter;
    flags: ACC_FINAL, ACC_SYNTHETIC

  test.Outter$1(test.Outter, int);
    descriptor: (Ltest/Outter;I)V
    flags:
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:Ltest/Outter;
         5: aload_0
         6: iload_2
         7: putfield      #2                  // Field val$b:I
        10: aload_0
        11: invokespecial #3                  // Method java/lang/Thread."<init>":()V
        14: return
      LineNumberTable:
        line 10: 0

  public void run();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: bipush        10
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
         8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_0
        12: getfield      #2                  // Field val$b:I
        15: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        18: return
      LineNumberTable:
        line 12: 0
        line 13: 8
        line 14: 18
}
SourceFile: "Outter.java"
EnclosingMethod: #21.#22                // test.Outter.test
InnerClasses:
     #6; //class test/Outter$1

我們看到在 run 方法中有一條指令:bipush 10

這條指令表示將操作數10壓棧,表示使用的是一個本地局部變量。

這個過程是在編譯期間由編譯器默認進行,如果這個變量的值在編譯期間可以確定,則編譯器默認會在匿名內部類(局部內部類)的常量池中添加一個內容相等的字面量或直接將相應的字節碼嵌入到執行字節碼中。

這樣一來,匿名內部類使用的變量是另一個局部變量,只不過值和方法中局部變量的值相等,因此和方法中的局部變量完全獨立開。

接下來也來看一下 test.Outter$1 的構造方法:test.Outter$1(test.Outter, int);

我們看到匿名內部類 Outter$1 的構造器含有兩個參數,一個是指向外部類對象的引用,一個是 int 型變量,很顯然,這裡是將變量 test 方法中的形參 b 以參數的形式傳進來對匿名內部類中的拷貝(變量 b 的拷貝)進行賦值初始化。

也就說如果局部變量的值在編譯期間就可以確定,則直接在匿名內部裏面創建一個拷貝。如果局部變量的值無法在編譯期間確定,則通過構造器傳參的方式來對拷貝進行初始化賦值。

從上面可以看出,在 run 方法中訪問的變量 b 根本就不是test方法中的局部變量 b 。這樣一來就解決了前面所說的 生命周期不一致的問題。但是新的問題又來了,既然在 run 方法中訪問的變量 b 和test方法中的變量 b 不是同一個變量,那麼當在 run 方法中改變變量 b 的值的話,會出現什麼情況?

會造成數據不一致性,這樣就達不到原本的意圖和要求。為了解決這個問題, Java 編譯器就限定必須將變量 b 限製為 final ,不允許對變量 b 進行更改(對於引用類型的變量,是不允許指向新的對象),這樣數據不一致性的問題就得以解決了。

到這裏,想必大家應該清楚為何 方法中的局部變量和形參都必須用 final 進行限定了。

靜態內部類有特殊的地方嗎?

從前面可以知道,靜態內部類是不依賴於外部類的,也就說可以在不創建外部類對象的情況下創建內部類的對象。

另外,靜態內部類是不持有指向外部類對象的引用的,這個讀者可以自己嘗試反編譯 class 文件看一下就知道了,是沒有 Outter this&0 引用的。

總結

今天介紹了內部類相關的知識,包括其一般的用法以及內部類和外部類的依賴關係,通過對字節碼進行反編譯詳細了解了其實現模式,最後留給大家一個任務自己去實際探索一下靜態內部類的實現。希望通過這篇介紹可以幫大家更加深刻了解內部類。

有興趣的話可以訪問我的博客或者關注我的公眾號、頭條號,說不定會有意外的驚喜。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

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

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

Java併發之volatile關鍵字

引言

說到多線程,我覺得我們最重要的是要理解一個臨界區概念。

舉個例子,一個班上1個女孩子(臨界區),49個男孩子(線程),男孩子的目標就是這一個女孩子,就是會有競爭關係(線程安全問題)。推廣到實際場景,例如對一個數相加或者相減等等情形,因為操作對象就只有一個,在多線程環境下,就會產生線程安全問題。理解臨界區概念,我們對多線程問題可以有一個好意識。

Jav內存模型(JMM)

談到多線程就應該了解一下Java內存模型(JMM)的抽象示意圖.下圖:

線程A和線程B執行的是時候,會去讀取共享變量(臨界區),然後各自拷貝一份回到自己的本地內存,執行後續操作。
JMM模型是一種規範,就像Java的接口一樣。JMM會涉及到三個問題:原子性,可見性,有序性。
所謂原子性。就是說一個線程的執行會不會被其他線程影響的。他是不可中斷的。舉個例子:

int i=1

這個語句在Jmm中就是原子性的。無論是一個線程執行還是多個線程執行這個語句,讀出來的i就是等於1。那什麼是非原子性呢,按道理如果Java的代碼都是原子性,應該就不會有線程問題了啊。其實JMM這是規定某些語句是原子性罷了。舉個非原子性例子:

i ++;

這個操作就不是原子性的了。因為他就是包含了三個操作:第一讀取i的值,第二將i加上1,第三將結果賦值回來給i,更新i的值。
所謂可見性。可見性表示如果一個值在線程A修改了,線程B就會馬上知道這個結果。
所謂有序性。所謂有序性值的是語意的有序性。就是說代碼順序可能會發生變化。因為有一個指令重排機制。所謂指令重排,他會改變代碼執行順序,為了讓cpu執行效率更高。為了防止重排序出錯,JMM有個happen-before規則,這個規則限制了那些語句執行在前,那些語句執行在後。
Happen-before:
程序順序原則:一個線程內保證語義的串行性
volatile原則:volatile變量的寫發生在讀之前
鎖規則:先加鎖再解鎖
傳遞性:a先於b,b先於c,則a必定先於c
線程的start方法先於他的每一個操作
線程所有的操作先於線程的終結
對象的構造函數執行、結束先於finalize()方法。

volatile

進入正題,volatile可以保證變量(臨界區)的可見性以及有序性,但是不能保證原子性。舉個例子:

public class VolatileTest implements Runnable{
    private static VolatileTest volatileTest = new VolatileTest();
    private  static volatile int i= 0;
    public static void main(String[] args) throws InterruptedException {
        for (int j = 0; j < 20; j++) {
            Thread a = new Thread(new VolatileTest());
            Thread b = new Thread(new VolatileTest());
            a.start();b.start();
            a.join();b.join();
            System.out.print(i+"&&");
        }

    }
    
    @Override
    public void run() {
        for (int j = 0; j < 1000; j++) {
            i++;
        }
    }

}

// 輸出結果
// 2000&&4000&&5852&&7852&&9852&&11852&&13655&&15655&&17655&&19655&&21306     
//&&22566&&24566&&26189&&28189&&30189&&32189&&34189&&36189&&38089&&

有結果看到有問題,雖然i已經添加了volatile關鍵字,說明volatile關鍵字不能保證i++的原子性。

那什麼場景適合使用volatile關鍵字

  1. 輕量級的“讀-寫鎖”策略
private volatile int value;
public int getValue(){ return value;}
public synchronized void doubleValue(){ value = value*value; }

2.單例模式(雙檢查鎖機制

private volatile static Singleton instace;   
public static Singleton getInstance(){  // 沒有使用同步方法,而是同步方法塊
    //第一次null檢查 ,利用volatile的線程間可見性,不需要加鎖,性能提高    
    if(instance == null){            
        synchronized(Singleton.class) {    //鎖住類對象,阻塞其他線程
            //第二次null檢查,以保證不會創建重複的實例       
            if(instance == null){       
                instance = new Singleton(); // 禁止重排序
            }  
        }           
    }  
    return instance;

參考

《現代操作系統(第三版)中文版》
《實戰Java高併發程序設計》
《Java併發編程的藝術》

如果我的文章幫助到您,可以關注我的微信公眾號,第一時間分享文章給您

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

政府重視電動機車電池電芯國產化,2020 持續補助購買電動機車

行政院副院長陳其邁在 7 月 15 日參訪 Gogoro 智能工廠,強調政府對電動機車電池電芯國產化的重視,並表示明年還是會持續補助消費者購買電動機車。

行政院副院長陳其邁與政務委員龔明鑫、經濟部次長林全能和環保署副署長沈志修等人前往桃園市龜山區,參訪 Gogoro 的智能工廠。除了現場試乘 5 月發表的新車種 Gogoro 3 車款並體驗交換電池外,也與 Gogoro 執行長陸學森針對電池電芯國產化進程與傳統機車產業合作等議題進行交流。

陳其邁表示,政府非常重視電動機車電池電芯國產化,目前除了經濟部的科專與 A+ 計畫外,對於國產電池的模組設計、儲能技術與電池的智慧管理也提供計畫類型的補助,希望加速國產電芯量產,並建立相關驗證機制,為國內電動機車產業鏈提供幫助。陳其邁強調,政府也相當重視電動機車產業與傳統機車行合作的問題,希望傳統機車行未來不論在營運、銷售與後續維修,都能與電動機車業者有更多合作機會,協助推動傳統機車行升級和轉型。

根據陳其邁的說法,政府在 2019 年對購買電動機車的補助維持不變,2020 年的補助還會持續下去。政府也研議鼓勵民眾淘汰排放污染的老舊機車,補助購買更環保的燃油機車,讓民眾使用的機車兼顧便捷與環保。

Gogoro 執行長陸學森表示,截至 2019 年 6 月底,Gogoro 生產銷售的電動機車已近 18 萬輛,全台已布建 1,283 個電池交換站,在 6 大都會區更是每 5 分鐘車程就有可即時換電的電池充電站。Gogoro 的每一部車子都是在桃園市龜山區的工廠生產製造,而且機車上的所有零組件,均來自全台 192 家供應商的供應鏈,做到電動機車的國產化。陸學森也向陳其邁建言,希望在台灣目前重型電動機車技術仍領先全球 3 到 5 年的優勢下,政府能夠支持電動機車產業,讓台灣的電動機車產業團結一致,一起為台灣打贏「世界盃」。

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

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

【其他文章推薦】

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

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

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

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

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

科大訊飛研發的全球中文學習平台上線

  隨着中國國際影響力的日益提升,漢語學習的需求與日俱增,為此,教育部、國家語委在《國家語言文字事業“十三五”發展規劃》中明確提出要“建設適應面廣、影響力大、權威性強的全球中文學習網絡平台”。10 月 25 日,在教育部、國家語委的指導下,由科大訊飛研發的全球中文學習平台正式上線。

  該平台上線發布儀式在京舉行。教育部副部長、國家語委主任田學軍,北京市人民政府副秘書長韓耕、科大訊飛股份有限公司董事長劉慶峰和人民教育出版社社長黃強共同為平台發布舉行了啟動儀式,200 多位中外嘉賓見證了全球中文學習平台(www.chinese-learning.cn)的正式上線。

  針對海外學習者的“譯學中文”模塊,學習者可以通過語音或文本輸入其母語內容,實時翻譯出中文並自動分句。學習者學習每個語句的標準音並錄音跟讀,系統會實時反饋評價,指出發音問題;針對錯誤字詞,可以反覆學習,直到掌握正確中文發音。

  這是科大訊飛承建國家語委的又一個重大項目!2004 年,科大訊飛承擔了國家語委“十五”重點科研項目“智能語音技術在普通話輔助學習中的應用研究”;2016 年,承擔國家語委“十三五”重大科研項目“智能語音及人工智能技術在語言學習中的應用研究”。目前,上述兩大項目均已成功落地,並取得了良好的社會效益。

  全球中文學習平台,匯聚各類中文學習資源,以更好地為廣大中文學習者提供優質服務為宗旨,於 2016 年底啟動建設,是落實《國家語言文字事業“十三五”發展規劃》相關任務要求的具體舉措。在教育部、國家語委與科大訊飛的共同努力下,歷經兩年多時間的不斷完善和改進,平台建設取得积極成效,相關基礎研究取得重要進展,為平台提供了堅實技術保障。其中智能語音、智能寫作和批改等關鍵技術研究成果在中小學語言能力評價、少數民族國家通用語言學習等方面得到實際應用。平台示範功能已分別在“砥礪奮進的五年”大型成就展、第二屆語博會、第十二屆孔子學院大會等不同場合進行展示,得到了各方好評。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Springboot 系列(十六)你真的了解 Swagger 文檔嗎?

前言

目前來說,在 Java 領域使用 Springboot 構建微服務是比較流行的,在構建微服務時,我們大多數會選擇暴漏一個 REST API 以供調用。又或者公司採用前後端分離的開發模式,讓前端和後端的工作由完全不同的工程師進行開發完成。不管是微服務還是這種前後端分離開發,維持一份完整的及時更新的 REST API 文檔,會極大的提高我們的工作效率。而傳統的文檔更新方式(如手動編寫),很難保證文檔的及時性,經常會年久失修,失去應有的意義。因此選擇一種新的 API 文檔維護方式很有必要,這也是這篇文章要介紹的內容。

1. OpenAPI 規範介紹

OpenAPI Specification 簡稱 OAS,中文也稱 OpenAPI 描述規範,使用 OpenAPI 文件可以描述整個 API,它制定了一套的適合通用的與語言無關的 REST API 描述規範,如 API 路徑規範、請求方法規範、請求參數規範、返回格式規範等各種相關信息,使人類和計算機都可以不需要訪問源代碼就可以理解和使用服務的功能。

下面是 OpenAPI 規範中建議的 API 設計規範,基本路徑設計規範。

https://api.example.com/v1/users?role=admin&status=active
\________________________/\____/ \______________________/
         server URL       endpoint    query parameters
                            path

對於傳參的設計也有規範,可以像下面這樣:

  • , 例如 /users/{id}
  • , 例如 /users?role=未讀代碼
  • , 例如 X-MyHeader: Value
  • , 例如 Cookie: debug=0; csrftoken=BUSe35dohU3O1MZvDCU

OpenAPI 規範的東西遠遠不止這些,目前 OpenAPI 規範最新版本是 3.0.2,如果你想了解更多的 OpenAPI 規範,可以訪問下面的鏈接。

2. Swagger 介紹

很多人都以為 Swagger 只是一個接口文檔生成框架,其實並不是。 Swagger 是一個圍繞着 OpenAPI Specification(OAS,中文也稱 OpenAPI規範)構建的一組開源工具。可以幫助你從 API 的設計到 API 文檔的輸出再到 API 的測試,直至最後的 API 部署等整個 API 的開發周期提供相應的解決方案,是一個龐大的項目。 Swagger 不僅免費,而且開源,不管你是企業用戶還是個人玩家,都可以使用 Swagger 提供的方案構建令人驚艷的 REST API

Swagger 有幾個主要的產品。

  • – 一個基於瀏覽器的 Open API 規範編輯器。
  • – 一個將 OpenAPI 規範呈現為可交互在線文檔的工具。
  • – 一個根據 OpenAPI 生成調用代碼的工具。

如果你想了解更多信息,可以訪問 Swagger 官方網站 。

3. Springfox 介紹

源於 Java 中 Spring 框架的流行,讓一個叫做 Marrty Pitt 的老外有了為 SpringMVC 添加接口描述的想法,因此他創建了一個遵守 OpenAPI 規範(OAS)的項目,取名為 swagger-springmvc,這個項目可以讓 Spring 項目自動生成 JSON 格式的 OpenAPI 文檔。這個框架也仿照了 Spring 項目的開發習慣,使用註解來進行信息配置。

後來這個項目發展成為 Springfox,再後來擴展出 springfox-swagger2 ,為了讓 JSON 格式的 API 文檔更好的呈現,又出現了 springfox-swagger-ui 用來展示和測試生成的 OpenAPI 。這裏的 springfox-swagger-ui 其實就是上面介紹的 Swagger-ui,只是它被通過 webjar 的方式打包到 jar 包內,並通過 maven 的方式引入進來。

上面提到了 Springfox-swagger2 也是通過註解進行信息配置的,那麼是怎麼使用的呢?下面列舉常用的一些註解,這些註解在下面的 Springboot 整合 Swagger 中會用到。

註解 示例 描述
@ApiModel @ApiModel(value = “用戶對象”) 描述一個實體對象
@ApiModelProperty @ApiModelProperty(value = “用戶ID”, required = true, example = “1000”) 描述屬性信息,執行描述,是否必須,給出示例
@Api @Api(value = “用戶操作 API(v1)”, tags = “用戶操作接口”) 用在接口類上,為接口類添加描述
@ApiOperation @ApiOperation(value = “新增用戶”) 描述類的一個方法或者說一個接口
@ApiParam @ApiParam(value = “用戶名”, required = true) 描述單個參數

更多的 Springfox 介紹,可以訪問 Springfox 官方網站。

4. Springboot 整合 Swagger

就目前來說 ,Springboot 框架是非常流行的微服務框架,在微服務框架下,很多時候我們都是直接提供 REST API 的。REST API 如果沒有文檔的話,使用者就很頭疼了。不過不用擔心,上面說了有一位叫 Marrty Pitt 的老外已經創建了一個發展成為 Springfox 的項目,可以方便的提供 JSON 格式的 OpenAPI 規範和文檔支持。且擴展出了 springfox-swagger-ui 用於頁面的展示。

需要注意的是,這裏使用的所謂的 Swagger 其實和真正的 Swagger 並不是一個東西,這裏使用的是 Springfox 提供的 Swagger 實現。它們都是基於 OpenAPI 規範進行 API 構建。所以也都可以 Swagger-ui 進行 API 的頁面呈現。

4.1. 創建項目

如何創建一個 Springboot 項目這裏不提,你可以直接從 下載一個標準項目,也可以使用 idea 快速創建一個 Springboot 項目,也可以順便拷貝一個 Springboot 項目過來測試,總之,方式多種多樣,任你選擇。

下面演示如何在 Springboot 項目中使用 swagger2。

4.2. 引入依賴

這裏主要是引入了 springfox-swagger2,可以通過註解生成 JSON 格式的 OpenAPI 接口文檔,然後由於 Springfox 需要依賴 jackson,所以引入之。springfox-swagger-ui 可以把生成的 OpenAPI 接口文檔显示為頁面。Lombok 的引入可以通過註解為實體類生成 get/set 方法。

<dependencies> 
    <!-- Spring Boot web 開發整合 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <artifactId>spring-boot-starter-json</artifactId>
                <groupId>org.springframework.boot</groupId>
            </exclusion>
        </exclusions>
    </dependency>

    <!-- 引入swagger2的依賴-->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    
    <!-- jackson相關依賴 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.5.4</version>
    </dependency>

    <!-- Lombok 工具 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

4.3. 配置 Springfox-swagger

Springfox-swagger 的配置通過一個 Docket 來包裝,Docket 里的 apiInfo 方法可以傳入關於接口總體的描述信息。而 apis 方法可以指定要掃描的包的具體路徑。在類上添加 @Configuration 聲明這是一個配置類,最後使用 @EnableSwagger2 開啟 Springfox-swagger2。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * <p>
 * Springfox-swagger2 配置
 *
 * @Author niujinpeng
 * @Date 2019/11/19 23:17
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("net.codingme.boot.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("未讀代碼 API")
                .description("公眾號:未讀代碼(weidudaima) springboot-swagger2 在線借口文檔")
                .termsOfServiceUrl("https://www.codingme.net")
                .contact("達西呀")
                .version("1.0")
                .build();
    }
}

4.4. 代碼編寫

文章不會把所有代碼一一列出來,這沒有太大意義,所以只貼出主要代碼,完整代碼會上傳到 Github,並在文章底部附上 Github 鏈接。

參數實體類 User.java,使用 @ApiModel@ApiModelProperty 描述參數對象,使用 @NotNull 進行數據校驗,使用 @Data 為參數實體類自動生成 get/set 方法。

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;

import javax.validation.constraints.NotNull;
import java.util.Date;

/**
 * <p>
 * 用戶實體類
 *
 * @Author niujinpeng
 * @Date 2018/12/19 17:13
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "用戶對象")
public class User {

    /**
     * 用戶ID
     *
     * @Id 主鍵
     * @GeneratedValue 自增主鍵
     */
    @NotNull(message = "用戶 ID 不能為空")
    @ApiModelProperty(value = "用戶ID", required = true, example = "1000")
    private Integer id;

    /**
     * 用戶名
     */
    @NotNull(message = "用戶名不能為空")
    @ApiModelProperty(value = "用戶名", required = true)
    private String username;
    /**
     * 密碼
     */
    @NotNull(message = "密碼不能為空")
    @ApiModelProperty(value = "用戶密碼", required = true)
    private String password;
    /**
     * 年齡
     */
    @ApiModelProperty(value = "用戶年齡", example = "18")
    private Integer age;
    /**
     * 生日
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd hh:mm:ss")
    @ApiModelProperty(value = "用戶生日")
    private Date birthday;
    /**
     * 技能
     */
    @ApiModelProperty(value = "用戶技能")
    private String skills;
}

編寫 Controller 層,使用 @Api 描述接口類,使用 @ApiOperation 描述接口,使用 @ApiParam 描述接口參數。代碼中在查詢用戶信息的兩個接口上都添加了 tags = "用戶查詢" 標記,這樣這兩個方法在生成 Swagger 接口文檔時候會分到一個共同的標籤組裡。

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import net.codingme.boot.domain.Response;
import net.codingme.boot.domain.User;
import net.codingme.boot.enums.ResponseEnum;
import net.codingme.boot.utils.ResponseUtill;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;

/**
 * <p>
 * 用戶操作
 *
 * @Author niujinpeng
 * @Date 2019/11/19 23:17
 */

@Slf4j
@RestController(value = "/v1")
@Api(value = "用戶操作 API(v1)", tags = "用戶操作接口")
public class UserController {

    @ApiOperation(value = "新增用戶")
    @PostMapping(value = "/user")
    public Response create(@Valid User user, BindingResult bindingResult) throws Exception {
        if (bindingResult.hasErrors()) {
            String message = bindingResult.getFieldError().getDefaultMessage();
            log.info(message);
            return ResponseUtill.error(ResponseEnum.ERROR.getCode(), message);
        } else {
            // 新增用戶信息 do something
            return ResponseUtill.success("用戶[" + user.getUsername() + "]信息已新增");
        }
    }

    @ApiOperation(value = "刪除用戶")
    @DeleteMapping(value = "/user/{username}")
    public Response delete(@PathVariable("username")
                           @ApiParam(value = "用戶名", required = true) String name) throws Exception {
        // 刪除用戶信息 do something
        return ResponseUtill.success("用戶[" + name + "]信息已刪除");
    }

    @ApiOperation(value = "修改用戶")
    @PutMapping(value = "/user")
    public Response update(@Valid User user, BindingResult bindingResult) throws Exception {
        if (bindingResult.hasErrors()) {
            String message = bindingResult.getFieldError().getDefaultMessage();
            log.info(message);
            return ResponseUtill.error(ResponseEnum.ERROR.getCode(), message);
        } else {
            String username = user.getUsername();
            return ResponseUtill.success("用戶[" + username + "]信息已修改");
        }
    }

    @ApiOperation(value = "獲取單個用戶信息", tags = "用戶查詢")
    @GetMapping(value = "/user/{username}")
    public Response get(@PathVariable("username")
                        @NotNull(message = "用戶名稱不能為空")
                        @ApiParam(value = "用戶名", required = true) String username) throws Exception {
        // 查詢用戶信息 do something
        User user = new User();
        user.setId(10000);
        user.setUsername(username);
        user.setAge(99);
        user.setSkills("cnp");
        return ResponseUtill.success(user);
    }

    @ApiOperation(value = "獲取用戶列表", tags = "用戶查詢")
    @GetMapping(value = "/user")
    public Response selectAll() throws Exception {
        // 查詢用戶信息列表 do something
        User user = new User();
        user.setId(10000);
        user.setUsername("未讀代碼");
        user.setAge(99);
        user.setSkills("cnp");
        List<User> userList = new ArrayList<>();
        userList.add(user);
        return ResponseUtill.success(userList);
    }
}

最後,為了讓代碼變得更加符合規範和好用,使用一個統一的類進行接口響應。

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "響應信息")
public class Response {
    /**
     * 響應碼
     */
    @ApiModelProperty(value = "響應碼")
    private String code;
    /**
     * 響應信息
     */
    @ApiModelProperty(value = "響應信息")
    private String message;

    /**
     * 響應數據
     */
    @ApiModelProperty(value = "響應數據")
    private Collection content;
}

4.5. 運行訪問

直接啟動 Springboog 項目,可以看到控制台輸出掃描到的各個接口的訪問路徑,其中就有 /2/api-docs

這個也就是生成的 OpenAPI 規範的描述 JSON 訪問路徑,訪問可以看到。

因為上面我們在引入依賴時,也引入了 springfox-swagger-ui 包,所以還可以訪問 API 的頁面文檔。訪問路徑是 /swagger-ui.html,訪問看到的效果可以看下圖。

也可以看到用戶查詢的兩個方法會歸到了一起,原因就是這兩個方法的註解上使用相同的 tag 屬性。

4.7. 調用測試

springfox-swagger-ui 不僅是生成了 API 文檔,還提供了調用測試功能。下面是在頁面上測試獲取單個用戶信息的過程。

  1. 點擊接口 [/user/{username}] 獲取單個用戶信息。
  2. 點擊 **Try it out** 進入測試傳參頁面。
  3. 輸入參數,點擊 Execute 藍色按鈕執行調用。
  4. 查看返回信息。

下面是測試時的響應截圖。

5. 常見報錯

如果你在程序運行中經常發現像下面這樣的報錯。

java.lang.NumberFormatException: For input string: ""
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) ~[na:1.8.0_111]
    at java.lang.Long.parseLong(Long.java:601) ~[na:1.8.0_111]
    at java.lang.Long.valueOf(Long.java:803) ~[na:1.8.0_111]
    at io.swagger.models.parameters.AbstractSerializableParameter.getExample(AbstractSerializableParameter.java:412) ~[swagger-models-1.5.20.jar:1.5.20]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_111]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_111]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_111]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_111]
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:536) [jackson-databind-2.5.4.jar:2.5.4]
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:666) [jackson-databind-2.5.4.jar:2.5.4]
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:156) [jackson-databind-2.5.4.jar:2.5.4]
    at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:113) [jackson-databind-2.5.4.jar:2.5.4]

那麼你需要檢查使用了 @ApiModelProperty 註解且字段類型為数字類型的屬性上,@ApiModelProperty 註解是否設置了 example 值,如果沒有,那就需要設置一下,像下面這樣。

@NotNull(message = "用戶 ID 不能為空")
@ApiModelProperty(value = "用戶ID", required = true, example = "1000")
private Integer id;

文中代碼都已經上傳到

參考文檔

個人網站:
如果你喜歡這篇文章,可以關注公眾號,一起成長。
關注公眾號回復資源可以沒有套路的獲取全網最火的的 Java 核心知識整理&面試核心資料。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

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

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

簡單的學習,實現,領域事件,事件存儲,事件溯源

為什麼寫這篇文章

自己以前都走了彎路,以為學習戰術設計就會DDD了,其實DDD的精華在戰略設計,但是對於我們菜鳥來說,學習一些技術概念也是挺好的
經常看到這些術語,概念太多,也想簡單學習一下,記憶力比較差記錄一下實現的細節

領域事件

1.領域事件是過去發生的與業務有關的事實,一但發生就不可更改,所以存儲事件時只能追加

3.領域事件具有時間點的特徵,所有事件連接起來會形成明顯的時間軸

4.領域事件會導致目標對象狀態的變化,聚合根的行為會產生領域事件,所以會改變聚合的狀態

在聚合根裏面維護一個領域事件的聚合,每一個事件對應一個Handle,通過反射維護一個數據字典,通過事件查找到指定的Handle

領域事件實現的方式:目前看到有3種方式,MediatR,消息隊列 ,發布訂閱模式

eShopOnContainers 中使用的是MediatR

ENode 中使用的是EQueue,EQueue是一個純C#寫的消息隊列

使用已經寫好的消息隊列Rabbitmq ,kafka

事件存儲,事件溯源,事件快照

事件存儲:存儲所有聚合根裏面發生過的事件

1.事件存儲中可以做併發的處理,比如Command 重複,領域事件的重複

2.領域事件的重複通過聚合根Id+版本號判斷,可以在數據庫中建立聯合唯一索引,在存儲事件時檢測重複,記錄重複的事件,根據業務做處理

3.這裏要保證存儲事件與發布領域事件的一致性

如何保證存儲事件與發布領域事件的一致性

先存儲事件然後在發布領域事件,如果發生異常,就一直重試,一直到成功為止,也可以做一定的處理,比如重試到一定的次數,就通知,進行人工處理

我選擇了CAP + Policy + Dapper

事件溯源:在事件存儲中記錄導致狀態變化的一系列領域事件。通過持久化記錄改變狀態的事件,通過重新播放獲得狀態改變的歷史。 事件回放可以返回系統到任何狀態

聚合快照:聚合的生命周期各有長短,有的聚合裏面有大量的事件,,事件越多加載事件以及重建聚合的執行效率就會越來越低,快照裏面存儲的是聚合

1.定時存儲整個聚合根:使用定時器每隔一段時間就存儲聚合到快照表中

2.定量存儲整個聚合根:根據事件存儲中的數量來存儲聚合到快照表中

事件溯源的實現方式

1.首先我們需要實現聚合In Memory,

2.在CommandHandler中訂閱 Command命令,

創建聚合時 ,在內存中維護一個數據字典,key為:聚合根的Id,value為:聚合

修改,刪除,聚合時,根據聚合根的Id,查詢出聚合

如果內存中聚合不存在時:根據聚合根的Id 從聚合快照表中查詢出聚合,然後根據聚合快照存儲的時間,聚合根Id,查詢事件存儲中的所有事件,然後回放事件,得到聚合最終的狀態

記錄遇到的問題

由於基礎非常的差,所以實現的方式都是以最簡單的方式來寫的,存在許多的問題,代碼中有問題的地方希望大家提出來,讓我學習一下

代碼的實現目前還沒有寫快照的部分,也沒有處理EventStorage中的命令重複與聚合根+版本號重複,具體的請看湯總的ENode,裏面有全部的實現

1.怎樣保證存儲事件,發布事件的最終一致性

2.怎麼解析EventStorage中的事件,回放事件

先存儲事件,當事件存儲成功之後,在發布事件

存儲事件失敗:就一直重試,發布事件失敗,使用的是CAP,CAP內部使用的是本地消息表的方式,如果發布事件失敗,也一直重試,如果服務器重啟了,Rabbitmq裏面消息為Ack,消息沒有丟,重連後會繼續執行

存儲事件,發布事件

    /// <summary>
    /// 存儲聚合根中的事件到EventStorage 發布事件
    /// </summary>
    /// <typeparam name="TAggregationRoot"></typeparam>
    /// <param name="event"></param>
    /// <returns></returns>
    public async Task AppendEventStoragePublishEventAsync<TAggregationRoot>(TAggregationRoot @event)
        where TAggregationRoot : IAggregationRoot
    {
        var domainEventList = @event.UncommittedEvents.ToList();
        if (domainEventList.Count == 0)
        {
            throw new Exception("請添加事件!");
        }

        await TryAppendEventStorageAsync(domainEventList).ContinueWith(async e =>
        {
            if (e.Result == (int)EventStorageStatus.Success)
            {
                await TryPublishDomainEventAsync(domainEventList).ConfigureAwait(false);
                @event.ClearEvents();
            }
        });
    }

    /// <summary>
    /// 發布領域事件
    /// </summary>
    /// <returns></returns>
    public async Task PublishDomainEventAsync(List<IDomainEvent> domainEventList)
    {
        using (var connection =
            new SqlConnection(ConnectionStr))
        {
            if (connection.State == ConnectionState.Closed)
            {
                await connection.OpenAsync().ConfigureAwait(false);
            }
            using (var transaction = await connection.BeginTransactionAsync().ConfigureAwait(false))
            {
                try
                {
                    if (domainEventList.Count > 0)
                    {
                        foreach (var domainEvent in domainEventList)
                        {
                            await _capPublisher.PublishAsync(domainEvent.GetRoutingKey(), domainEvent).ConfigureAwait(false);
                        }
                    }
                    await transaction.CommitAsync().ConfigureAwait(false);
                }
                catch (Exception e)
                {
                    await transaction.RollbackAsync().ConfigureAwait(false);
                    throw;
                }
            }
        }
    }

    /// <summary>
    /// 發布領域事件重試
    /// </summary>
    /// <param name="domainEventList"></param>
    /// <returns></returns>
    public async Task TryPublishDomainEventAsync(List<IDomainEvent> domainEventList)
    {
        var policy = Policy.Handle<SocketException>().Or<IOException>().Or<Exception>()
            .RetryForeverAsync(onRetry: exception =>
            {
                Task.Factory.StartNew(() =>
                {
                    //記錄重試的信息
                    _loggerHelper.LogInfo("發布領域事件異常", exception.Message);
                });
            });
        await policy.ExecuteAsync(async () =>
        {
            await PublishDomainEventAsync(domainEventList).ConfigureAwait(false);
        });

    }

    /// <summary>
    /// 存儲聚合根中的事件到EventStorage中
    /// </summary>
    /// <returns></returns>
    public async Task<int> AppendEventStorageAsync(List<IDomainEvent> domainEventList)
    {
        if (domainEventList.Count == 0)
        {
            throw new Exception("請添加事件!");
        }
        var status = (int)EventStorageStatus.Failure;
        using (var connection = new SqlConnection(ConnectionStr))
        {
            try
            {
                if (connection.State == ConnectionState.Closed)
                {
                    await connection.OpenAsync().ConfigureAwait(false);
                }
                using (var transaction = await connection.BeginTransactionAsync().ConfigureAwait(false))
                {
                    try
                    {
                        if (domainEventList.Count > 0)
                        {
                            foreach (var domainEvent in domainEventList)
                            {
                                EventStorage eventStorage = new EventStorage
                                {
                                    Id = Guid.NewGuid(),
                                    AggregateRootId = domainEvent.AggregateRootId,
                                    AggregateRootType = domainEvent.AggregateRootType,
                                    CreateDateTime = domainEvent.CreateDateTime,
                                    Version = domainEvent.Version,
                                    EventData = Events(domainEvent)
                                };
                                var eventStorageSql =
                                    $"INSERT INTO EventStorageInfo(Id,AggregateRootId,AggregateRootType,CreateDateTime,Version,EventData) VALUES (@Id,@AggregateRootId,@AggregateRootType,@CreateDateTime,@Version,@EventData)";
                                await connection.ExecuteAsync(eventStorageSql, eventStorage, transaction).ConfigureAwait(false);
                            }
                        }
                        await transaction.CommitAsync().ConfigureAwait(false);
                        status = (int)EventStorageStatus.Success;
                    }
                    catch (Exception e)
                    {
                        await transaction.RollbackAsync().ConfigureAwait(false);
                        throw;
                    }
                }

            }
            catch (Exception e)
            {
                connection.Close();
                throw;
            }
        }
        return status;
    }

    /// <summary>
    /// AppendEventStorageAsync異常重試
    /// </summary>
    public async Task<int> TryAppendEventStorageAsync(List<IDomainEvent> domainEventList)
    {
        var policy = Policy.Handle<SocketException>().Or<IOException>().Or<Exception>()
            .RetryForeverAsync(onRetry: exception =>
            {
                Task.Factory.StartNew(() =>
                {
                    //記錄重試的信息
                    _loggerHelper.LogInfo("存儲事件異常", exception.Message);
                });
            });
        var result = await policy.ExecuteAsync(async () =>
          {
              var resulted = await AppendEventStorageAsync(domainEventList).ConfigureAwait(false);
              return resulted;
          });
        return result;
    }

    /// <summary>
    /// 根據DomainEvent序列化事件Json
    /// </summary>
    /// <param name="domainEvent"></param>
    /// <returns></returns>
    public string Events(IDomainEvent domainEvent)
    {
        ConcurrentDictionary<string, string> dictionary = new ConcurrentDictionary<string, string>();
        //獲取領域事件的類型(方便解析Json)
        var domainEventTypeName = domainEvent.GetType().Name;
        var domainEventStr = JsonConvert.SerializeObject(domainEvent);
        dictionary.GetOrAdd(domainEventTypeName, domainEventStr);
        var eventData = JsonConvert.SerializeObject(dictionary);
        return eventData;
    }

解析EventStorage中存儲的事件

    public async Task<List<IDomainEvent>> GetAggregateRootEventStorageById(Guid AggregateRootId)
    {
        try
        {
            using (var connection = new SqlConnection(ConnectionStr))
            {
                var eventStorageList = await connection.QueryAsync<EventStorage>($"SELECT * FROM dbo.EventStorageInfo WHERE AggregateRootId='{AggregateRootId}'");
                List<IDomainEvent> domainEventList = new List<IDomainEvent>();
                foreach (var item in eventStorageList)
                {
                    var dictionaryDomainEvent = JsonConvert.DeserializeObject<Dictionary<string, string>>(item.EventData);
                    foreach (var entry in dictionaryDomainEvent)
                    {
                        var domainEventType = TypeNameProvider.GetType(entry.Key);
                        if (domainEventType != null)
                        {
                            var domainEvent = JsonConvert.DeserializeObject(entry.Value, domainEventType) as IDomainEvent;
                            domainEventList.Add(domainEvent);
                        }
                    }
                }
                return domainEventList;
            }
        }
        catch (Exception ex)
        {
            throw;
        }

注意事項

1.事件沒持久化就代表事件還沒發生成功,事件存儲可能失敗,必須先存儲事件,在發布事件,保證存儲事件與發布事件一致性
1.使用事件驅動,必須要做好冥等的處理
2.如果業務場景中有狀態時:通過狀態來控制
3.新建一張表,用來記錄消費的信息,消費端的代碼裏面,根據唯一的標識,判斷是否處理過該事件
4.Q端的任何更新都應該把聚合根ID和事件版本號作為條件,Q端的更新不用遵循聚合的原則,可以使用最簡單的方式處理
5.倉儲是用來重建聚合的,它的行為和集合一樣只有Get ,Add ,Delete
6.DDD不是技術,是思想,核心在戰略模塊,戰術設計是實現的一種選擇,戰略設計,需要面向對象的分析能力,職責分配,深層次的分析業務

感謝

雖然學習DDD的時間不短了,感覺還是在入門階段,在學習的過程中有許多的不解,經常問ENode群裏面的大佬,也經常@湯總,謝謝大家的幫助與解惑。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Class文件結構全面解析(下)

接上回書

書接,分享了Class文件的主要構成,同時也詳細分析了魔數、次版本號、主版本號、常量池集合、訪問標誌的構造,接下來我們就繼續學習。

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

類索引和父類索引

類索引(this_class)和父類索引(super_class)都是一個u2類型的數據,類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類全限定名。由於java語言不允許多重繼承,所以父類索引只有一個。

類索引和父類索引各自指向常量池中類型為CONSTANT_Class_info的類描述符,再通過類描述符中的索引值找到常量池中類型為CONSTANT_Utf8_info的字符串。再來看一下之前的Class文件例子:

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

結合之前javap分析出來的常量池內容:

   #3 = Class         #17        // OneMoreStudy
   #4 = Class         #18        // java/lang/Object
  #17 = Utf8          OneMoreStudy
  #18 = Utf8          java/lang/Object

類索引為0x0003,去常量池裡找索引為3的類描述符,類描述符中的索引為17,再去找索引為17的字符串,就是“OneMoreStudy”。

父類索引為0x0004,去常量池裡找索引為4的類描述符,類描述符中的索引為18,再去常量池裡找索引為18的字符串,就是“java/lang/Object”。

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

接口索引集合

接口索引集合(interface)是一組u2類型的數據的集合,由於java語言允許實現多個接口,所以接口索引也有多個,它們按照implements語句后的接口順序從左到右依次排列在接口索引集合中。接口索引集合的第一項數據是接口集合計數值(interfaces_count),表示有多少接口索引。如果該類沒有實現任何接口,那麼該計數值為0,後面的接口索引表不佔任何字節。之前的例子OneMoreStudy類沒有實現任何接口,所以接口集合計數值就是0,如下圖:

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

字段表集合

字段表(field_info)是用來描述接口或類中聲明的變量。包括類級變量(靜態變量)和實例級變量(成員變量),但是不包括在方法內部聲明的局部變量。具體結構如下錶:

類型 名稱 數量 描述
u2 access_flags 1 字段的訪問標誌
u2 name_index 1 字段的簡單名稱索引
u2 descriptor_index 1 字段的描述符索引
u2 attributes_count 1 字段的屬性計數值
attribute_info attributes attributes_count 字段的屬性

字段表中的access_flags,和類的access_flags是非常類似的,但是標識和含義是不一樣的。具體如下錶:

標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 字段是否public
ACC_PRIVATE 0x0002 字段是否private
ACC_PROTECTED 0x0004 字段是否protected
ACC_STATIC 0x0008 字段是否static
ACC_FINAL 0x0010 字段是否為final
ACC_VOLATILE 0x0040 字段是否volatile
ACC_TRANSIENT 0x0080 字段是否transient
ACC_SYNTHETIC 0x1000 字段是否由編譯器自動產生的
ACC_ENUM 0x4000 字段是否enum

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

這裏提到了簡單名稱、描述符,和全限定名有什麼區別呢?稍微說一下。

簡單名稱是沒有類型和參數修飾的方法或字段名稱,比如OneMoreStudy類中的number字段和plusOne()方法的簡單名稱分別是“number”和“plusOne”。

全限定名是把類全名中的“.”替換成“/”就可以了,比如java.lang.Object類的全限定名就是“java/lang/Object”。

描述符是用來描述字段的數據類型、方法的參數列表(包括數量、類型以及順序)和返回值。基礎數據類型和無返回的void類型都有一個大寫字母表示,對象類型用字符L加對象的全限定名來表示,如下錶:

標識字符 含義
B 基本類型byte
C 基本類型char
D 基本類型double
F 基本類型float
I 基本類型int
J 基本類型long
S 基本類型short
Z 基本類型boolean
V 特殊類型void
L 對象類型 如 Ljava/lang/Object

對於數組類型,每一維度使用一個前置的“[”字符來描述,比如java.lang.Object[][]的二維數據,就是“[[Ljava/lang/Object”。在描述方法時,按照先參數列表,后返回值的順序描述,參數列表按照嚴格順序放在“()”值中,比如boolean equals(Object anObject),就是“(Ljava/lang/Object)B”。

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

再來看一下之前的Class文件例子:

OneMoreStudy類中只有一個字段number,所以字段計數值為0x0001。字段number只被private修飾,沒有其他修飾,所以字段的訪問標誌位為0x0002。字段的簡單名稱索引是0x0005,去常量池中找索引為5的字符串,為“number”。字段的描述符索引為0x0006,去常量池中找索引為6的字符串,為“I”,是基本類型int。以下是常量池相關內容:

   #5 = Utf8          number
   #6 = Utf8          I

字段number的屬性計數值為0x0000,也就是沒有需要額外描述的信息。

字段表集合中不會列出從父類或者父接口中繼承而來的字段,但有可能列出原版Java代碼中沒有的字段,比如在內部類中為了保持對外部類的訪問性,會自動添加指向外部類實例的字段。

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

方法表集合

方法表的結構和字段表的是一樣的,也是依次包括了訪問標誌(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)和屬性表集合(attributes)。具體如下錶:

類型 名稱 數量 描述
u2 access_flags 1 方法的訪問標誌
u2 name_index 1 方法的簡單名稱索引
u2 descriptor_index 1 方法的描述符索引
u2 attributes_count 1 方法的屬性計數值
attribute_info attributes attributes_count 方法的屬性

對於方法的訪問標誌,所有標誌位和取值如下錶:

標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 方法是否public
ACC_PRIVATE 0x0002 方法是否private
ACC_PROTECTED 0x0004 方法是否protected
ACC_STATIC 0x0008 方法是否static
ACC_FINAL 0x0010 方法是否為final
ACC_SYNCHRONIZED 0x0020 方法是否sychronized
ACC_BRIDGE 0x0040 方法是否是由編譯器產生的橋接方法
ACC_VARARGS 0x0080 方法是否接受不定參數
ACC_NATIVE 0x0100 方法是否為native
ACC_ABSTRACT 0x0400 方法是否為abstract
ACC_STRICT 0x0800 方法是否為strictfp
ACC_SYNTHETIC 0x1000 方法是否由編譯器自動產生

方法中的Java代碼,經過編譯器編程成字節碼指令后,放在方法屬性表集合中一個名為“Code”的屬性里,後面會有更多分享。

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

再來看一下之前的Class文件例子:

方法計算值為0x0003,表示集合中有兩個方法(編譯器自動添加的無參構造方法和源碼中的plusOne方法)。第一個方法的訪問標誌是0x0001,表示只有ACC_PUBLIC標誌為true。

名稱索引為0x0007,在常量池中為索引為7的字符串為“ ”,這就是編譯器自動添加的無參構造方法。描述符索引為0x0008,在常量池中為索引為7的字符串為“()V”,方法的屬性計數值為0x0001,表示該方法有1個屬性,屬性名稱索引為0x0009,在常量池中為索引為7的字符串為“Code”。以下是常量池相關內容:

   #7 = Utf8          <init>
   #8 = Utf8          ()V
   #9 = Utf8          Code

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

屬性表集合

屬性表(attribute_info)在前面的分享中出現了幾次,在Class文件、字段表、方法表都可以有自己的屬性表集合,用來描述某些場景下特有的信息。

屬性表不在要求具有嚴格的順序,並且只要不與已有的屬性名重複,任何人實現的編譯器都可以寫入自己定義的屬性信息,Java虛擬機在運行時會忽略掉它不認識的屬性。

我總結了一些比較常見的屬性,如下錶:

屬性名稱 使用位置 含義
Code 方法表 Java代碼編譯成的字節碼指令
ConstantValue 字段表 final關鍵字定義的常量值
Exceptions 方法表 方法拋出的異常
InnerClasses 類文件 內部類列表
LineNumberTable Code屬性 Java源碼的行號與字節碼指定的對應關係
LocalVariableTable Code屬性 方法的局部變量描述
SourceFile 類文件 記錄源文件名稱

對於每個屬性,它的名稱都從常量池中引用一個CONSTANT_Utf8_info類型的常量,而屬性值的結構則是完全自定義的,只需要用一個u4類型來說明屬性值所佔的位數就可以了。具體結構如下:

類型 名稱 數量 含義
u2 attribute_name_index 1 屬性名稱索引
u2 attribute_length 1 屬性值所佔的位數
u1 info attribute_length 屬性值

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

總結

Class文件主要由魔數、次版本號、主版本號、常量池集合、訪問標誌、類索引、父類索引、接口索引集合、字段表集合、方法表集合和屬性表集合組成。隨着JDK版本的不斷升級,Class文件結構也在不斷更新,學習之路,永不止步。

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

J.U.C剖析與解讀1(Lock的實現)

J.U.C剖析與解讀1(Lock的實現)

前言

為了節省各位的時間,我簡單介紹一下這篇文章。這篇文章主要分為三塊:Lock的實現,AQS的由來(通過演變的方式),JUC三大工具類的使用與原理剖析。

  • Lock的實現:簡單介紹ReentrantLock,ReentrantReadWriteLock兩種JUC下經典Lock的實現,並通過手寫簡化版的ReentrantLock和ReentrantReadWriteLock,從而了解其實現原理。

  • AQS的由來:通過對兩個簡化版Lock的多次迭代,從而獲得AQS。並且最終的Lock實現了J.U.C下Lock接口,既可以使用我們演變出來的AQS,也可以對接JUC下的AQS。這樣一方面可以幫助大家理解AQS,另一方面大家可以從中了解,如何利用AQS實現自定義Lock。而這兒,對後續JUC下的三大Lock工具的理解有非常大的幫助。

  • JUC三大工具:經過前兩個部分的學習,這個部分不要太easy。可以很容易地理解CountDownLatch,Semaphore,CyclicBarrier的內部運行及實現原理。

不過,由於這三塊內容較多,所以我將它拆分為三篇子文章進行論述。

一,介紹

Lock

Lock接口位於J.U.C下locks包內,其定義了Lock應該具備的方法。

Lock 方法簽名:

  • void lock():獲取鎖(不死不休,拿不到就一直等)
  • boolean tryLock():獲取鎖(淺嘗輒止,拿不到就算了)
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException:獲取鎖(過時不候,在一定時間內拿不到鎖,就算了)
  • void lockInterruptibly() throws InterruptedException:獲取鎖(任人擺布,xxx)
  • void unlock():釋放鎖
  • Condition newCondition():獲得Condition對象

ReentrantLock

簡介

ReentrantLock是一個可重入鎖,一個悲觀鎖,默認是非公平鎖(但是可以通過Constructor設置為公平鎖)。

Lock應用

ReentrantLock通過構造方法獲得lock對象。利用lock.lock()方法對當前線程進行加鎖操作,利用lock.unlock()方法對當前線程進行釋放鎖操作。

Condition應用

通過


    ReentrantLock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

獲得Condition對象(Condition是J.U.C下locks包下的接口)。

通過Condition對象的.await(*),可以將當前線程的線程狀態切換到Waiting狀態(如果是有參,則是Time Waiting狀態)。而.signal(),.signalAll()等方法則正好相反,恢複線程狀態為Runnable狀態。

ReentrantReadWriteLock

簡介

ReentrantLock和Synchronized功能類似,更加靈活,當然,也更加手動了。

大家都知道,只有涉及資源的競爭時,採用同步的必要。寫操作自然屬於資源的競爭,但是讀操作並不屬於資源的競爭行為。簡單說,就是寫操作最多只能一個線程(因為寫操作涉及數據改變,多個線程同時寫,會產生資源同步問題),而讀操作可以有多個(因為不涉及數據改變)。

所以在讀多寫少的場景下,ReentrantLock就比較浪費資源了。這就需要一種能夠區分讀寫操作的鎖,那就是ReentrantReadWriteLock。通過ReentrantReadWriteLock,可以獲得讀鎖與寫鎖。當寫鎖存在時,有且只能有一個線程持有鎖。當寫鎖不存在時,可以有多個線程持有讀鎖(寫鎖,必須等待讀鎖釋放完,才可以持有鎖)。

Lock及Condition應用


        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
        readLock.lock();
        readLock.unlock();

        readLock.newCondition();

        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
        writeLock.lock();
        writeLock.unlock();

        writeLock.newCondition();

與之前ReentrantLock應用的區別,就是需要通過lock.readLock()與lock.writeLock()來獲取讀鎖,寫鎖,再進行加鎖,釋放鎖的操作,以及Condition的獲取操作。

二,手寫ReentrantLock

獲取需求

終於上大餐了。

首先第一步操作,我們需要確定我們要做什麼。

我們要做一個鎖,這裏姑且命名為JarryReentrantLock。

這個鎖,需要具備以下特性:可重入鎖,悲觀鎖。

另外,為了更加規範,以後更好地融入到AQS中,該鎖需要實現Lock接口。

而Lock的方法簽名,在文章一開始,就已經寫了,這裏不再贅述。

當然,我們這裏只是一個demo,所以就不實現Condition了。另外tryLock(long,TimeUnit)也不再實現,因為實現了整體后,這個實現其實並沒有想象中那麼困難。

JarryReentrantLock實現原理

既然需要已經確定,並且API也確定了。

那麼第二步操作,就是簡單思考一下,如何實現。

類成員方面:

  1. 首先,我們需要一個owner屬性,來保存持有鎖的線程對象。

  2. 其次,由於是可重入鎖,所以我們需要一個count來保存重入次數。

  3. 最後,我們需要一個waiters屬性,來保存那些競爭鎖失敗后,還在等待(不死不休型)的線程對象。

類方法方面:

  • tryLock:嘗試獲取鎖,成功返回true,失敗返回false。首先是獲取鎖的行為,可以通過CAS操作實現,或者更簡單一些,通過Atomic包實現(其底層也還是CAS)。另外,由於是可重入鎖,所以在嘗試獲取鎖時,需要判斷嘗試獲取鎖的線程是否為當前鎖的持有者線程。
  • lock:嘗試獲取鎖,直到成功獲得鎖。看到這種不成功便成仁的精神,我第一個想法是循環調用tryLock。但是這實在太浪費資源了(畢竟長時間的忙循環是非常消耗CPU資源的)。所以就是手動通過LockSupport.park()將當前線程掛起,然後置入等待隊列waiters中,直到釋放鎖操作來調用。
  • tryUnlock:嘗試解鎖,成功返回true,失敗返回false。首先就是在釋放鎖前,需要判斷嘗試解鎖的線程與鎖的持有者是否為同一個線程(總不能線程A把線程B持有的鎖給釋放了吧)。其次,需要判斷可重入次數count是否為0,從而決定是否將鎖的持有owner設置為null。最後,就是為了避免在count=0時,其他線程同時進行加鎖操作,造成的count>0,owner=null的情況,所以count必須是Atomic,並此處必須採用CAS操作(這裡有些難理解,可以看代碼,有相關註釋)。
  • unlock:解鎖操作。這裏嘗試進行解鎖,如果解鎖成功,需要從等待隊列waiters中喚醒一個線程(喚醒后的線程,由於在循環中,所以會繼續進行競爭鎖操作。但是切記該線程不一定競爭鎖成功,因為可能有新來的線程,搶先一步。那麼該線程會重新進入隊列。所以,此時的JarryReentrantLock只支持不公平鎖)。

JarryReentrantLock實現

那麼接下來,就根據之前的信息,進行編碼吧。


    package tech.jarry.learning.netease;
    
    import java.util.concurrent.LinkedBlockingQueue;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.concurrent.atomic.AtomicReference;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.LockSupport;
    
    /**
     * @Description: 仿ReentrantLock,實現其基本功能及特性
     * @Author: jarry
     */
    public class JarryReentrantLock implements Lock {
    
        // 加鎖計數器
        private AtomicInteger count = new AtomicInteger(0);
        // 鎖持有者
        private AtomicReference<Thread> owner = new AtomicReference<>();
        // 等待池
        private LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();
    
    
        @Override
        public boolean tryLock() {
            // 判斷當前count是否為0
            int countValue = count.get();
            if (countValue != 0){
                // countValue不為0,意味着鎖被線程持有
                // 進而判斷鎖的持有者owner是否為當前線程
                if (Thread.currentThread() == owner.get()){
                    // 鎖的持有者為當前線程,那麼就重入加鎖
                    // 既然鎖已經被當前線程佔有,那麼就不用擔心count被其他線程修改,即不需要使用CAS
                    count.set(countValue+1);
                    // 執行重入鎖,表示當前線程獲得了鎖
                    return true;
                }else{
                    // 如果當前線程不是鎖的持有者,返回false(該方法是tryLock,即淺嘗輒止)
                    return false;
                }
            }else {
                // countValue為0,意味着當前鎖不被任何線程持有
                // 通過CAS操作將count修改為1
                if (count.compareAndSet(countValue,countValue+1)){
                    // count修改成功,意味着該線程獲得了鎖(只有一個CAS成功修改count,那麼這個CAS的線程就是鎖的持有者)
                    // 至於這裏為什麼不用擔心可見性,其實一開始我也比較擔心其發生類似doubleCheck中重排序造成的問題(tryUnlock是會設置null的)
                    // 看了下源碼,AtomicReference中的value是volatile的
                    owner.set(Thread.currentThread());
                    return true;
                } else {
                    // CAS操作失敗,表示當前線程沒有成功修改count,即獲取鎖失敗
                    return false;
                }
            }
        }
    
        @Override
        public void lock() {
            // lock()【不死不休型】就等於執行tryLock()失敗后,仍然不斷嘗試獲取鎖
            if (!tryLock()){
                // 嘗試獲取鎖失敗后,就只能進入等待隊列waiers,等待機會,繼續tryLock()
                waiters.offer(Thread.currentThread());
    
                // 通過自旋,不斷嘗試獲取鎖
                // 其實我一開始也不是很理解為什麼這樣寫,就可以確保每個執行lock()的線程就在一直競爭鎖。其實,想一想執行lock()的線程都有這個循環。
                // 每次unlock,都會將等待隊列的頭部喚醒(unpark),那麼處在等待隊列頭部的線程就會繼續嘗試獲取鎖,等待隊列的其它線程仍然,繼續阻塞(park)
                // 這也是為什麼需要在循環體中執行一個檢測當前線程是否為等待隊列頭元素等一系列操作。
                // 另外,還有就是:處於等待狀態的線程可能收到錯誤警報和偽喚醒,如果不在循環中檢測等待條件,程序就會在沒有滿足結束條件的情況下退出。反正最後無論那個分支,都return,結束方法了。
                // 即使沒有偽喚醒問題,while還是需要的,因為線程需要二次嘗試獲得鎖
                while (true){
                    // 獲取等待隊列waiters的頭元素(peek表示獲取頭元素,但不刪除。poll表示獲取頭元素,並刪除其在隊列中的位置)
                    Thread head = waiters.peek();
                    // 如果當前線程就是等待隊列中的頭元素head,說明當前等待隊列就剛剛加入的元素。
                    if (head == Thread.currentThread()){
                        // 嘗試再次獲得鎖
                        if (!tryLock()){
                            // 再次嘗試獲取鎖失敗,即將該線程(即當前線程)掛起,
                            LockSupport.park();
                        } else {
                            // 獲取鎖成功,即將該線程(等待隊列的頭元素)從等待隊列waiters中移除
                            waiters.poll();
                            return;
                        }
                    } else {
                        // 如果等待隊列的頭元素head,不是當前線程,表示等待隊列在當前線程加入前,就還有別的線程在等待
                        LockSupport.park();
                    }
                }
            }
        }
    
        private boolean tryUnlock() {
            // 首先確定當前線程是否為鎖持有者
            if (Thread.currentThread() != owner.get()){
                // 如果當前線程不是鎖的持有者,就拋出一個異常
                throw new IllegalMonitorStateException();
            } else {
                // 如果當前線程是鎖的持有者,就先count-1
                // 另外,同一時間執行解鎖的只可能是鎖的持有者線程,故不用擔心原子性問題(原子性問題只有在多線程情況下討論,才有意義)
                int countValue = count.get();
                int countNextValue = countValue - 1;
                count.compareAndSet(countValue,countNextValue);
                if (countNextValue == 0){
                    // 如果當前count為0,意味着鎖的持有者已經完全解鎖成功,故應當失去鎖的持有(即設置owner為null)
                    // 其實我一開始挺糾結的,這裏為什麼需要使用CAS操作呢。反正只有當前線程才可以走到程序這裏。
                    // 首先,為什麼使用CAS。由於count已經設置為0,其它線程已經可以修改count,修改owner了。所以不用CAS就可能將owner=otherThread設置為owner=null了,最終的結果就是徹底卡死
                    //TODO_FINISHED 但是unlock()中的unpark未執行,根本就不會有其它線程啊。囧
                    // 這裏代碼還是為了體現源碼的一些特性。實際源碼是將這些所的特性,抽象到了更高的層次,形成一個AQS。
                    // 雖然tryUnlock是由實現子類實現,但countNextValue是來自countValue(而放在JarryReadWriteLock中就是writeCount),在AQS源碼中,則是通過state實現
    
                    // 其次,有沒有ABA問題。由於ABA需要將CAS的expect值修改為currentThread,而當前線程只能單線程執行,所以不會。
                    // 最後,這裏owner設置為null的操作到底需不需要。實際源碼可能是需要的,但是這裏貌似真的不需要。
                    owner.compareAndSet(Thread.currentThread(),null);
                    // 解鎖成功
                    return true;
                } else {
                    // count不為0,解鎖尚未完全完成
                    return false;
                }
            }
        }
    
        @Override
        public void unlock() {
            if (tryUnlock()){
                // 如果當前線程成功tryUnlock,就表示當前鎖被空置出來了。那就需要從備胎中,啊呸,從waiters中“放“出來一個
                Thread head = waiters.peek();
                // 這裏需要做一個簡單的判斷,防止waiters為空時,拋出異常
                if (head != null){
                    LockSupport.unpark(head);
                }
            }
        }
    
    
        // 非核心功能就不實現了,起碼現在不實現了。
    
        @Override
        public void lockInterruptibly() throws InterruptedException {
    
        }
    
        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return false;
        }
    
        @Override
        public Condition newCondition() {
            return null;
        }
    }

這裏就不進行一些解釋了。因為需要的解釋,在註釋中都寫的很明確了,包括我踩的一些坑。

如果依舊有一些看不懂的地方,或者錯誤的地方,歡迎@我,或者私信我。

三,手寫ReentrantReadWriteLock

獲取需求

與ReentrantLock一樣,首先第一步操作,我們需要確定我們要做什麼。

我們要做一個鎖,這裏姑且命名為JarryReadWriteLock。

這個鎖,需要具備以下特性:讀寫鎖,可重入鎖,悲觀鎖。

一方面了為了更好理解(第一版本,重在理解基礎,不是嘛),另一方面也是為了更好地復用前面ReentrantLock的代碼(畢竟ReentrantLock其實就是讀寫鎖的寫鎖,不是嘛),這裏的JarryReadWriteLock的API不再與官方的ReentrantReadWriteLock相同,而是做了小小調整。直接調用相關讀鎖的加解鎖API,已經相關寫鎖的加解鎖API。具體看代碼部分。

JarryReadWriteLock實現原理

既然需要已經確定,並且API也確定了。

那麼第二步操作,就是簡單思考一下,如何實現。

類成員方面:

  1. 首先,我們需要一個owner屬性,來保存持有寫鎖的線程對象。

  2. 其次,由於寫鎖是可重入鎖,所以我們需要一個readCount來保存重入次數。

  3. 然後,由於讀鎖是可以有多個線程持有的,所以我們需要一個writeCount來保存讀鎖持有線程數。

  4. 最後,我們需要一個waiters屬性,來保存那些競爭鎖失敗后,還在等待(不死不休型)的線程對象。

自定義數據結構:

到這這裏,就不禁會有一個疑問。如何判斷嘗試獲取鎖的線程想要獲得的鎖是什麼類型的鎖。在API調用階段,我們可以根據API判斷。但是放入等待隊列后,我們如何判斷呢?如果還是如之前那樣,等待隊列只是保存競爭鎖的線程對象,是完全不夠的。

所以我們需要新建一個WaitNode的Class,用來保存等待隊列中線程對象及相關必要信息。所以,WaitNode會有如下屬性:

  • Thread thread:標識該等待者的線程。
  • int type:標識該線程對象希望競爭的鎖的類型。0表示寫鎖(獨佔鎖),1表示讀鎖(共享鎖)。
  • int arg:擴展參數。其實在手寫的簡易版,看不出來價值。但是實際AQS中的Node就是類似設計。不過AQS中,並不是採用queue保存Node,而是通過一個鏈表的方式保存Node。

類方法方面:

  • 獨佔鎖:
    • tryLock:與JarryReentrantLock類似,不過增加了兩點。一方面需要考量共享鎖是否被佔用。另一方面需要引入acquire參數(目前是固定值),呼應WaitNode的arg。
    • lock:與JarryReentrantLock類似,不過需要手動設置arg。
    • tryUnlock:與JarryReentrantLock類似,同樣需要引入release參數(目前是固定值),呼應WaitNode的arg。
    • unlock:與JarryReentrantLock類似,不過需要手動設置arg。
  • 共享鎖:
    • tryLockShared:嘗試獲取共享鎖,成功返回true,失敗返回false。其實和獨佔鎖的tryLock類似,只不過需要額外考慮獨佔鎖是否已經存在。另外為了實現鎖降級,如果獨佔鎖存在,需要判斷獨佔鎖的持有者與當前嘗試獲得共享鎖的線程是否一致。
    • lockShared:獲取共享鎖,直到成功。由於已經有了WaitNode.type,用於判斷鎖類型,所以共享鎖與獨佔鎖使用的是同一隊列。同樣的,這裏需要手動設置arg。其它方面與獨佔鎖的lock操作基本一致。
    • tryUnlockShared:嘗試釋放鎖,成功返回true,失敗返回false。類似於tryUnlock,只不過增加了release參數(固定值),呼應WaitNode的arg。
    • unlockShared:釋放鎖。類似unlock,不過需要手動設置arg。

JarryReentrantLock實現


    package tech.jarry.learning.netease;
    
    import java.util.concurrent.LinkedBlockingQueue;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.concurrent.atomic.AtomicReference;
    import java.util.concurrent.locks.LockSupport;
    
    /**
     * @Description:
     * @Author: jarry
     */
    public class JarryReadWriteLock {
    
        // 用於讀鎖(共享鎖)的鎖計數器   這裏真的有必要volatile嘛(Atomic中的value時volatile的),再看看後續代碼
        // 這裏確實不需要volatile,至於源碼,更過分,源碼是通過一個變量state的位運算實現readCount與writeCount
        volatile AtomicInteger readCount = new AtomicInteger(0);
        // 用於寫鎖(獨佔鎖)的鎖計數器   這裏之所以不用volatile是因為獨佔鎖,只有一個線程在改變writeCount(即使有緩存,也還是這個線程,所以不會因為緩存問題,導致問題)
        AtomicInteger writeCount = new AtomicInteger(0);
        // 用於保存鎖的持有者(這裏專指寫鎖(獨佔鎖)的鎖持有者)
        AtomicReference<Thread> owner = new AtomicReference<>();
        // 用於保存期望獲得鎖的線程(為了區分線程希望獲得的鎖的類型,這裏新建一個新的數據類型(通過內部類實現))
        public volatile LinkedBlockingQueue<WaitNode> waiters = new LinkedBlockingQueue<>();
    
        // 內部類實現等待隊列中的自定義數據類型
        class WaitNode{
            // 表示該等待者的線程
            Thread thread = null;
            // 表示希望爭取的鎖的類型。0表示寫鎖(獨佔鎖),1表示讀鎖(共享鎖)
            int type = 0;
            // 參數,acquire,狀態相關,再看看
            int arg = 0;
    
            public WaitNode(Thread thread, int type, int arg) {
                this.type = type;
                this.thread = thread;
                this.arg = arg;
            }
        }
    
        /**
         * 嘗試獲取獨佔鎖(針對獨佔鎖)
         * @param acquires 用於加鎖次數。一般傳入waitNode.arg(本代碼中就是1。為什麼不用一個常量1,就不知道了?)(可以更好的對接AQS)
         * @return
         */
        public boolean tryLock(int acquires){
            //TODO_FINISHED 這裏readCount的判斷,與修改writeCount的操作可以被割裂,並不是原子性的。不就有可能出現readCount與writeCount的值同時大於零的情況。
            // 該示例代碼,確實存在該問題,但實際源碼,writeCount與readCount是通過同一變量state實現的,所以可以很好地通過CAS確保原子性
    
            // readCount表示讀鎖(共享鎖)的上鎖次數
            if (readCount.get() == 0){
                // readCount的值為0,表示讀鎖(共享鎖)空置,所以當前線程是有可能獲得寫鎖(獨佔鎖)。
                // 接下來判斷寫鎖(獨佔鎖)是否被佔用
                int writeCountValue = writeCount.get();
                if (writeCountValue == 0){
                    // 寫鎖(獨佔鎖)的鎖次數為0,表示寫鎖(獨佔鎖)並沒未被任何線程持有
                    if (writeCount.compareAndSet(writeCountValue,writeCountValue+acquires)){
                        // 修改writeCount,來獲得鎖。該機制與ReentrantLock相同
                        // 設置獨享鎖的持有者owner
                        owner.set(Thread.currentThread());
                        // 至此,表示當前線程搶鎖成功
                        return true;
                    }
                } else {
                    // 寫鎖(獨佔鎖)的鎖次數不為0,表示寫鎖(獨佔鎖)已經被某線程持有
                    if (Thread.currentThread() == owner.get()){
                        // 如果持有鎖的線程為當前線程,那就進行鎖的重入操作
                        writeCount.set(writeCountValue+acquires);
                        // 重入鎖,表示當前線程是持有鎖的
                        return true;
                    }
                    // 讀鎖未被佔用,但寫鎖被佔用,且佔據寫鎖的線程不是當前線程
                }
            }
            // 讀鎖被佔據
            // 其它情況(1.讀鎖被佔據,2讀鎖未被佔用,但寫鎖被佔用,且佔據寫鎖的線程不是當前線程),都返回false
            return false;
        }
    
        /**
         * 獲取獨佔鎖(針對獨佔鎖)
         */
        public void lock(){
            // 設定waitNote中arg參數
            int arg = 1;
            // 嘗試獲取獨佔鎖。成功便退出方法,失敗,則進入“不死不休”邏輯
            if (!tryLock(arg)){
                // 需要將當前保存至等待隊列,在這之前,需要封裝當前線程為waitNote
                WaitNode waitNode = new WaitNode(Thread.currentThread(), 0, arg);
                // 將封裝好的waitNode放入等待隊列waiters中(offer方法會在隊列滿時,直接返回false。put則是阻塞。add則是拋出異常)
                waiters.offer(waitNode);
    
                // 如ReentrantLock一般,開始循環嘗試拿鎖
                while (true){
                    // 獲取隊列頭部元素
                    WaitNode headNote = waiters.peek();
                    // 如果等待隊列頭部元素headNote不為null(有可能是null嘛?),並且就是當前線程,那就嘗試獲取鎖
                    if (headNote !=null && headNote.thread == Thread.currentThread()){
                        // 如果再次嘗試獲取鎖失敗,那就只能掛起了
                        if (!tryLock(headNote.arg)){
                            LockSupport.park();
                        } else {
                            // 再次嘗試獲取鎖成功,那就將隊列頭部元素,踢出等待隊列waiters
                            waiters.poll();
                            return;
                        }
                    }else {
                        // 如果headNote不是當前線程的封裝,就直接掛起(這裏就沒處理headNote==null的情況)
                        LockSupport.park();
                    }
                }
            }
        }
    
        /**
         * 嘗試解鎖(針對獨佔鎖)
         * @param releases 用於設定解鎖次數。一般傳入waitNode.arg
         * @return
         */
        public boolean tryUnlock(int releases){
            // 首先判斷鎖的持有者是否為當前線程
            if (owner.get() != Thread.currentThread()){
                // 鎖的持有者不是當前線程(即使鎖的持有者為null,鎖的持有者是null,還解鎖,仍然是拋出異常)
                throw new IllegalMonitorStateException();
            }
            // 鎖的持有者就是當前線程
            // 首先按照releases進行解鎖(經過一番思考後,這裏不會出現類似DoubleCheck中的問題(Atomic中的value是volatile的),所以這個值同時只會有一個線程對其操作)
            int writeCountValue = writeCount.get();
            // 為writeCount設置新值
            writeCount.set(writeCountValue-releases);
            // 根據writeCount的新值,判斷鎖的持有者是否發生變化
            if (writeCount.get() == 0){
                // writeCount的值為0,表示當前線程已經完全解鎖,所以修改鎖的持有者為null
                owner.set(null);
                // 而這表示完全解鎖成功
                return true;
            } else {
                // writeCount的值不為0,表示當前線程尚未完全解鎖,故鎖的持有者未發生變化。即嘗試解鎖失敗
                return false;
            }
        }
    
        /**
         * 解鎖(針對獨佔鎖)
         */
        public void unlock(){
            // 設定tryUnlock的參數releases
            int arg = 1;
            // 先嘗試解鎖
            if (tryUnlock(arg)){
                // 獲得等待隊列的頭部元素
                WaitNode head = waiters.peek();
                // 檢測一下頭部元素head是否null(也許等待隊列根本就沒有元素)
                if (head == null){
                    // 如果頭部元素head為null,說明隊列為null,直接return
                    return;
                }
                // 解鎖成功,就要把等待隊列中的頭部元素喚醒(unpark)
                // 這裡有一點注意,即使隊列的頭元素head被喚醒了,也不一定就是這個頭元素head獲得鎖(詳見tryLock,新來的線程可能獲得鎖)
                // 如果這個頭元素無法獲得鎖,就會park(while循環嘛)。並且一次park,可以多次unpark(已實踐)
                LockSupport.unpark(head.thread);
            }
        }
    
        /**
         * 嘗試獲取共享鎖(針對共享鎖)
         * @param acquires
         * @return
         */
        public boolean tryLockShared(int acquires){
            // 判斷寫鎖(獨佔鎖)是否被別的線程持有(這個條件意味着:同一個線程可以同時持有讀鎖與寫鎖)
            // 該方法是為了進行  鎖降級******
            if (writeCount.get() == 0 || owner.get() == Thread.currentThread()){
                // 如果寫鎖(獨佔鎖)沒有別的被線程持有,就可以繼續嘗試獲取讀鎖(共享鎖)
                // 通過循環實現自旋,從而實現加鎖(避免加鎖失敗)
                while(true){
                    // 由於讀鎖(共享鎖)是共享的,不存在獨佔行為,故直接在writeCount增加當前線程加鎖行為的次數acquires
                    int writeCountValue = writeCount.get();
                    // 通過CAS進行共享鎖的次數的增加
                    if (writeCount.compareAndSet(writeCountValue, writeCountValue+acquires)){
                        break;
                    }
                }
            }
            // 寫鎖已經被別的線程持有,共享鎖獲取失敗
            return false;
        }
    
        /**
         * 獲取共享鎖(針對共享鎖)
         */
        public void lockShared(){
            // 設定waitNote中arg參數
            int arg = 1;
            // 判斷是否獲取共享鎖成功
            if (!tryLockShared(arg)){
                // 如果獲取共享鎖失敗,就進入等待隊列
                // 與獲取同步鎖操作一樣的,需要先對當前線程進行WaitNote的封裝
                WaitNode waitNode = new WaitNode(Thread.currentThread(),1,arg);
                // 將waitNote置入waiters(offer方法會在隊列滿時,直接返回false。put則是阻塞。add則是拋出異常)
                waiters.offer(waitNode);
    
                // 使用循環。一方面避免偽喚醒,另一方面便於二次嘗試獲取鎖
                while (true){
                    // 獲取等待隊列waiters的頭元素head
                    WaitNode head = waiters.peek();
                    // 校驗head是否為null,並判斷等待隊列的頭元素head是否為當前線程的封裝(也許head時當前線程的封裝,但並不意味着head就是剛剛放入waiters的元素)
                    if (head != null && head.thread == Thread.currentThread()){
                        // 如果校驗通過,並且等待隊列的頭元素head為當前線程的封裝,就再次嘗試獲取鎖
                        if (tryLockShared(head.arg)){
                            // 獲取共享鎖成功,就從當前隊列中移除head元素(poll()方法移除隊列頭部元素)
                            waiters.poll();
    
                            // 在此處就是與獨佔鎖不同的地方了,獨佔鎖意味着只可能有一個線程獲得鎖,而共享鎖是可以有多個線程獲得的
                            // 獲得等待隊列的新頭元素newHead
                            WaitNode newHead = waiters.peek();
                            // 校驗該元素是否為null,並判斷它的鎖類型是否為共享鎖
                            if (newHead != null && newHead.type == 1){
                                // 如果等待隊列的新頭元素是爭取共享鎖的,那麼就喚醒它(這是一個類似迭代的過程,剛喚醒的線程會會做出同樣的舉動)
                                //TODO_FINISHED 這裡有一點,我有些疑惑,那麼如果等待隊列是這樣的{共享鎖,共享鎖,獨佔鎖,共享鎖,共享鎖},共享鎖們被一個獨佔鎖隔開了。是不是就不能喚醒後面的共享鎖了。再看看後面的代碼
                                // 這個實際源碼,並不是這樣的。老師表示現有代碼是這樣的,不用理解那麼深入,後續有機會看看源碼
                                LockSupport.unpark(newHead.thread);
                            }
                        } else {
                            // 如果再次獲取共享鎖失敗,就掛起
                            LockSupport.park();
                        }
                    } else {
                        // 如果校驗未通過,或等待隊列的頭元素head不是當前線程的封裝,就掛起當前線程
                        LockSupport.park();
                    }
                }
            }
        }
    
        /**
         * 嘗試解鎖(針對共享鎖)
         * @param releases
         * @return
         */
        public boolean tryUnlockShared(int releases){
            // 通過CAS操作,減少共享鎖的鎖次數,即readCount的值(由於是共享鎖,所以是可能多個線程同時減少該值的,故採用CAS)
            while (true){
                // 獲取讀鎖(共享鎖)的值
                int readCountValue = readCount.get();
                int readCountNext = readCountValue - releases;
                // 只有成功修改值,才可以跳出
                if (readCount.compareAndSet(readCountValue,readCountNext)){
                    // 用於表明共享鎖完全解鎖成功
                    return readCountNext == 0;
                }
            }
            // 由於讀鎖沒有owner,所以不用進行有關owner的操作
        }
    
        /**
         * 解鎖(針對共享鎖)
         */
        public boolean unlockShared(){
            // 設定tryUnlockShared的參數releases
            int arg = 1;
            // 判斷是否嘗試解鎖成功
            if (tryUnlockShared(arg)){
                // 如果嘗試解鎖成功,就需要喚醒等待隊列的頭元素head的線程
                WaitNode head = waiters.peek();
                // 校驗head是否為null,畢竟可能等待隊列為null
                if (head != null){
                    // 喚醒等待隊列的頭元素head的線程
                    LockSupport.unpark(head.thread);
                }
                //TODO_FINISHED 嘗試共享鎖解鎖成功后,就應當返回true(雖然有些不大理解作用)
                // 用於對應源碼
                return true;
            }
            //TODO_FINISHED 嘗試共享鎖解鎖失敗后,就應當返回false(雖然有些不大理解作用)
            // 用於對應源碼
            return false;
        }
    }

這裏同樣不進行相關解釋了。因為需要的解釋,在註釋中都寫的很明確了,包括我踩的一些坑。

如果依舊有一些看不懂的地方,或者錯誤的地方,歡迎@我,或者私信我。

四,總結

技術

  • CAS:通過CAS實現鎖持有數量等的原子性操作,從而完成鎖的競爭操作。
  • Atomic:為了簡化操作(避免自己獲取Unsafe,offset等),通過Atomic實現CAS 操作。
  • volatile:為了避免多線程下的可見性問題,採用了volatile的no cache特性。
  • transient:可以避免對應變量序列化,源碼中有採用。不過考慮后,並沒有使用。
  • while:一方面通過while避免偽喚醒問題,另一方面,通過while推動流程(這個需要看代碼)。
  • LinkedBlockingQueue:實現線程等待隊列。實際的AQS是通過Node構成鏈表結構的。
  • LockSupport:通過LockSupport實現線程的掛起,喚醒等操作。
  • IllegalMonitorStateException:就是一個異常類型,仿Synchronized的,起碼看起來更明確,還不用自己實現新的Exception類型。

方案

其實,這兩個demo有兩個重要的方面。一方面是可以親自感受,一個鎖是怎麼實現的,它的方案是怎樣的。另一方面就是去思量,其中有關原子性,以及可見性的思量與設計。

你們可以嘗試改動一些東西,然後去考慮,這樣改動后,是否存在線程安全問題。這樣的考慮對自己在線程安全方面的提升是巨大的。反正我當時那一周,就不斷的改來改去。甚至有些改動,根本調試不出來問題,然後諮詢了別人,才知道其中的一些坑。當然也有一些改動是可以的。

後言

如果有問題,可以@我,或者私信我。

如果覺得這篇文章不錯的話,請點擊推薦。這對我,以及那些需要的人,很重要。

謝謝。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

Orleans 3.0 為我們帶來了什麼

 

原文:https://devblogs.microsoft.com/dotnet/orleans-3-0/

作者:Reuben BondOrleans首席軟件開發工程師

翻譯:艾心

這是一篇來自Orleans團隊的客座文章,Orleans是一個使用.NET創建分佈式應用的跨平台框架。獲取更多信息,請查看https://github.com/dotnet/orleans。

我們激動的宣布Orleans3.0的發布。自Orleans2.0以來,加入了大量的改進與修復,以及一些新特性。這些變化是由許多人在生產環境的大量場景中運行基於Orleans應用程序的經驗,以及全球Orleans社區的智慧和熱情推動的,他們致力於使代碼庫更好、更快、更靈活。非常感謝所有以各種方式為這個版本做出貢獻的人。

自Orleans 2.0以來的關鍵變化:

Orleans 2.0發佈於18個多月前,從那時起Orleans便取得了巨大的進步。以下是自Orleans 2.0以來的重大變化:

  • 分佈式ACID事務-多個Grains加入到一個事務中,不管他們的狀態存儲在哪裡
  • 一個新的調度器,在某些情況下,僅它就可以將性能提升30%以上
  • 一種基於Roslyn代碼分析的新的代碼生成器
  • 重寫集群成員以提升恢復速度
  • 聯合(Co-hosting)支持

還有很多其他的提升以及修復。

自從致力於開發Orleans2.0以來,團隊就建立了一套實現或者繼承某些功能的良性循環,包括通用主機、命名選項,在準備將這些功能好成為.NETCore的一部分之前與.NET團隊密切合作、提供反饋和改進“upstream”,在以後的版本中會切換到.NET版本附帶的最終實現。在開發Orleans 3.0期間,這個循環繼續着,在最終發布為.NET Core 3.0的一部分之前,Orleans 3.0.0-beta1使用了Bedrock代碼。類似的,TCP套接字連接對TLS的支持是作為Orleans 3.0的一部分實現的,並計劃成為.NET Core未來版本的一部分。我們把這種持續的合作視為是我們對更大的.NET生態系統的貢獻,這是真正的開源精神。

使用ASP.NET Bedrock替換網絡層

一段時間以來,社區和內部合作夥伴一直要求支持與TLS的安全通信。在3.0版本中,我們引入了TLS支持,可以通過Microsoft.Orleans.Connections.Security包獲取。有關更多信息,請查看TransportLayerSecurity範例。實現TLS支持之所以是一個重大任務要歸因於上一個版本中Orleans網絡層的實現方式:它並不容易適應使用SslStream的方式,而SslStream又是實現TLS最常用的方法。在TLS的推動下,我們着手重寫Orleans的網絡層。

Orleans 3.0使用了一個來自ASP.NET團隊倡議的基於Bedrock項目構建的網絡層替換了自己的整個網絡層,Bedrock旨在幫助開發者構建快速的、健壯的網絡客戶端和服務器。

ASP.NET團隊和Orleans團隊一同合作設計了同時支持網絡客戶端和服務端的抽象,這些抽象與傳輸無關,並且可以通過中間件實現定製化。這些抽象允許我們通過配置修改網絡,而不用修改內部的、特定於Orleans的網絡代碼。Orleans的TLS支持是作為Bedrock中間件實現的,我們的目的是使之通用,以便與.NET生態圈的其他人共享。

儘管這項工作是的動力是啟用TLS支持,但是在夜間負載測試中,我們看到了平均吞吐量提升了大約30%。

網絡層重寫還包括藉助使用MemoryPool<byte>替換我們的自定義緩存池,在進行這項修改時,序列化更多的使用到了Span<T>。有一些代碼路徑之前是依靠調用BlockingCollection<T>的專有線程進行阻塞,現在使用Channel<T>來異步傳輸消息。這將導致更少的專有線程佔用,同時將工作移動到了.NET線程池。

Orleans的核心連接協議自發布以來一直都是固定的。在Orleans3.0中,我們已經增加了通過協議協商(negotiation)逐步更新網絡層的支持。Orleans 3.0中添加的協議協商支持未來的功能增強,如定製核心序列化器,同時向後保持兼容性。新的網絡協議的一個優點是支持全雙工Silo到Silo的連接,而不是以前在Silo之間建立的單工連接對。協議版本可以通過ConnectionOptions.ProtocolVersion進行配置。

通過通用主機進行聯合託管

Orleans與其他框架共同進行聯合託管,如ASP.NETCore,得益於.NET通用主機,相同的進程中(使用聯合託管)現在要比以前容易多了。

下面是一個使用UseOrleans將Orleans和ASP.NETCore一起添加到主機的例子:

 1 var host = new HostBuilder()
 2   .ConfigureWebHostDefaults(webBuilder =>
 3   {
 4     // Configure ASP.NET Core
 5     webBuilder.UseStartup<Startup>();
 6   })
 7   .UseOrleans(siloBuilder =>
 8   {
 9     // Configure Orleans
10     siloBuilder.UseLocalHostClustering();
11   })
12   .ConfigureLogging(logging =>
13   {
14     /* Configure cross-cutting concerns such as logging */
15   })
16   .ConfigureServices(services =>
17   {
18     /* Configure shared services */
19   })
20   .UseConsoleLifetime()
21   .Build();
22 
23 // Start the host and wait for it to stop.
24 await host.RunAsync();

使用通過主機構建器,Orleans將與其他託管程序共享同一個服務提供者。這使得這些服務可以訪問Orleans。例如,一個開發者可以注入IClusterClient或者IGrainFactory到ASP.NETCore MVC Controller中,然後從MVC應用中直接調用Grains。

這個功能可以簡化你的部署拓撲或者向現有程序中額外添加功能。一些團隊內部使用聯合託管,通過ASP.NET Core健康檢查將Kubernetes活躍性和就緒性探針添加到其Orleans Silo中。

可靠性提高

得益於擴展了Gossip,集群現在可以更快的從失敗中恢復。在以前的Orleans版本中,Silo會向其他Silo發送成員Gossip信息,指示他們更新成員信息。現在Gossip消息包括集群成員的版本化、不可變快照。這樣可以縮短Silo加入或者離開集群的收斂時間(例如在更新、擴展或者失敗后),並減輕共享成員存儲上的爭用,從而加快集群轉換的速度。故障檢測也得到了改進,利用更多的診斷信息和改進功能以確保更快、更準確的檢測。故障檢測涉及集群中的Silo,他們相互監控,每個Silo會定期向其他Silo的子集發送健康探測。Silo和客戶端現在還主動與已聲明為已失效的Silo的連接斷開,它們將拒絕與此類Silo的連接。

現在,消息錯誤得到了更一致的處理,從而將錯誤提示信息傳播回調用者。這有助於開發者更快地發現錯誤。例如,當消息無法被完全序列化或者反序列化時,詳細的異常信息將會被返回到原始調用方。

可擴展性增強

現在,Streams可以有自定義的數據適配器,從而允許他們以任何格式提取數據。這使得開發人員更好的控制Streamitems在存儲中的表示方式。他還使Stream提供者可以控制如何寫入數據,從而允許Streams與老的系統和Orleans服務集成。

Grain擴展允許通過自己的通信接口附件新的組件,從而在運行時向Grain添加其他行為。例如,Orleans事務使用Grain擴展對用戶透明的向Grain中添加事務生命周期方法,如“準備”、“提交”和“中止”。Grain擴展現在也可用於Grain服務和系統目標。

現在,自定義事務狀態可以聲明其在事務中能夠扮演的角色。例如,將事務生命周期事件寫入服務總線隊列的事務狀態實現不能滿足事務管理器的職責,因為它(該事務狀態的職責)是只寫的。

由於預定義的放置策略現在可以公開訪問,因此在配置期間可以替換任何放置控制器。

共同努力

既然Orleans 3.0已經發布,我們也就會將注意力轉向未來的版本-我們有一些令人興奮的計劃!快來加入我們在GitHub和Gitter上的社區,幫助我們實現這些計劃。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

[ch02-02] 非線性反向傳播

系列博客,原文在筆者所維護的github上:,
點擊star加星不要吝嗇,星越多筆者越努力。

2.2 非線性反向傳播

2.2.1 提出問題

在上面的線性例子中,我們可以發現,誤差一次性地傳遞給了初始值w和b,即,只經過一步,直接修改w和b的值,就能做到誤差校正。因為從它的計算圖看,無論中間計算過程有多麼複雜,它都是線性的,所以可以一次傳到底。缺點是這種線性的組合最多只能解決線性問題,不能解決更複雜的問題。這個我們在神經網絡基本原理中已經闡述過了,需要有激活函數連接兩個線性單元。

下面我們看一個非線性的例子,如圖2-8所示。

圖2-8 非線性的反向傳播

其中\(1<x<=10,0<y<2.15\)。假設有5個人分別代表x、a、b、c、y:

正向過程

  1. 第1個人,輸入層,隨機輸入第一個x值,x取值範圍(1,10],假設第一個數是2
  2. 第2個人,第一層網絡計算,接收第1個人傳入x的值,計算:\(a=x^2\)
  3. 第3個人,第二層網絡計算,接收第2個人傳入a的值,計算b:\(b=\ln (a)\)
  4. 第4個人,第三層網絡計算,接收第3個人傳入b的值,計算c:\(c=\sqrt{b}\)
  5. 第5個人,輸出層,接收第4個人傳入c的值

反向過程

  1. 第5個人,計算y與c的差值:\(\Delta c = c – y\),傳回給第4個人
  2. 第4個人,接收第5個人傳回\(\Delta c,計算\Delta b:\Delta b = \Delta c \cdot 2\sqrt{b}\)
  3. 第3個人,接收第4個人傳回\(\Delta b,計算\Delta a:\Delta a = \Delta b \cdot a\)
  4. 第2個人,接收第3個人傳回\(\Delta a,計算\Delta x:\Delta x = \Delta a / 2x\)
  5. 第1個人,接收第2個人傳回\(\Delta x,更新x:x = x – \Delta x\),回到第1步

提出問題:假設我們想最後得到c=2.13的值,x應該是多少?(誤差小於0.001即可)

2.2.2 數學解析解

\[c=\sqrt{b}=\sqrt{\ln(a)}=\sqrt{\ln(x^2)}=2.13\]
\[x = 9.6653\]

2.2.3 梯度迭代解

\[ \frac{da}{dx}=\frac{d(x^2)}{dx}=2x=\frac{\Delta a}{\Delta x} \tag{1} \]
\[ \frac{db}{da} =\frac{d(\ln{a})}{da} =\frac{1}{a} = \frac{\Delta b}{\Delta a} \tag{2} \]
\[ \frac{dc}{db}=\frac{d(\sqrt{b})}{db}=\frac{1}{2\sqrt{b}}=\frac{\Delta c}{\Delta b} \tag{3} \]
因此得到如下一組公式,可以把最後一層\(\Delta c\)的誤差一直反向傳播給最前面的\(\Delta x\),從而更新x值:
\[ \Delta c = c – y \tag{4} \]
\[ \Delta b = \Delta c \cdot 2\sqrt{b} \tag{根據式3} \]
\[ \Delta a = \Delta b \cdot a \tag{根據式2} \]
\[ \Delta x = \Delta a / 2x \tag{根據式1} \]

我們給定初始值\(x=2,\Delta x=0\),依次計算結果如表2-2。

表2-2 正向與反向的迭代計算

方向 公式 迭代1 迭代2 迭代3 迭代4 迭代5
正向 \(x=x-\Delta x\) 2 4.243 7.344 9.295 9.665
正向 \(a=x^2\) 4 18.005 53.934 86.404 93.233
正向 \(b=\ln(a)\) 1.386 2.891 3.988 4.459 4.535
正向 \(c=\sqrt{b}\) 1.177 1.700 1.997 2.112 2.129
標籤值y 2.13 2.13 2.13 2.13 2.13
反向 \(\Delta c = c – y\) -0.953 -0.430 -0.133 -0.018
反向 \(\Delta b = \Delta c \cdot 2\sqrt{b}\) -2.243 -1.462 -0.531 -0.078
反向 \(\Delta a = \Delta b \cdot a\) -8.973 -26.317 -28.662 -6.698
反向 \(\Delta x = \Delta a / 2x\) -2.243 -3.101 -1.951 -0.360

表2-2,先看“迭代-1”列,從上到下是一個完整的正向+反向的過程,最後一行是-2.243,回到“迭代-2”列的第一行,2-(-2.243)=4.243,然後繼續向下。到第5輪時,正向計算得到的c=2.129,非常接近2.13了,迭代結束。

運行示例代碼的話,可以得到如下結果:

how to play: 1) input x, 2) calculate c, 3) input target number but not faraway from c
input x as initial number(1.2,10), you can try 1.3:
2
c=1.177410
input y as target number(0.5,2), you can try 1.8:
2.13
forward...
x=2.000000,a=4.000000,b=1.386294,c=1.177410
backward...
delta_c=-0.952590, delta_b=-2.243178, delta_a=-8.972712, delta_x=-2.243178
......
forward...
x=9.655706,a=93.232666,b=4.535098,c=2.129577
backward...
done!

為節省篇幅只列出了第一步和最後一步(第5步)的結果,第一步時c=1.177410,最後一步時c=2.129577,停止迭代。

代碼位置

ch02, Level2

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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