第21章併發

1、程序中的所有事物在任意時刻都只能執行一個步驟。
21.1併發的多面性
1、併發編程令人困惑的主要原因:使用併發時需要解決的問題有多個,而實現併發的方式也有多種,並且在這兩者之間沒有明顯的映射關係(而且通常只具有模糊的界線)。
21.1.1更快的執行
1、併發通常是提高運行在單處理器上的程序的性能。
2、在單處理器上運行的併發程序開銷確實應該比該程序的所有部分都順序執行的開銷大,因爲其中增加了所謂上下文切換的代價(從一個任務切換到另一個任務)。表面上看,將程序的所有部分當作單個的任務運行好像是開銷小一點,並且可以節省上下文切換的代價。使這個問題變得有些不同的是阻塞。如果程序中的某個任務因爲該線程控制範圍之外的某些條件(通常是I/O)而導致不能繼續執行,就說這個任務阻塞了。如果沒有併發,則整個程序都將停止下來,直至外部條件發生變化。但是,如果使用併發來編寫程序,那麼當一個任務阻塞的時候,程序中其他任務還可以繼續執行,因此這個程序可以保持繼續向前執行。事實上,從性能的角度看,如果沒有任務會阻塞,那麼在單處理器機器上使用併發就沒有任何意義。
3、實現併發最直接的方式是在操作系統級別使用進程。進程是運行在它自己的地址空間內的自包容的程序。多任務操作系統可以通過週期性地將CPU從一個進程切換到另一個進程,來實現同時運行多個進程(程序)、儘管這使得每個進程看起來在其執行過程中都是歇歇停停。
4、每個任務都作爲進程在其自己的地址空間中執行,因此任務之間根本不可能互相干涉。
5、線程機制是在由執行程序表示的單一進程中創建任務。
21.1.2改進代碼設計
21.2基本的線程機制
1、併發編程時我們可以將程序劃分爲多個分離的、獨立運行的任務。通過使用多線程機制,這些獨立任務(也被稱爲子任務)中的每一個都將由執行線程來驅動。一個線程就是在進程中一個單一的順序控制流,因此,單個進程可以擁有多個併發執行的任務,但是程序使得每個任務都好像有其自己的CPU一樣。其底層機制是切分CPU時間。
2、線程模型爲編程帶來了便利,簡化了在單一程序中同時交織在一起的多個操作的處理。在使用線程時,CPU將輪流給每個任務分配其佔用時間。每個任務都覺得自己在一直佔用CPU,但事實上CPU時間是劃分成片段分配給了所有任務。
3、線程的一大好處是可以使程序員從這個層次抽身出來,即代碼不必知道它是運行在具有一個還是多個CPU的機器上。所以,使用線程機制是一種建立透明的、可擴展的程序的方法,如果程序運行太慢,爲機器增添一個CPU就能很容易地加快程序的運行速度。多任務和多線程往往是使用多處理器系統的最合理方式。
21.2.1定義任務
1、

package com21;

/**
 * Created by Panda on 2018/5/22.
 */
public class ListOff implements Runnable{
    protected int countDown=10;
    private static int taskCount=0;
    private final int id=taskCount++;
    public ListOff() {
    }

    public ListOff(int countDown) {
        this.countDown = countDown;
    }
    public String status(){
        return "#"+id+"("+(countDown>0?countDown:"Liftoff")+"),";
    }

    @Override
    public void run() {
      while (countDown-->0){
          System.out.println(status());
          //在run()中對靜態方法Thread.yield()的調用時對線程調度器的一種建議(Java線程機制的一部分,可以將
          //CPU從一個線程轉移到另一個線程)
          Thread.yield();
      }
    }

  /*  public static void main(String[] args) {
        ListOff listOff = new ListOff();
        listOff.run();
    }*/
  public static void main(String[] args) {
      Thread thread = new Thread(new ListOff());
      thread.start();
  }
}

21.2.3使用Executor
1、Executor在客戶端和任務執行之間提供了一個間接層;與客戶端直接執行任務不同,這個中介對象將執行任務。Executor允許你管理一部任務的執行,而無須顯示地管理線程的生命週期。

package com21;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Created by Panda on 2018/5/22.
 */
public class CacheThreadPool {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i <5 ; i++) {
            executorService.execute(new ListOff());
            executorService.shutdown();
        }
    }
}

21.2.4從任務中產生返回值
1、Runnable是執行工作的獨立任務,不返回任何值。如果希望任務在完成時能夠返回一個值,可以實現Callable接口而不是Runnable接口。類型參數表示的是從方法call()中返回的值,並且必須使用ExecutorService.submit()方法調用它。
2、///////callableDemo
21.2.2休眠
21.2.6優先級
1、線程的優先級將該線程的重要性傳遞給了調度器。儘管CPU處理現有線程集的順序是不確定的,但是調度器將傾向於讓優先權最高的線程先執行。然而, 這並不是意味着優先權較低的線程將得不到執行(也就是說,優先權不會導致死鎖)。優先級較低的線程僅僅是執行的頻率較低。
2、//////////////simpelPriorities
21.2.7讓步
1、yield():建議具有相同優先級的其他線程可以運行。
21.2.8後臺線程
1、後臺線程:是指在程序運行的時候在後臺提供一種通用服務的線程,並且這種線程並不屬於程序中不可或缺的部分。因此,當所有的非後臺線程結束時,程序也就終止了,同時會殺死進程中的所有後臺線程。反過來說,只要有任何非後臺線程還在運行,程序就不會終止。
2、

package com21;

import java.util.concurrent.TimeUnit;

/**
 * Created by Panda on 2018/5/22.
 */
public class SimpleDaemons implements Runnable {

    @Override
    public void run() {
        try{
            while (true){
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread()+" "+this);
            }
        }catch (InterruptedException e){
            System.out.println("sleep() interrupted");
        }
    }

    public static void main(String[] args) throws Exception{
        for (int i = 0; i < 10; i++) {
            Thread daemon = new Thread(new SimpleDaemons());
            daemon.setDaemon(true);
            daemon.start();
        }
        System.out.println("all daemons started");
        TimeUnit.MILLISECONDS.sleep(175);
    }
}

3、

package com21;

import java.util.concurrent.TimeUnit;

/**
 * Created by Panda on 2018/5/22.
 */
class ADaemon implements Runnable{
    @Override
    public void run() {
        try{
            System.out.println("Starting ADaemon");
            TimeUnit.SECONDS.sleep(1);
        }catch (InterruptedException e){
            System.out.println("Exiting via InterruptedException");
        }finally {
            System.out.println("This should always run()?");
        }
    }
}
public class DaemosDontRunFinally {
    public static void main(String[] args) {
        Thread thread = new Thread(new ADaemon());
        thread.setDaemon(true);
        thread.start();
    }
    //finally 不會執行。當註釋掉setDaemon()時候,finally就會執行。
    //當最後一個非後臺線程終止時候,後臺線程會“突然”終止。因爲一旦main()退出,JVM就會立即關閉所有的後臺線程
    //而不會有任何你希望出現的確認形式
}

21.2.9編碼的變體

package com21;

import java.util.concurrent.TimeUnit;

/**
 * Created by Panda on 2018/5/22.
 */
public class SimpleDaemons implements Runnable {

    @Override
    public void run() {
        try{
            while (true){
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread()+" "+this);
            }
        }catch (InterruptedException e){
            System.out.println("sleep() interrupted");
        }
    }

    public static void main(String[] args) throws Exception{
        for (int i = 0; i < 10; i++) {
            Thread daemon = new Thread(new SimpleDaemons());
            daemon.setDaemon(true);
            daemon.start();
        }
        System.out.println("all daemons started");
        TimeUnit.MILLISECONDS.sleep(175);
    }
}
package com21;

/**
 * Created by Panda on 2018/5/22.
 */
public class SimpleManaged implements Runnable {
    private int count=5;
    private Thread thread = new Thread(this);

    public SimpleManaged() {
        thread.start();
    }
    public String toString(){
        return  Thread.currentThread().getName()+"("+count+")";
    }

    @Override
    public void run() {
     while (true){
         System.out.println(this);
         if(--count==0){
             return;
         }
     }
    }

    public static void main(String[] args) {
        for (int i = 0; i <5 ; i++) {
            new SimpleManaged();
        }
    }
}

21.2.10術語
21.2.11加入一個線程
1、一個線程可以在其他線程之上調用join()方法,其效果是等待一段時間直到第二個線程結束才繼續執行。如果某個線程在另一個線程t上調用t.join(),此線程將被掛起,直到目標線程t結束才恢復(即t.isAlive()返回爲假)
2、調用join()時帶上一個超時參數(單位可以是毫秒,或者毫秒和納秒),這樣如果目標線程在這段時間到期時還沒有結束的話,join()方法總能返回。
3、join()方法的調用可以被中斷,做法是在調用線程上調用interrupt()方法,這時候需要使用try-catch子句。

package com21;

/**
 * Created by Panda on 2018/5/22.
 */
class Sleeper extends Thread{
    private int duration;
    public Sleeper(String name,int sleepTime){
        super(name);
        duration=sleepTime;
        start();
    }
    public void run(){
        try{
            sleep(duration);
        }catch (InterruptedException e){
            System.out.println(getName()+" was interrupted. "+"isInterrupted(): "+isInterrupted());
            return;
        }
        System.out.println(getName()+" has awakened");
    }
}
class Joiner extends Thread{
    private Sleeper sleeper;

    public Joiner(String name,Sleeper sleeper) {
        super(name);
        this.sleeper = sleeper;
        start();
    }
    public void run(){
        try{
            sleeper.join();
        }catch (InterruptedException e){
            System.out.println("Interrupted");
        }
        System.out.println(getName()+" join completed");
    }
}
public class Joining {
    public static void main(String[] args) {
        Sleeper sleeper = new Sleeper("Sleepy",1500);
        Sleeper sleeper1 = new Sleeper("Grumpy",1500);
        Joiner joiner = new Joiner("Dopey",sleeper);
        Joiner joiner1 = new Joiner("Doc",sleeper1);
        sleeper1.interrupt();
    }
    /**
     * Grumpy was interrupted. isInterrupted(): false
     Doc join completed
     Sleepy has awakened
     Dopey join completed
     */
}

21.2.13線程組
1、線程組持有一個線程集合。
21.2.14捕獲異常
1、由於線程的本質特性,使得不能捕獲從線程中逃逸的異常。一旦異常逃出任務的run()方法,就會向外傳播到控制檯。
2

package com21;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

/**
 * Created by Panda on 2018/5/23.
 */
class ExceptionThread2 implements Runnable{
    @Override
    public void run() {
        Thread thread = Thread.currentThread();
        System.out.println("run() by"+thread);
        System.out.println("eh= "+thread.getUncaughtExceptionHandler());
        throw new RuntimeException();
    }
}
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("caught "+e);
    }
}

class HandlerThreadFactory implements ThreadFactory{
    @Override
    public Thread newThread(Runnable r) {
        System.out.println(this + " creating new Thread");
        Thread thread = new Thread(r);
        System.out.println("created "+thread);
        thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        System.out.println("eh= "+thread.getUncaughtExceptionHandler());
        return thread;
    }
}

public class CaptureUncaughtException {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool(new HandlerThreadFactory());
        executorService.execute(new ExceptionThread2());
    }
    /**
     * com21.HandlerThreadFactory@1540e19d creating new Thread
     created Thread[Thread-0,5,main]
     eh= com21.MyUncaughtExceptionHandler@677327b6
     run() byThread[Thread-0,5,main]
     eh= com21.MyUncaughtExceptionHandler@677327b6
     com21.HandlerThreadFactory@1540e19d creating new Thread
     created Thread[Thread-1,5,main]
     eh= com21.MyUncaughtExceptionHandler@5c74f769
     caught java.lang.RuntimeException
     */
}

21.3 共享受限資源
21.3.1不正確地訪問資源
2.3.2解決共享資源競爭
1、基本上所有的併發模式在解決線程衝突問題的時候,都是採用序列化訪問共享資源的方案。這意味着在給定時刻只允許一個任務訪問共享資源。通常這是通過在代碼前面加上一條鎖語句來實現的,這就使得在一段時間內只允許一個任務可以運行這段代碼。因爲鎖語句產生了一種互相排斥的效果,所以這種機制常常稱爲互斥量。
2、一個任務可以多次獲得鎖對象的鎖。如果一個方法在同一個對象上調用了第二個方法,後者又調用了同一個對象上的另一個方法,就會發生這種情況。JVN負責跟蹤對象唄加鎖的次數。如果一個對象被加鎖(即鎖被完全釋放),其計數變爲0.在任務第一次給對象加鎖的時候,計數變爲1.每當這個相同的任務在這個對象上獲得鎖時,計數都會遞增。顯然,只有首先獲得了鎖的任務才能允許繼續獲取多個鎖。每當任務離開一個synchronized方法,計數遞減,當計數爲零的時候,鎖被完全釋放,此時別的任務就可以使用此資源。
3、使用顯示的Lock對象。

package com21;



import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by Panda on 2018/5/23.
 */
public class MutexEventGenerator extends IntGenerator{
    private int currentEvenValue=0;
    private Lock lock = new ReentrantLock();
    @Override
    public int next() {
        lock.lock();
        try{
            ++currentEvenValue;
            Thread.yield();
            ++currentEvenValue;
            return currentEvenValue;
        }finally {
          lock.unlock();
        }

    }

    public static void main(String[] args) {
        EvenChecker.test(new MutexEventGenerator());
    }
}

21.3.3原子性與易變性
1、原子性可以應用於除long和double之外的所有基本類型之上的“簡單操作”。對於讀取和寫入除long和double之外的基本類型變量這樣的操作,可以保證它們會被當做不可分(原子)的操作來操作內存。
21.3.4原子類

package com21;

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by Panda on 2018/5/23.
 */
public class AtomicIntegerTest implements Runnable {
    private AtomicInteger integer = new AtomicInteger(0);
    public int getValue(){
        return integer.get();
    }
    public void evenIncrement(){integer.addAndGet(2);}
    @Override
    public void run() {
        while (true){
            evenIncrement();
        }
    }

    public static void main(String[] args) {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("Aborting");
                System.exit(0);
            }
        },5000);
        ExecutorService executorService = Executors.newCachedThreadPool();
        AtomicIntegerTest atomicIntegerTest = new AtomicIntegerTest();
        executorService.execute(atomicIntegerTest);
        while (true){
            int val = atomicIntegerTest.getValue();
            if(val%2!=0){
                System.out.println(val);
                System.exit(0);
            }
        }
    }
}

21.3.5臨界區
1、希望防止多個線程同時訪問方法內部的部分代碼而不是防止訪問整個方法。通過這種方式分離出來的代碼段被稱爲臨界區,它也使用synchronized關鍵字建立。這裏synchronized被用來指定某個對象,此對象的鎖被用來對花括號內的代碼進行同步控制。
2、通過使用同步控制塊,而不是對整個方法進行同步控制,可以使多個任務訪問對象的時間性能顯著提高。
21.3.6在其他對象上同步
1、synchronized塊必須給定一個在其上進行同步的對象,並且最合理的方式是,使用其方法正在被調用的當前對象:synchronized(this)。在這種方式中,如果獲得了synchronized塊上的鎖,那麼該對象其他的synchronized方法和臨界區就不能被調用了。因此,如果在this上同步,臨界區的效果就會直接縮小在同步的範圍內。
21.3.7線程本地存儲
1、線程本地存儲是一種自動化機制,可以爲使用相同變量的每個不同的線程都創建不同的存儲。
2、

package com21;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * Created by Panda on 2018/5/23.
 */
//ThreadLocal 對象通常當作靜態域存儲。在創建ThreadLocal時,只能通過get()和set()方法來訪問該對象的內容
    //get()方法將返回與其線程相關聯的對象的副本
    //set()方法會將參數插入到爲期線程存儲的對象中,並返回存儲中原有的對象。
class Accessor implements Runnable{
    private final int id;

    public Accessor(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()){
            ThreadLocalVariableHolder.increment();
            System.out.println(this);
            Thread.yield();
        }
    }
    public String toString(){
        return "#"+id+":"+ThreadLocalVariableHolder.get();
    }
}
public class ThreadLocalVariableHolder {
    private static ThreadLocal<Integer> value=new ThreadLocal<Integer>(){
      private Random random = new Random(47);
      protected synchronized Integer initialValue(){
          return random.nextInt(10000);
      }
    };
    public static void increment(){
        value.set(value.get()+1);
    }
    public static int get(){return value.get();}

    public static void main(String[] args) throws Exception{
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i <5 ; i++) {
            executorService.execute(new Accessor(i));
        }
        TimeUnit.SECONDS.sleep(3);
        executorService.shutdown();
    }

}

21.4終結任務
21.4.1裝飾性花園
21.4.2在阻塞時終結
1、線程狀態:
①新建(new):當線程被創建時,它只會短暫地處於這種狀態。此時它已經分配了必需的系統資源,並執行了初始化。此刻線程已經有資格獲得CPU時間了,之後調度器將把這個線程轉變爲可運行狀態或阻塞狀態。
②就緒(Runnable):在這種狀態下,只要調度器把時間片分配給線程,線程就可以運行。也就是說,在任意時刻,線程可以運行也可以不運行。只要調度器能分配時間片給線程,它就可以運行,這不同於死亡和阻塞狀態。
③阻塞(Blocked):線程能夠運行,但有某個條件阻止它的運行。當線程處於阻塞狀態時,調度器將忽略線程,不會分配給線程任何CPU時間。直到線程重新進入了就緒狀態,它纔有可能執行操作。
④死亡(Dead):處於死亡或終止狀態的線程將不再是可調度的,並且再也不會得到CPU時間,它的任務已結束,或不再是可運行的。任務死亡的通常方式是從run()方法返回,但是任務的線程還可以被中斷。
2、進入阻塞狀態:
一個任務進入阻塞狀態,可能有如下原因:
①通過調用sleep(milliseconds)使任務進入休眠狀態,在這種情況下,任務在指定的時間內不會運行。
②通過調用wait()使線程掛起。直到線程得到了notify()或notifyAll()消息,線程纔會進入就緒狀態。
③任務在等待某個輸入/輸出完成。
④任務試圖在某個對象上調用其他同步控制方法,但是對象鎖不可用,因爲另一個任務已經獲取了這個鎖。
21.4.3中斷
1、Thread類包含interrupt()方法,因此可以終止被阻塞的任務,這個方法將設置線程的中斷狀態。如果一個線程已經被阻塞,或者試圖執行一個阻塞操作,那麼設置這個線程的中斷狀態將拋出InterruptedException。當掏出該異常或者該任務調用Thread.interrupted()時,中斷狀態將被複位。
2、

package com21;

import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
 * Created by Panda on 2018/5/23.
 */
class SleepBlocked implements Runnable{
    @Override
    public void run() {
        try{
            TimeUnit.SECONDS.sleep(100);
        }catch (InterruptedException e){
            System.out.println("interruptedException");
        }
        System.out.println("Exiting SleepBlocked.run()");
    }
}
class IOBlocked implements Runnable{
    private InputStream inputStream;

    public IOBlocked(InputStream inputStream) {
        this.inputStream = inputStream;
    }

    @Override
    public void run() {
        try{
            System.out.println("waiting for read() : ");
            inputStream.read();
        }catch (IOException e){
            if(Thread.currentThread().isInterrupted()){
                System.out.println("Interrupted from blocked I/O");
            }else {
                throw  new RuntimeException(e);
            }
        }
        System.out.println("Exiting IOBlocked.run()");
    }
}

class SynchronizedBlocked implements Runnable{
    public synchronized void f(){
        while (true) Thread.yield();
    }
    public SynchronizedBlocked(){
        new Thread(){
            public void run(){
                f();
            }
        }.start();
    }

    @Override
    public void run() {
        System.out.println("Trying to call f()");
        f();
        System.out.println("Exiting SynchronizedBlocked.run()");
    }
}
public class Interrupting {
    private static ExecutorService executorService= Executors.newCachedThreadPool();
    static void test(Runnable t) throws InterruptedException{
        Future<?> f=executorService.submit(t);
        TimeUnit.MILLISECONDS.sleep(100);
        System.out.println("Interrupting  "+t.getClass().getName());
        f.cancel(true);

        System.out.println("Interrupt sent to "+t.getClass().getName() );
    }

    public static void main(String[] args) throws Exception{
        test(new SleepBlocked());
        test(new IOBlocked(System.in));
        test(new SynchronizedBlocked());
        TimeUnit.SECONDS.sleep(3);
        System.out.println("Aborting with System.exit(0)");
        System.exit(0);
    }
    /**
     * Interrupting  com21.SleepBlocked
     Interrupt sent to com21.SleepBlocked
     interruptedException
     Exiting SleepBlocked.run()
     waiting for read() :
     Interrupting  com21.IOBlocked
     Interrupt sent to com21.IOBlocked
     Trying to call f()
     Interrupting  com21.SynchronizedBlocked
     Interrupt sent to com21.SynchronizedBlocked
     Aborting with System.exit(0)
     */

}

3、被互斥所阻塞。如果嘗試着在一個對象上調用其synchronized方法,而這個對象的鎖已經被其他任務獲取,那麼調用任務將被掛起(阻塞),直至這個鎖可獲得。
4、

package com21;

/**
 * Created by Panda on 2018/5/23.
 */
//嘗試在一個對象上調用其synchronized方法,而這個對象的鎖已經被其他任務獲得,那麼調用任務將被掛起(阻塞)
    //直至這個鎖可獲得。
    //同一個互斥如何能被同一個任務多次獲得。
public class MultiLock {
    public synchronized void f1(int count){
        if(count-->0){
            System.out.println("f1() calling f2() with count "+count);
            f2(count);
        }
    }
    public synchronized void f2(int count){
        if(count-->0){
            System.out.println("f2() calling f1() count "+count);
            f1(count);
        }
    }

    public static void main(String[] args) {
        final MultiLock multiLock=new MultiLock();
        new Thread(){
            public void run(){
                multiLock.f1(10);
            }
        }.start();
    }

}
package com21;

import org.omg.CORBA.TIMEOUT;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by Panda on 2018/5/23.
 */
class BlockedMutex{
    private Lock lock = new ReentrantLock();
    public BlockedMutex(){
        lock.lock();
    }
    public void f(){
        try{
            lock.lockInterruptibly();
            System.out.println("lock acquired in f()");
        }catch (InterruptedException e){
            System.out.println("Interrupted from lock acquisition in f()");
        }
    }
}
class Blocked2 implements Runnable{
    BlockedMutex blockedMutex = new BlockedMutex();
    @Override
    public void run() {
        System.out.println("Waiting for f() in BlockedMutex");
        blockedMutex.f();
        System.out.println("Broken out of blocked call");
    }
}
public class Interrupting2 {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Blocked2());
        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.isInterrupted();
    }
}
發佈了64 篇原創文章 · 獲贊 1 · 訪問量 6740
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章