posts - 1,  comments - 25,  trackbacks - 0

          Preface


          最近看了一下<Java Concurrency In Practice> 這本書, 總體來說還是一本不錯的書, 不過粒度不夠細, 是從大的角度, 例如: 設計整體項目上如何考慮并發(fā)的多方面因素,不過總體上來說還是一本不錯的書,結合部分網(wǎng)絡上的資料,總結一下自己的知識,免的忘了。

          下面是一些最基本的知識,不想再寫了,反正網(wǎng)上多的是,挑了一篇還不錯的轉過來,大家要支持別人的成果O:
          http://yanxuxin.iteye.com/blog/547261 

          對于進程的概念我們都很熟悉,它是應用程序級的隔離,不同的應用程序之間的進程幾乎不共享任何資源。而線程則可以說是應用程序內的隔離,一種相對低級別的隔離。一個進程可以有多個線程,它們之間隔離的內容大致包括:a.自身的堆棧,b.程序計數(shù)器,c.局部變量;共享應用的內容大致包括:a.內存,b.文件句柄,c.進程狀態(tài)等。線程不是Java自身的概念,它是操作系統(tǒng)底層的概念。Java作為一種應用語言把線程的操作通過API提升到應用開發(fā)的支持,但是在并發(fā)性的支持上并不是那么美好。 
             Java在設計時,每個對象都有一個隱式的鎖,這個鎖的使用則是通過synchronized關鍵字來顯式的使用。在JDK5.0以后引用了java.util.concurrent.ReentrantLock作為synchronized之外的選擇,配和Condition可以以一種條件鎖的機制來管理并發(fā)的線程,之后的總結再介紹。提到synchronized,多數(shù)的初學者都知道Object的wait(),notify(),notifyAll()是配和其使用的,但是為什么要在同步內才能用對象的這些方法呢(不然拋IllegalMonitorStateException)? 
             我想因為沒有synchronized讓對象的隱式鎖發(fā)揮作用,那么方法或者方法塊內的線程在同一時間可能存在多個,假設wait()可用,它會把這些線程統(tǒng)統(tǒng)的加到wait set中等待被喚醒,這樣永遠沒有多余的線程去喚醒它們。每個對象管理調用其wait(),notify()的線程,使得別的對象即使想幫忙也幫不上忙。這樣的結果就是多線程永遠完成不了多任務,基于此Java在設計時使其必須與synchronized一起使用,這樣獲得隱式鎖的線程同一時間只有一個,當此線程被對象的wait()扔到wait set中時,線程會釋放這個對象的隱式鎖等待被喚醒的機會,這樣的設計會大大降低死鎖。另外同一個對象隱式鎖作用下的多個方法或者方法塊在沒有鎖的限制下可以同時允許多個線程在不同的方法內wait和notify,嚴重的競爭條件使得死鎖輕而易舉。所以Java設計者試圖通過Monitor Object模式解決這些問題,每個對象都是Monitor用于監(jiān)視擁有其使用權的線程。 
             但是synchronized這種獲得隱式鎖的方式本身也是有隱患問題的:
              a.不能中斷正在試圖獲得鎖的線程,
          b.試圖獲得鎖時不能設定超時,
          c.每個鎖只有一個條件太少。

          對于最后一項的設計前面提到的JDK5的方案是可以彌補的,一個ReentrantLock可以有多個Condition,每個條件管理獲得對象鎖滿足條件的線程,通過await(),signalAll()使只關于Condition自己放倒的線程繼續(xù)運行,或者放倒一些線程,而不是全部喚醒等等。但對于前兩者的極端情況會出現(xiàn)死鎖。下面的這個例子: 

          Java代碼  收藏代碼
          1. class DeadLockSample{  
          2.     public final Object lock1 = new Object();  
          3.     public final Object lock2 = new Object();  
          4.   
          5.     public void methodOne(){  
          6.        synchronized(lock1){  
          7.           ...  
          8.           synchronized(lock2){...}  
          9.        }  
          10.     }  
          11.   
          12.     public void methodTwo(){  
          13.        synchronized(lock2){  
          14.       ...  
          15.           synchronized(lock1){...}  
          16.        }  
          17.     }  
          18. }  

          假設場景:線程A調用methodOne(),獲得lock1的隱式鎖后,在獲得lock2的隱式鎖之前線程B進入運行,調用methodTwo(),搶先獲得了lock2的隱式鎖,此時線程A等著線程B交出lock2,線程B等著lock1進入方法塊,死鎖就這樣被創(chuàng)造出來了。 
             以上的例子不直觀的話,再看一個實例順便看看wait()的缺陷: 
          Java代碼  收藏代碼
          1. import java.util.LinkedList;  
          2. import java.util.List;  
          3.   
          4. /** 
          5.  * User: yanxuxin 
          6.  * Date: Dec 9, 2009 
          7.  * Time: 5:58:39 PM 
          8.  */  
          9. public class DeadLockSample {  
          10.     public static void main(String[] args) {  
          11.         final WaitAndNotify wan = new WaitAndNotify();  
          12.   
          13.         Thread t1 = new Thread(new Runnable(){  
          14.             public void run() {  
          15.               wan.pop();  
          16.             }  
          17.         });  
          18.   
          19.         Thread t2 = new Thread(new Runnable(){  
          20.             public void run() {  
          21.               wan.push("a");  
          22.             }  
          23.         });  
          24.   
          25.         t1.start();  
          26.         t2.start();  
          27.    }  
          28. }  
          29.   
          30. class WaitAndNotify {  
          31.   
          32.     final List<String> list = new LinkedList<String>();  
          33.   
          34.     public synchronized void push(String x) {  
          35.         synchronized(list) {  
          36.             list.add(x);  
          37.             notify();  
          38.         }  
          39.     }  
          40.   
          41.     public synchronized Object pop() {  
          42.         synchronized(list) {  
          43.             if(list.size() <= 0) {  
          44.                 try {  
          45.                     wait();  
          46.                 } catch (InterruptedException e) {  
          47.                     e.printStackTrace();  
          48.                 }  
          49.             }  
          50.             return list.size();  
          51.         }  
          52.     }  
          53.   
          54. }  

          上面的這個例子也會出現(xiàn)死鎖,為什么呢?首先看WaitAndNotify這個類,在push和pop方法上有synchronized關鍵字,方法內部也有synchronized,那么當WaitAndNotify實例化時會有兩個對象的隱式鎖,一個是WaitAndNotify對象自身的,作用在方法上;另一個就是方法內部同步用到的list的。主線程開啟兩個線程t1和t2,t1進入pop方法此時list為空,它先后獲得了wan和list的隱式鎖,接著就被wait扔進wait set等待去了。注意這個wait()方法是誰的?答案是wan的,所以它釋放了wan的隱式鎖,但是把list的死死的抓著不放。此時t2終于得到了wan的隱式鎖進入push方法,但是不幸的是list的隱式鎖它這輩子也得不到了。。。 


          此外synchronized的重點說的簡單:它就是配和對象的隱式鎖使用的,注意一定是對象的隱式鎖!那么下面的這個例子又怎么解釋呢? 
          Java代碼  收藏代碼
          1. /** 
          2.  * User: yanxuxin 
          3.  * Date: Dec 17, 2009 
          4.  * Time: 9:38:27 PM 
          5.  */  
          6. public class ImplicitLockSample {  
          7.   
          8.     public static void main(String[] args) {  
          9.         final ImplicitLock sample = new ImplicitLock();  
          10.           
          11.         new Thread(new Runnable() {  
          12.             public void run() {  
          13. //                ImplicitLock.method1();  
          14.                 sample.method1();  
          15.             }  
          16.         }).start();  
          17.   
          18.         new Thread(new Runnable() {  
          19.             public void run() {  
          20.                 sample.method2();  
          21.             }  
          22.         }).start();  
          23.     }  
          24. }  
          25.   
          26.   
          27. class ImplicitLock {  
          28.   
          29.     public static synchronized void method1() {  
          30.         System.out.println("method1 executing...");  
          31.         try {  
          32.             Thread.sleep(3000);  
          33.         } catch (InterruptedException e) {  
          34.             e.printStackTrace();  
          35.         }  
          36.     }  
          37.   
          38.     public synchronized void method2() {  
          39.         System.out.println("method2 executing...");  
          40.         try {  
          41.             Thread.sleep(3000);  
          42.         } catch (InterruptedException e) {  
          43.             e.printStackTrace();  
          44.         }  
          45.     }  
          46. }  

          這里ImplicitLock有兩個同步方法,一個是static的,一個是普通的。ImplicitLockSample是一個測試主程序,實例化一個ImplicitLock對象,并且開啟兩個線程,每個線程分別調用對象的method1和method2方法。每個進入方法的線程都會強制休眠3秒。那么執(zhí)行的現(xiàn)象是什么呢? 
             
             要知道答案有以下幾點要清楚:a.Class和Object的關系,b.static方法的含義,c.synchronized的機制,d.sleep的作用。清楚的知道這些之后,一眼就能辨別method1和method2方法上的synchronized配和的是兩把不同的對象隱式鎖。答案也就清晰的知道這兩個線程執(zhí)行的打印語句根本就不會相差近3秒的等待,而是幾乎同時。下面我試著解釋一下。 

             Class是Object的子類,說明了Class是特殊的對象,它自然也有對象隱式鎖。static聲明方法意味著這個方法不依賴于類的實例,而是可以理解成去掉了隱式參數(shù)this的,類對象的方法。synchronized是與對象隱式鎖綁定的,這代表了將其置于方法聲明上它將與方法的持有對象綁定。所以method1的同步鎖是ImplicitLock類對象的隱式鎖,而method2的同步鎖是ImplicitLock實例對象的隱式鎖。sleep雖然能讓當前的線程休眠,但是它不會釋放持有的隱式鎖。這樣主程序執(zhí)行是雖然用同一個實例讓兩個線程分別去調用兩個方法,但是它們之間并沒有任何競爭鎖的關系,所以幾乎同時打印,不會有近3秒的間隔。把method1的調用改成已注釋的代碼將更容易理解。如果method1的synchronized去掉,或者method2加上synchronized的聲明,那么它們將競爭同一個隱式鎖。先獲得鎖的線程將在3秒后交出鎖,后面的線程才能執(zhí)行打印。 

             寫這篇補遺源自于對懶漢式單例的重新理解,之前對synchronized的機制不明了時,只知道使用synchronized關鍵字在static方法上聲明就能保證單例的線程安全,但是確不知道那算是誤打誤撞的理解。構造這個驗證例子之前,static和synchronized的共同使用讓我對synchronized隱式鎖有了更清晰的認識。所以打算再寫寫來分享這段體會。 

          volatile關鍵字


          Volatile是JDK7里的核心,為什么,隨便看一下LinkedBlockQueue, head 等關鍵就會發(fā)現(xiàn)不懂volatile那什么都是扯。。

          volatile就是被認為“輕量級的synchronized”,但是使用其雖然可以簡化同步的編碼,并且運行開銷相對于JVM沒有優(yōu)化的競爭線程同步低,但是濫用將不能保證程序的正確性。鎖的兩個特性是:互斥和可見。互斥保證了同時只有一個線程持有對象鎖進行共享數(shù)據(jù)的操作,從而保證了數(shù)據(jù)操作的原子性,而可見則保證共享數(shù)據(jù)的修改在下一個線程獲得鎖后看到更新后的數(shù)據(jù)。volatile僅僅保證了無鎖的可見性,但是不提供原子性操作的保證!這是因為volatile關鍵字作用的設計是JVM阻止volatile變量的值放入處理器的寄存器,在寫入值以后會被從處理器的cache中flush掉,寫到內存中去。這樣讀的時候限制處理器的cache是無效的,只能從內存讀取值,保證了可見性。從這個實現(xiàn)可以看出volatile的使用場景:多線程大量的讀取,極少量或者一次性的寫入,并且還有其他限制。 
             由于其無法保證“讀-修改-寫”這樣操作的原子性(當然java.util.concurrent.atomic包內的實現(xiàn)滿足這些操作,主要是通過CAS--比較交換的機制,后續(xù)會嘗試寫寫。),所以像++,--,+=,-=這樣的變量操作,即使聲明volatile也不會保證正確性。圍繞這個原理的主題,我們可以大致的整理一下volatile代替synchronized的條件:對變量的寫操作不依賴自身的狀態(tài)。所以除了剛剛介紹的操作外,例如: 
          Java代碼  收藏代碼
          1. private volatile boolean flag;  
          2.   if(!flag) {  
          3.     flag == true;  
          4. }  

          類似這樣的操作也是違反volatile使用條件的,很可能造成程序的問題。所以使用volatile的簡單場景是一次性的寫入之后,大量線程的讀取并且不再改變變量的值(如果這樣的話,都不是并發(fā)了)。這個關鍵字的優(yōu)勢還是在于多線程的讀取,既保證了讀取的低開銷(與單線程程序變量差不多),又能保證讀到的是最新的值。所以利用這個優(yōu)勢我們可以結合synchronized使用實現(xiàn)低開銷讀寫鎖: 
          Java代碼  收藏代碼
          1. /** 
          2.  * User: yanxuxin 
          3.  * Date: Dec 12, 2009 
          4.  * Time: 8:28:29 PM 
          5.  */  
          6. public class AnotherSyncSample {  
          7.     private volatile int counter;  
          8.   
          9.     public int getCounter() {   
          10.     return counter;   
          11.     }  
          12.   
          13.     public synchronized void add() {  
          14.         counter++;  
          15.     }  
          16. }  

          這個簡單的例子在讀的方法上沒有使用synchronized關鍵字,所以讀的操作幾乎沒有等待;而由于寫的操作是原子性的違反了使用條件,不能得到保證,所以使用synchronized同步得到寫的正確性保證,這個模型在多讀取少寫入的實際場景中應該要比都用synchronized的性能有不小的提升。 
             另外還有一個使用volatile的好處,得自于其原理:內部禁止改變兩個volatile變量的賦值或者初始化順序,并且嚴格限制volatile變量和其周圍非volatile變量的賦值或者初始化順序。 
          Java代碼  收藏代碼
          1. /** 
          2.  * User: yanxuxin 
          3.  * Date: Dec 12, 2009 
          4.  * Time: 8:34:07 PM 
          5.  */  
          6. public class VolatileTest {  
          7.     public static void main(String[] args) {  
          8.         final VolatileSample sample = new VolatileSample();  
          9.   
          10.         new Thread(new Runnable(){  
          11.             public void run() {  
          12.                 sample.finish();  
          13.             }  
          14.         }).start();  
          15.   
          16.          new Thread(new Runnable(){  
          17.             public void run() {  
          18.                 sample.doSomething();  
          19.             }  
          20.         }).start();  
          21.     }  
          22. }  
          23.   
          24. class VolatileSample {  
          25.     private volatile boolean finished;  
          26.     private int lucky;  
          27.   
          28.     public void doSomething() {  
          29.         if(finished) {  
          30.             System.out.println("lucky: " + lucky);  
          31.         }  
          32.     }  
          33.   
          34.     public void finish() {  
          35.         lucky = 7;  
          36.         finished = true;  
          37.     }  
          38. }  

          這里首先線程A執(zhí)行finish(),完成finished變量的賦值后,線程B進入方法doSomething()讀到了finish的值為true,打印lucky的值,預想狀態(tài)下為7,這樣完美的執(zhí)行結束了。但是,事實是如果finished變量不是聲明了volatile的話,過程就有可能是這樣的:線程A執(zhí)行finish()先對finished賦值,與此同時線程B進入doSomething()得到finished的值為true,打印lucky的值為0,鏡頭切回線程A,接著給lucky賦值為7,可憐的是這個幸運數(shù)字不幸杯具了。因為這里發(fā)生了扯淡的事情:JVM或許為了優(yōu)化執(zhí)行把兩者的賦值順序調換了。這個結果在單線程的程序中簡直絕對一定肯定就是不可能,遺憾的是多線程存在這個隱患。 

          ThreadLocal


          作為一個JDK5以后支持范型的類,主要是想利用范型把非線程安全的共享變量,封裝成綁定線程的安全不共享變量。這樣的解釋我想我們多半能猜出它的實現(xiàn)思路:把一個共享變量在每個線程使用時,初始化一個副本,并且和線程綁定。以后所有的線程對共享變量的操作都是對線程內部那個副本,完全的線程內部變量的操作。 

             要實現(xiàn)這樣功能類的設計,主要技術點是要能把副本和線程綁定映射,程序可以安全查找到當前線程的副本,修改后安全的綁定給線程。所以我們想到了Map的存儲結構,ThreadLocal內部就是使用了線程安全的Map形式的存儲把currentThread和變量副本一一映射。 
          既然要把共享的變成不共享的,那么就要變量滿足一個場景:變量的狀態(tài)不需要共享。例如無狀態(tài)的bean在多線程之間是安全的,因為線程之間不需要同步bean的狀態(tài),用了就走(很不負責啊),想用就用。但是對于有狀態(tài)的bean在線程之間則必須小心,線程A剛看到狀態(tài)是a,正想利用a做事情,線程B把bean的狀態(tài)改為了b,結果做了不該做的。但是如果有狀態(tài)的bean不需要共享狀態(tài),每個線程看到狀態(tài)a或者b都可以做出自己的行為,這種情況下不同步的選擇就是ThreadLocal了。 

             利用ThreadLocal的優(yōu)勢就在于根本不用擔心有狀態(tài)的bean為了狀態(tài)的一致而犧牲性能,去使用synchronized限制只有一個線程在同一時間做出關于bean狀態(tài)的行為。而是多個線程同時根據(jù)自己持有的bean的副本的狀態(tài)做出行為,這樣的轉變對于并發(fā)的支持是那么的不可思議。例如一個Dao內有個Connection的屬性,當多個線程使用Dao的同一個實例時,問題就來了:多個線程用一個Connection,而且它還是有連接,關閉等等的狀態(tài)轉變的,我們很敏感的想到這個屬性不安全!再看這個屬性,其實它是多么的想告訴線程哥哥們:我的這些狀態(tài)根本就不想共享,不要因為我的狀態(tài)而不敢一起追求。線程哥哥們也郁悶:你要是有多胞胎姐妹該多好啊!這時候ThreadLocal大哥過來說:小菜,我來搞定!你們這些線程一人一個Connection,你想關就關,想連接就連接,再也不用抱怨說它把你的連接關了。這樣Dao的實例再也不用因為自己有個不安全的屬性而自卑了。當然ThreadLocal的思路雖然是很好的,但是官方的說法是最初的實現(xiàn)性能并不好,隨著Map結構和Thread.currentThread的改進,性能較之synchronized才有了明顯的優(yōu)勢。所以要是使用的是JDK1.2,JDK1.3等等,也不要妄想麻雀變鳳凰... 

             再看ThreadLocal和synchronized的本質。前者不在乎多占點空間,但是絕對的忍受不了等待;后者對等待無所謂,但是就是不喜歡浪費空間。這也反映出了算法的一個規(guī)律:通常是使用場景決定時間和空間的比例,既省時又省地的算法多數(shù)情況下只存在于幻想之中。下面寫個簡單的例子解釋一下,不過個人覺得設計的例子不太好,以后有實際的啟發(fā)再替換吧。 
          Java代碼  收藏代碼
          1. import java.util.concurrent.atomic.AtomicInteger;  
          2.   
          3. /** 
          4.  * User: yanxuxin 
          5.  * Date: Dec 14, 2009 
          6.  * Time: 9:26:41 PM 
          7.  */  
          8. public class ThreadLocalSample extends Thread {  
          9.     private OperationSample2 operationSample;  
          10.   
          11.     public ThreadLocalSample(OperationSample2 operationSample) {  
          12.         this.operationSample = operationSample;  
          13.     }  
          14.   
          15.     @Override  
          16.     public void run() {  
          17.         operationSample.printAndIncrementNum();  
          18.     }  
          19.   
          20.     public static void main(String[] args) {  
          21.   
          22.         final OperationSample2 operation = new OperationSample2();//The shared Object for threads.  
          23.   
          24.         for (int i = 0; i < 5; i++) {  
          25.             new ThreadLocalSample(operation).start();  
          26.         }  
          27.     }  
          28. }  
          29.   
          30. class OperationSample {  
          31.     private int num;  
          32.   
          33.     //public synchronized void printAndIncrementNum() {  
          34.     public void printAndIncrementNum() {  
          35.         for (int i = 0; i < 2; i++) {  
          36.             System.out.println(Thread.currentThread().getName() + "[id=" + num + "]");  
          37.             num += 10;  
          38.         }  
          39.     }  
          40. }  
          41.   
          42. class OperationSample2 {  
          43.   
          44.     private static ThreadLocal<Integer> threadArg = new ThreadLocal<Integer>() {  
          45.         @Override  
          46.         protected Integer initialValue() {  
          47.             return 0;  
          48.         }  
          49.     };  
          50.   
          51.     public void printAndIncrementNum() {  
          52.         for (int i = 0; i < 2; i++) {  
          53.             int num = threadArg.get();  
          54.             threadArg.set(num + 10);  
          55.             System.out.println(Thread.currentThread().getName() + "[id=" + num + "]");  
          56.         }  
          57.     }  
          58. }  
          59.   
          60. class OperationSample3 {  
          61.   
          62.     private static final AtomicInteger uniqueId = new AtomicInteger(0);  
          63.     private static ThreadLocal<Integer> threadArg = new ThreadLocal<Integer>() {  
          64.         @Override  
          65.         protected Integer initialValue() {  
          66.             return uniqueId.getAndIncrement();  
          67.         }  
          68.     };  
          69.   
          70.     public void printAndIncrementNum() {  
          71.         for (int i = 0; i < 2; i++) {  
          72.             int num = threadArg.get();  
          73.             threadArg.set(num + 10);  
          74.             System.out.println(Thread.currentThread().getName() + "[id=" + num + "]");  
          75.         }  
          76.     }  
          77. }  

          這個例子中ThreadLocalSample繼承自Thread持有OperationSample三個版本中的一個引用,并且在線程運行時執(zhí)行printAndIncrementNum()方法。 

             首先看版本1:OperationSample有個共享變量num,printAndIncrementNum()方法沒有同步保護,方法就是循環(huán)給num賦新值并打印改變值的線程名。因為沒有任何的同步保護,所以原本打算每個線程打印出的值是相鄰遞加10的結果變成了不確定的遞加。有可能線程1的循環(huán)第一次打印0,第二次就打印50。這時候我們使用被注釋的方法聲明,結果就是預想的同一個線程的兩次結果是相鄰的遞加,因為同一時刻只有一個線程獲得OperationSample實例的隱式鎖完成循環(huán)釋放鎖。 

             再看版本2:假設我們有個遞增10的簡單計數(shù)器,但是是對每個線程的計數(shù)。也就是說我們有一個Integer計數(shù)器負責每個線程的計數(shù)。雖然它是有狀態(tài)的,會變的,但是因為每個線程之間不需要共享變化,所以可以用ThreadLocal管理這個Integer。在這里看到我們的ThreadLocal變量的initialValue()方法被覆寫了,這個方法的作用就是當調用ThreadLocal的get()獲取線程綁定的副本時如果還沒綁定則調用這個方法在Map中添加當前線程的綁定映射。這里我們返回0,表示每個線程的初始副本在ThreadLocal的Map的紀錄都是0。再看printAndIncrementNum()方法,沒有任何的同步保護,所以多個線程可以同時進入。但是,每個線程通過threadArg.get()拿到的僅僅是自己的Integer副本,threadArg.set(num + 10)的也是自己的副本值。所以結果就是雖然線程的兩次循環(huán)打印有快有慢,但是每個線程的兩次結果都是0和10。 

             最后是版本3:和版本2的不同在于新加了一個uniqueId的變量。這個變量是java.util.concurrent.atomic包下的原子變量類。這是基于硬件支持的CAS(比較交換)原語的實現(xiàn),所以保證了++,--,+=,-=等操作的原子性。所以在ThreadLocal變量的initialValue()方法中使用uniqueId.getAndIncrement()將為每個線程初始化唯一不會重復的遞加1的Integer副本值。而結果就會變成5個線程的首次打印是0~4的5個數(shù)字,第二次每個線程的打印是線程對應的首次數(shù)字加10的值。 

             對于ThreadLocal的使用,Spring的源碼中有大量的應用,主要是要支持Singleton的實例管理,那么自身的一些Singleton的實現(xiàn)內非線程安全的變量,屬性要用ThreadLocal隔離共享。同時我們在使用Spring的IOC時也要注意有可能多線程調用的注冊到IOC容器的Singleton型實例是否真的線程安全。另外java.util.concurrent.atomic內的原子變量類簡單的提了一下,再看看怎么能瞎編出東西來吧。
          posted on 2012-06-25 09:37 Daniel 閱讀(1411) 評論(0)  編輯  收藏 所屬分類: CoreJava
          <2025年6月>
          25262728293031
          1234567
          891011121314
          15161718192021
          22232425262728
          293012345

          常用鏈接

          留言簿(3)

          隨筆檔案

          文章分類

          文章檔案

          相冊

          搜索

          •  

          最新評論

          主站蜘蛛池模板: 余江县| 莆田市| 和田县| 久治县| 乃东县| 上犹县| 黔西县| 巩义市| 柞水县| 承德县| 玉溪市| 西乡县| 双城市| 清流县| 罗源县| 静乐县| 通化县| 哈尔滨市| 泉州市| 北宁市| 宝坻区| 林甸县| 将乐县| 方正县| 静宁县| 南漳县| 襄垣县| 青海省| 姚安县| 阿鲁科尔沁旗| 五常市| 桑日县| 阳高县| 泸溪县| 左权县| 庆安县| 黑河市| 凤阳县| 龙海市| 威海市| 喜德县|