日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

非阻塞同步算法与CAS(Compare and Swap)无锁算法

發(fā)布時間:2024/4/13 编程问答 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 非阻塞同步算法与CAS(Compare and Swap)无锁算法 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

鎖(lock)的代價

鎖是用來做并發(fā)最簡單的方式,當然其代價也是最高的。內(nèi)核態(tài)的鎖的時候需要操作系統(tǒng)進行一次上下文切換,加鎖、釋放鎖會導致比較多的上下文切換和調(diào)度延時,等待鎖的線程會被掛起直至鎖釋放。在上下文切換的時候,cpu之前緩存的指令和數(shù)據(jù)都將失效,對性能有很大的損失。操作系統(tǒng)對多線程的鎖進行判斷就像兩姐妹在為一個玩具在爭吵,然后操作系統(tǒng)就是能決定他們誰能拿到玩具的父母,這是很慢的。用戶態(tài)的鎖雖然避免了這些問題,但是其實它們只是在沒有真實的競爭時才有效。

Java在JDK1.5之前都是靠synchronized關鍵字保證同步的,這種通過使用一致的鎖定協(xié)議來協(xié)調(diào)對共享狀態(tài)的訪問,可以確保無論哪個線程持有守護變量的鎖,都采用獨占的方式來訪問這些變量,如果出現(xiàn)多個線程同時訪問鎖,那第一些線線程將被掛起,當線程恢復執(zhí)行時,必須等待其它線程執(zhí)行完他們的時間片以后才能被調(diào)度執(zhí)行,在掛起和恢復執(zhí)行過程中存在著很大的開銷。鎖還存在著其它一些缺點,當一個線程正在等待鎖時,它不能做任何事。如果一個線程在持有鎖的情況下被延遲執(zhí)行,那么所有需要這個鎖的線程都無法執(zhí)行下去。如果被阻塞的線程優(yōu)先級高,而持有鎖的線程優(yōu)先級低,將會導致優(yōu)先級反轉(zhuǎn)(Priority Inversion)。

樂觀鎖與悲觀鎖

獨占鎖是一種悲觀鎖,synchronized就是一種獨占鎖,它假設最壞的情況,并且只有在確保其它線程不會造成干擾的情況下執(zhí)行,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。而另一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。

volatile的問題

與鎖相比,volatile變量是一和更輕量級的同步機制,因為在使用這些變量時不會發(fā)生上下文切換和線程調(diào)度等操作,但是volatile變量也存在一些局限:不能用于構(gòu)建原子的復合操作,因此當一個變量依賴舊值時就不能使用volatile變量。(參考:談談volatiile)

volatile只能保證變量對各個線程的可見性,但不能保證原子性。為什么?見我的另外一篇文章:《為什么volatile不能保證原子性而Atomic可以?》

Java中的原子操作( atomic operations)

原子操作指的是在一步之內(nèi)就完成而且不能被中斷。原子操作在多線程環(huán)境中是線程安全的,無需考慮同步的問題。在java中,下列操作是原子操作:

  • all assignments of primitive types except for long and double
  • all assignments of references
  • all operations of java.concurrent.Atomic* classes
  • all assignments to volatile longs and doubles

問題來了,為什么long型賦值不是原子操作呢?例如:

1

long foo = 65465498L;

實時上java會分兩步寫入這個long變量,先寫32位,再寫后32位。這樣就線程不安全了。如果改成下面的就線程安全了:

1

private volatile long foo;

因為volatile內(nèi)部已經(jīng)做了synchronized.

CAS無鎖算法

要實現(xiàn)無鎖(lock-free)的非阻塞算法有多種實現(xiàn)方法,其中CAS(比較與交換,Compare and swap)是一種有名的無鎖算法。CAS, CPU指令,在大多數(shù)處理器架構(gòu),包括IA32、Space中采用的都是CAS指令,CAS的語義是“我認為V的值應該為A,如果是,那么將V的值更新為B,否則不修改并告訴V的值實際為多少”,CAS是項樂觀鎖技術(shù),當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程并不會被掛起,而是被告知這次競爭中失敗,并可以再次嘗試。CAS有3個操作數(shù),內(nèi)存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內(nèi)存值V相同時,將內(nèi)存值V修改為B,否則什么都不做。CAS無鎖算法的C實現(xiàn)如下:

1

2

3

4

5

6

7

8

9

int compare_and_swap (int* reg, int oldval, int newval)

{

??ATOMIC();

??int old_reg_val = *reg;

??if (old_reg_val == oldval)

?????*reg = newval;

??END_ATOMIC();

??return old_reg_val;

}

CAS(樂觀鎖算法)的基本假設前提

CAS比較與交換的偽代碼可以表示為:

do{??
?????? 備份舊數(shù)據(jù);?
?????? 基于舊數(shù)據(jù)構(gòu)造新數(shù)據(jù);?
}while(!CAS( 內(nèi)存地址,備份的舊數(shù)據(jù),新數(shù)據(jù) ))??

(上圖的解釋:CPU去更新一個值,但如果想改的值不再是原來的值,操作就失敗,因為很明顯,有其它操作先改變了這個值。)

就是指當兩者進行比較時,如果相等,則證明共享數(shù)據(jù)沒有被修改,替換成新值,然后繼續(xù)往下運行;如果不相等,說明共享數(shù)據(jù)已經(jīng)被修改,放棄已經(jīng)所做的操作,然后重新執(zhí)行剛才的操作。容易看出 CAS 操作是基于共享數(shù)據(jù)不會被修改的假設,采用了類似于數(shù)據(jù)庫的 commit-retry 的模式。當同步?jīng)_突出現(xiàn)的機會很少時,這種假設能帶來較大的性能提升。

CAS的開銷(CPU Cache Miss problem)

前面說過了,CAS(比較并交換)是CPU指令級的操作,只有一步原子操作,所以非常快。而且CAS避免了請求操作系統(tǒng)來裁定鎖的問題,不用麻煩操作系統(tǒng),直接在CPU內(nèi)部就搞定了。但CAS就沒有開銷了嗎?不!有cache miss的情況。這個問題比較復雜,首先需要了解CPU的硬件體系結(jié)構(gòu):

上圖可以看到一個8核CPU計算機系統(tǒng),每個CPU有cache(CPU內(nèi)部的高速緩存,寄存器),管芯內(nèi)還帶有一個互聯(lián)模塊,使管芯內(nèi)的兩個核可以互相通信。在圖中央的系統(tǒng)互聯(lián)模塊可以讓四個管芯相互通信,并且將管芯與主存連接起來。數(shù)據(jù)以“緩存線”為單位在系統(tǒng)中傳輸,“緩存線”對應于內(nèi)存中一個 2 的冪大小的字節(jié)塊,大小通常為 32 到 256 字節(jié)之間。當 CPU 從內(nèi)存中讀取一個變量到它的寄存器中時,必須首先將包含了該變量的緩存線讀取到 CPU 高速緩存。同樣地,CPU 將寄存器中的一個值存儲到內(nèi)存時,不僅必須將包含了該值的緩存線讀到 CPU 高速緩存,還必須確保沒有其他 CPU 擁有該緩存線的拷貝。

比如,如果 CPU0 在對一個變量執(zhí)行“比較并交換”(CAS)操作,而該變量所在的緩存線在 CPU7 的高速緩存中,就會發(fā)生以下經(jīng)過簡化的事件序列:

  • CPU0 檢查本地高速緩存,沒有找到緩存線。
  • 請求被轉(zhuǎn)發(fā)到 CPU0 和 CPU1 的互聯(lián)模塊,檢查 CPU1 的本地高速緩存,沒有找到緩存線。
  • 請求被轉(zhuǎn)發(fā)到系統(tǒng)互聯(lián)模塊,檢查其他三個管芯,得知緩存線被 CPU6和 CPU7 所在的管芯持有。
  • 請求被轉(zhuǎn)發(fā)到 CPU6 和 CPU7 的互聯(lián)模塊,檢查這兩個 CPU 的高速緩存,在 CPU7 的高速緩存中找到緩存線。
  • CPU7 將緩存線發(fā)送給所屬的互聯(lián)模塊,并且刷新自己高速緩存中的緩存線。
  • CPU6 和 CPU7 的互聯(lián)模塊將緩存線發(fā)送給系統(tǒng)互聯(lián)模塊。
  • 系統(tǒng)互聯(lián)模塊將緩存線發(fā)送給 CPU0 和 CPU1 的互聯(lián)模塊。
  • CPU0 和 CPU1 的互聯(lián)模塊將緩存線發(fā)送給 CPU0 的高速緩存。
  • CPU0 現(xiàn)在可以對高速緩存中的變量執(zhí)行 CAS 操作了

以上是刷新不同CPU緩存的開銷。最好情況下的 CAS 操作消耗大概 40 納秒,超過 60 個時鐘周期。這里的“最好情況”是指對某一個變量執(zhí)行 CAS 操作的 CPU 正好是最后一個操作該變量的CPU,所以對應的緩存線已經(jīng)在 CPU 的高速緩存中了,類似地,最好情況下的鎖操作(一個“round trip 對”包括獲取鎖和隨后的釋放鎖)消耗超過 60 納秒,超過 100 個時鐘周期。這里的“最好情況”意味著用于表示鎖的數(shù)據(jù)結(jié)構(gòu)已經(jīng)在獲取和釋放鎖的 CPU 所屬的高速緩存中了。鎖操作比 CAS 操作更加耗時,是因深入理解并行編程
為鎖操作的數(shù)據(jù)結(jié)構(gòu)中需要兩個原子操作。緩存未命中消耗大概 140 納秒,超過 200 個時鐘周期。需要在存儲新值時查詢變量的舊值的 CAS 操作,消耗大概 300 納秒,超過 500 個時鐘周期。想想這個,在執(zhí)行一次 CAS 操作的時間里,CPU 可以執(zhí)行 500 條普通指令。這表明了細粒度鎖的局限性。

以下是cache miss cas 和lock的性能對比:

JVM對CAS的支持:AtomicInt, AtomicLong.incrementAndGet()

在JDK1.5之前,如果不編寫明確的代碼就無法執(zhí)行CAS操作,在JDK1.5中引入了底層的支持,在int、long和對象的引用等類型上都公開了CAS的操作,并且JVM把它們編譯為底層硬件提供的最有效的方法,在運行CAS的平臺上,運行時把它們編譯為相應的機器指令,如果處理器/CPU不支持CAS指令,那么JVM將使用自旋鎖。因此,值得注意的是,CAS解決方案與平臺/編譯器緊密相關(比如x86架構(gòu)下其對應的匯編指令是lock cmpxchg,如果想要64Bit的交換,則應使用lock cmpxchg8b。在.NET中我們可以使用Interlocked.CompareExchange函數(shù))

在原子類變量中,如java.util.concurrent.atomic中的AtomicXXX,都使用了這些底層的JVM支持為數(shù)字類型的引用類型提供一種高效的CAS操作,而在java.util.concurrent中的大多數(shù)類在實現(xiàn)時都直接或間接的使用了這些原子變量類。

Java 1.6中AtomicLong.incrementAndGet()的實現(xiàn)源碼為:

+ View Code

由此可見,AtomicLong.incrementAndGet的實現(xiàn)用了樂觀鎖技術(shù),調(diào)用了sun.misc.Unsafe類庫里面的 CAS算法,用CPU指令來實現(xiàn)無鎖自增。所以,AtomicLong.incrementAndGet的自增比用synchronized的鎖效率倍增。

1

2

3

4

5

6

7

8

9

10

11

12

public final int getAndIncrement() {?

????????for (;;) {?

????????????int current = get();?

????????????int next = current + 1;?

????????????if (compareAndSet(current, next))?

????????????????return current;?

????????}?

}?

???

public final boolean compareAndSet(int expect, int update) {?

????return unsafe.compareAndSwapInt(this, valueOffset, expect, update);?

}

下面是測試代碼:可以看到用AtomicLong.incrementAndGet的性能比用synchronized高出幾倍。

+ View Code

CAS的例子:非阻塞堆棧

下面是比非阻塞自增稍微復雜一點的CAS的例子:非阻塞堆棧/ConcurrentStack 。ConcurrentStack 中的 push() 和 pop() 操作在結(jié)構(gòu)上與NonblockingCounter 上相似,只是做的工作有些冒險,希望在 “提交” 工作的時候,底層假設沒有失效。push() 方法觀察當前最頂?shù)墓?jié)點,構(gòu)建一個新節(jié)點放在堆棧上,然后,如果最頂端的節(jié)點在初始觀察之后沒有變化,那么就安裝新節(jié)點。如果 CAS 失敗,意味著另一個線程已經(jīng)修改了堆棧,那么過程就會重新開始。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

public class ConcurrentStack<E> {

????AtomicReference<Node<E>> head = new AtomicReference<Node<E>>();

????public void push(E item) {

????????Node<E> newHead = new Node<E>(item);

????????Node<E> oldHead;

????????do {

????????????oldHead = head.get();

????????????newHead.next = oldHead;

????????} while (!head.compareAndSet(oldHead, newHead));

????}

????public E pop() {

????????Node<E> oldHead;

????????Node<E> newHead;

????????do {

????????????oldHead = head.get();

????????????if (oldHead == null)

????????????????return null;

????????????newHead = oldHead.next;

????????} while (!head.compareAndSet(oldHead,newHead));

????????return oldHead.item;

????}

????static class Node<E> {

????????final E item;

????????Node<E> next;

????????public Node(E item) { this.item = item; }

????}

}

在輕度到中度的爭用情況下,非阻塞算法的性能會超越阻塞算法,因為 CAS 的多數(shù)時間都在第一次嘗試時就成功,而發(fā)生爭用時的開銷也不涉及線程掛起和上下文切換,只多了幾個循環(huán)迭代。沒有爭用的 CAS 要比沒有爭用的鎖便宜得多(這句話肯定是真的,因為沒有爭用的鎖涉及 CAS 加上額外的處理),而爭用的 CAS 比爭用的鎖獲取涉及更短的延遲。

在高度爭用的情況下(即有多個線程不斷爭用一個內(nèi)存位置的時候),基于鎖的算法開始提供比非阻塞算法更好的吞吐率,因為當線程阻塞時,它就會停止爭用,耐心地等候輪到自己,從而避免了進一步爭用。但是,這么高的爭用程度并不常見,因為多數(shù)時候,線程會把線程本地的計算與爭用共享數(shù)據(jù)的操作分開,從而給其他線程使用共享數(shù)據(jù)的機會。

CAS的例子3:非阻塞鏈表

以上的示例(自增計數(shù)器和堆棧)都是非常簡單的非阻塞算法,一旦掌握了在循環(huán)中使用 CAS,就可以容易地模仿它們。對于更復雜的數(shù)據(jù)結(jié)構(gòu),非阻塞算法要比這些簡單示例復雜得多,因為修改鏈表、樹或哈希表可能涉及對多個指針的更新。CAS 支持對單一指針的原子性條件更新,但是不支持兩個以上的指針。所以,要構(gòu)建一個非阻塞的鏈表、樹或哈希表,需要找到一種方式,可以用 CAS 更新多個指針,同時不會讓數(shù)據(jù)結(jié)構(gòu)處于不一致的狀態(tài)。

在鏈表的尾部插入元素,通常涉及對兩個指針的更新:“尾” 指針總是指向列表中的最后一個元素,“下一個” 指針從過去的最后一個元素指向新插入的元素。因為需要更新兩個指針,所以需要兩個 CAS。在獨立的 CAS 中更新兩個指針帶來了兩個需要考慮的潛在問題:如果第一個 CAS 成功,而第二個 CAS 失敗,會發(fā)生什么?如果其他線程在第一個和第二個 CAS 之間企圖訪問鏈表,會發(fā)生什么?

對于非復雜數(shù)據(jù)結(jié)構(gòu),構(gòu)建非阻塞算法的 “技巧” 是確保數(shù)據(jù)結(jié)構(gòu)總處于一致的狀態(tài)(甚至包括在線程開始修改數(shù)據(jù)結(jié)構(gòu)和它完成修改之間),還要確保其他線程不僅能夠判斷出第一個線程已經(jīng)完成了更新還是處在更新的中途,還能夠判斷出如果第一個線程走向 AWOL,完成更新還需要什么操作。如果線程發(fā)現(xiàn)了處在更新中途的數(shù)據(jù)結(jié)構(gòu),它就可以 “幫助” 正在執(zhí)行更新的線程完成更新,然后再進行自己的操作。當?shù)谝粋€線程回來試圖完成自己的更新時,會發(fā)現(xiàn)不再需要了,返回即可,因為 CAS 會檢測到幫助線程的干預(在這種情況下,是建設性的干預)。

這種 “幫助鄰居” 的要求,對于讓數(shù)據(jù)結(jié)構(gòu)免受單個線程失敗的影響,是必需的。如果線程發(fā)現(xiàn)數(shù)據(jù)結(jié)構(gòu)正處在被其他線程更新的中途,然后就等候其他線程完成更新,那么如果其他線程在操作中途失敗,這個線程就可能永遠等候下去。即使不出現(xiàn)故障,這種方式也會提供糟糕的性能,因為新到達的線程必須放棄處理器,導致上下文切換,或者等到自己的時間片過期(而這更糟)。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

public class LinkedQueue <E> {

????private static class Node <E> {

????????final E item;

????????final AtomicReference<Node<E>> next;

????????Node(E item, Node<E> next) {

????????????this.item = item;

????????????this.next = new AtomicReference<Node<E>>(next);

????????}

????}

????private AtomicReference<Node<E>> head

????????= new AtomicReference<Node<E>>(new Node<E>(null, null));

????private AtomicReference<Node<E>> tail = head;

????public boolean put(E item) {

????????Node<E> newNode = new Node<E>(item, null);

????????while (true) {

????????????Node<E> curTail = tail.get();

????????????Node<E> residue = curTail.next.get();

????????????if (curTail == tail.get()) {

????????????????if (residue == null) /* A */ {

????????????????????if (curTail.next.compareAndSet(null, newNode)) /* C */ {

????????????????????????tail.compareAndSet(curTail, newNode) /* D */ ;

????????????????????????return true;

????????????????????}

????????????????} else {

????????????????????tail.compareAndSet(curTail, residue) /* B */;

????????????????}

????????????}

????????}

????}

}

具體算法相見IBM Developerworks

Java的ConcurrentHashMap的實現(xiàn)原理

Java5中的ConcurrentHashMap,線程安全,設計巧妙,用桶粒度的鎖,避免了put和get中對整個map的鎖定,尤其在get中,只對一個HashEntry做鎖定操作,性能提升是顯而易見的。

具體實現(xiàn)中使用了鎖分離機制,在這個帖子中有非常詳細的討論。這里有關于Java內(nèi)存模型結(jié)合ConcurrentHashMap的分析。以下是JDK6的ConcurrentHashMap的源碼:

+ View Code

Java的ConcurrentLinkedQueue實現(xiàn)方法

ConcurrentLinkedQueue也是同樣使用了CAS指令,但其性能并不高因為太多CAS操作。其源碼如下:

+ View Code

高并發(fā)環(huán)境下優(yōu)化鎖或無鎖(lock-free)的設計思路

服務端編程的3大性能殺手:1、大量線程導致的線程切換開銷。2、鎖。3、非必要的內(nèi)存拷貝。在高并發(fā)下,對于純內(nèi)存操作來說,單線程是要比多線程快的, 可以比較一下多線程程序在壓力測試下cpu的sy和ni百分比。高并發(fā)環(huán)境下要實現(xiàn)高吞吐量和線程安全,兩個思路:一個是用優(yōu)化的鎖實現(xiàn),一個是lock-free的無鎖結(jié)構(gòu)。但非阻塞算法要比基于鎖的算法復雜得多。開發(fā)非阻塞算法是相當專業(yè)的訓練,而且要證明算法的正確也極為困難,不僅和具體的目標機器平臺和編譯器相關,而且需要復雜的技巧和嚴格的測試。雖然Lock-Free編程非常困難,但是它通常可以帶來比基于鎖編程更高的吞吐量。所以Lock-Free編程是大有前途的技術(shù)。它在線程中止、優(yōu)先級倒置以及信號安全等方面都有著良好的表現(xiàn)。

  • 優(yōu)化鎖實現(xiàn)的例子:Java中的ConcurrentHashMap,設計巧妙,用桶粒度的鎖和鎖分離機制,避免了put和get中對整個map的鎖定,尤其在get中,只對一個HashEntry做鎖定操作,性能提升是顯而易見的(詳細分析見《探索 ConcurrentHashMap 高并發(fā)性的實現(xiàn)機制》)。
  • Lock-free無鎖的例子:CAS(CPU的Compare-And-Swap指令)的利用和LMAX的disruptor無鎖消息隊列數(shù)據(jù)結(jié)構(gòu)等。有興趣了解LMAX的disruptor無鎖消息隊列數(shù)據(jù)結(jié)構(gòu)的可以移步slideshare。

disruptor無鎖消息隊列數(shù)據(jù)結(jié)構(gòu)的類圖和技術(shù)文檔下載

另外,在設計思路上除了盡量減少資源爭用以外,還可以借鑒nginx/node.js等單線程大循環(huán)的機制,用單線程或CPU數(shù)相同的線程開辟大的隊列,并發(fā)的時候任務壓入隊列,線程輪詢?nèi)缓笠粋€個順序執(zhí)行。由于每個都采用異步I/O,沒有阻塞線程。這個大隊列可以使用RabbitMQueue,或是JDK的同步隊列(性能稍差),或是使用Disruptor無鎖隊列(Java)。任務處理可以全部放在內(nèi)存(多級緩存、讀寫分離、ConcurrentHashMap、甚至分布式緩存Redis)中進行增刪改查。最后用Quarz維護定時把緩存數(shù)據(jù)同步到DB中。當然,這只是中小型系統(tǒng)的思路,如果是大型分布式系統(tǒng)會非常復雜,需要分而治理,用SOA的思路,參考這篇文章的圖。(注:Redis是單線程的純內(nèi)存數(shù)據(jù)庫,單線程無需鎖,而Memcache是多線程的帶CAS算法,兩者都使用epoll,no-blocking io)

深入JVM的OS的無鎖非阻塞算法

如果深入 JVM 和操作系統(tǒng),會發(fā)現(xiàn)非阻塞算法無處不在。垃圾收集器使用非阻塞算法加快并發(fā)和平行的垃圾搜集;調(diào)度器使用非阻塞算法有效地調(diào)度線程和進程,實現(xiàn)內(nèi)在鎖。在 Mustang(Java 6.0)中,基于鎖的 SynchronousQueue 算法被新的非阻塞版本代替。很少有開發(fā)人員會直接使用 SynchronousQueue,但是通過 Executors.newCachedThreadPool() 工廠構(gòu)建的線程池用它作為工作隊列。比較緩存線程池性能的對比測試顯示,新的非阻塞同步隊列實現(xiàn)提供了幾乎是當前實現(xiàn) 3 倍的速度。在 Mustang 的后續(xù)版本(代碼名稱為 Dolphin)中,已經(jīng)規(guī)劃了進一步的改進。

參考文獻

IBM developerworks: Java theory and practice: Going atomic

?

轉(zhuǎn)載于:https://www.cnblogs.com/Mainz/p/3546347.html

總結(jié)

以上是生活随笔為你收集整理的非阻塞同步算法与CAS(Compare and Swap)无锁算法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。