Java 同步器學習筆記

同步器

同步器爲多線程環境下多個線程訪問修改同一個資源時,提供線程同步、互斥、等待等一系列功能。JDK 提供的主要同步器類繼承如下:
Workek 同步器類繼承關係
FairSync類繼承關係

AbstractOwnableSynchronizer 類介紹

AbstractOwnableSynchronizer 類最基本的功能是夠被一個線程獨佔(類名的意義)。AbstractOwnableSynchronizer 有個私有屬性 exclusiveOwnerThread,是獨佔線程的引用,並提供該線程的 get,set 方法。

AbstractQueuedSynchronizer

AbstractQueuedSynchronizer 是同步器的重要實現。AbstractQueuedSynchronizer 是基於先進先出(FIFO)的等待隊列實現的多線程間的同步工具。AbstractQueuedSynchronizer 等待隊列上的線程都會有一個對應的需要原子操作的 int 類型的數值表示線程當前的狀態。AbstractQueuedSynchronizer 包含兩部分內容,一部分是要求子類實現改變狀態的方法,即需要子類實現獲取鎖和釋放鎖的邏輯;另一部分是實現線程進度等待隊列自旋和阻塞,通知喚醒,出等待隊列等功能。

由於 AbstractQueuedSynchronizer 本身提供了大量的 public 方法,並且方法實現的是線程進入等待隊列等保證線程同步的功能,並不一定適合直接暴露給其他類直接使用,所以要求子類是一個 non-public 類型的內部類。

AbstractQueuedSynchronizer 提供了兩種同步策略,分別是獨佔模式和共享模式。獨佔模式只允許一個線程獲取鎖。如果當前已經有一個線程獲取鎖了,那麼其他線程獲取鎖時,都進入等待隊列。共享模式允許多個線程同時獲取鎖,但是不保證線程獲取鎖時一定能夠成功。AbstractQueuedSynchronizer 本身是沒有獲取鎖和釋放鎖的具體實現,但是爲了考慮到默寫情況下,子類可能只需要提供一種策略模式,所以定義的獲取鎖和釋放鎖的方法時 protected,並且方法體只是拋出 UnsupportedOperationException 異常。但是 AbstractQueuedSynchronizer 本身是負責維護等待隊列和通知喚醒,所以一旦線程在共享模式下獲取鎖,AbstractQueuedSynchronizer 需要判斷下一個等待線程是否需要也是需要獲取鎖。AbstractQueuedSynchronizer 只維護一個雙向鏈表作爲等待隊列,所以不同線程使用不同的同步策略時,他們都位於一個等待隊列上。子類如果只需要一種同步策略,那麼只需要實現一種同步策略。

鎖定義了獲取競態條件(Condition)的接口,並且鎖一般都依賴同步器實現許多功能,可能是爲了方便鎖的實現,所以在 AbstractQueuedSynchronizer 中有一個競態條件的實現類 ConditionObject 的內部類。

AbstractQueuedSynchronizer 要求子類實現的內容有

  • tryAcquire 獲取獨佔鎖
  • tryRelease 釋放獨佔鎖鎖
  • tryAcquireShared 獲取共享鎖
  • tryReleaseShared 釋放共享鎖
  • isHeldExclusively 是否是當前線程在持有同步器的獨佔鎖

在使用上述方法時,必須保證線程安全,並且這個方法的執行時間應該要儘可能的短,並且要求不會被阻塞。

線程被AbstractQueuedSynchronizer 丟入等待隊列的操作並不是一個原子操作,能夠成功進入等待隊列的核心邏輯如下:

while (!tryAcquire(arg)) {
    //如果還沒有進隊列則進入隊列
    //根據需要阻塞當前線程
}

while (!tryRelease(arg)) {
    //將隊列第一個元素綁定的線程喚醒 LockSupport.unpack
}

由於在入隊前會處理許多判斷校驗等工作,所以如果不做特殊處理,可能會發生後續進入的線程(進入到等待隊列前進來)會比先進來的線程更早的進入到等待隊列中。不做特殊處理的方式是非公平鎖,而保證先進來的線程會先入隊到等待隊列中的是公平鎖。

AbstractQueuedSynchronizer 提供了一個基礎的但是搞笑並具有擴展性的同步器功能,但是 AbstractQueuedSynchronizer 並不是一個完整的同步器。AbstractQueuedSynchronizer 的一部分功能比如 tryAcquire 等依賴子類實現。

以下是一個文檔中完整同步器的例子:

 class Mutex implements Lock, java.io.Serializable {

  // Our internal helper class
  private static class Sync extends AbstractQueuedSynchronizer {
    // Report whether in locked state
    protected boolean isHeldExclusively() {
      return getState() == 1;
    }

    // Acquire the lock if state is zero
    public boolean tryAcquire(int acquires) {
      assert acquires == 1; // Otherwise unused
      if (compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
      }
      return false;
    }

    // Release the lock by setting state to zero
    protected boolean tryRelease(int releases) {
      assert releases == 1; // Otherwise unused
      if (getState() == 0) throw new IllegalMonitorStateException();
      setExclusiveOwnerThread(null);
      setState(0);
      return true;
    }

    // Provide a Condition
    Condition newCondition() { return new ConditionObject(); }

    // Deserialize properly
    private void readObject(ObjectInputStream s)
        throws IOException, ClassNotFoundException {
      s.defaultReadObject();
      setState(0); // reset to unlocked state
    }
  }

  // The sync object does all the hard work. We just forward to it.
  private final Sync sync = new Sync();

  public void lock()                { sync.acquire(1); }
  public boolean tryLock()          { return sync.tryAcquire(1); }
  public void unlock()              { sync.release(1); }
  public Condition newCondition()   { return sync.newCondition(); }
  public boolean isLocked()         { return sync.isHeldExclusively(); }
  public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
  public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
  }
  public boolean tryLock(long timeout, TimeUnit unit)
      throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
  }
}
  • 特別提醒:Mutex 不能直接繼承 AbstractQueuedSynchronizer,因爲 AbstractQueuedSynchronizer 有太多的同步器功能相關的 public 方法,不適合暴露出去。所以 Mutex 有一個內部類 Sync,繼承 AbstractQueuedSynchronizer,實現 tryAcquire 等 AbstractQueuedSynchronizer 未提供的功能。

AbsctractQueuedSynchronizer 屬性與方法介紹:

public abstract class AbstractQueuedSynchronizer
  extends AbstractOwnableSynchronizer
  implements java.io.Serializable {

  private static final long serialVersionUID = 7373984972572414691L;

  /**
   * 創建一個同步狀態爲 0 (state=0)的同步器
   */
  protected AbstractQueuedSynchronizer() { }

  /**
   *
   * Node 是構成同步器等待隊列的基礎數據結構,詳情參考:Condition 筆記關於 Node 部分的說明
   *
   */
  static final class Node {...}

  /**
   * Node 組成的雙向鏈表的第一個元素
   * 初始化 AbstractQueuedSynchronizer 時,head 指向 null。如果需要初始化 AbstractQueuedSynchronizer 時,構
   * 建 head 元素,那麼通過 setHead 方法設置 head 的值。並且保證 waitStatus 不能是取消(CANCELLED)的狀態。
   */
  private transient volatile Node head;

  /**
   * Node 組成的雙向鏈表的最後一個元素。通過進隊列的方法(enq)改變 tail 的值。
   */
  private transient volatile Node tail;

  /**
   * 同步器的狀態
   */
  private volatile int state;

  /**
   * 返回同步器的狀態
   */
  protected final int getState() {
      return state;
  }

  /**
   * 改變同步器的狀態
   */
  protected final void setState(int newState) {
      state = newState;
  }

  /**
   * 比較並修改同步器的狀態。。如果和給定的值相等,那麼將狀態改成具體的值。
   *
   */
  protected final boolean compareAndSetState(int expect, int update) {
      // See below for intrinsics setup to support this
      return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
  }

  /**
   * 自旋時間的閥值。
   * 在阻塞線程前,線程會先嚐試獲取鎖,直到超過這個閥值,使用 LockSupport.park 方法阻塞線程。
   * 線程阻塞和喚醒需要一定的資源開銷,如果能夠在自旋期間獲得鎖將節省這部分的時間開銷。
   * 但是自旋代表線程利用計算機資源做目標外的事情,雖然喚醒線程需要資源開銷,但是一直自旋也需要浪費當前的 CPU 資源,
   * 所以自旋的時間也不建議很長。
   */
  static final long spinForTimeoutThreshold = 1000L;

  /**
   * 將 Node 元素加入到列表中。這是一個先進先出隊列,所以加入的元素都是添加到隊尾。
   */
  private Node enq(final Node node) {
      for (;;) {
          Node t = tail;
          if (t == null) { // Must initialize
              if (compareAndSetHead(new Node()))
                  tail = head;
          } else {
              node.prev = t;
              if (compareAndSetTail(t, node)) {
                  t.next = node;
                  return t;
              }
          }
      }
  }

  /**
   * 根據給定的策略並關聯當前線程創建一個元素,並將元素入隊。
   */
  private Node addWaiter(Node mode) {
      Node node = new Node(Thread.currentThread(), mode);
      // Try the fast path of enq; backup to full enq on failure
      Node pred = tail;
      if (pred != null) {
          node.prev = pred;
          if (compareAndSetTail(pred, node)) {
              pred.next = node;
              return node;
          }
      }
      enq(node);
      return node;
  }

  /**
   * 設置隊列的第一個元素,並且將元素的相關線程和前一個元素置空。置空的目的一來是提醒 GC 回收,
   * 二來是廢除不必要的隊列相關的操作。
   *
   * @param node the node
   */
  private void setHead(Node node) {
      head = node;
      node.thread = null;
      node.prev = null;
  }

  /**
   * 喚醒後續元素綁定的線程。
   *
   * @param node the node
   */
  private void unparkSuccessor(Node node) {
      /*
       * 如果 waitStatus 是負數(通常表示需要告訴鏈表的後續元素當前元素已經喚醒了)那麼將元素的 waitStatus 改成 0。
       * 如果當前操作沒有修改成功,或者被後續元素綁定的線程修改了 waitStatus 的值,對同步器會不造成功能上的影響。
       */
      int ws = node.waitStatus;
      if (ws < 0)
          compareAndSetWaitStatus(node, ws, 0);

      /*
       * 喚醒後續元素綁定的線程。如果當前元素指定的下一個元素不存在,那麼從鏈表的結尾往回回溯一個 waitStatus<=0 的元素,
       * 並將它綁定的線程喚醒
       */
      Node s = node.next;
      if (s == null || s.waitStatus > 0) {
          s = null;
          for (Node t = tail; t != null && t != node; t = t.prev)
              if (t.waitStatus <= 0)
                  s = t;
      }
      if (s != null)
          LockSupport.unpark(s.thread);
  }

  /**
   * 共享模式下釋放鎖。
   */
  private void doReleaseShared() {
      /*
       * 需要確保將 head 狀態置爲 PROPAGATE
       */
      for (;;) {
          Node h = head;
          if (h != null && h != tail) {
              int ws = h.waitStatus;
              if (ws == Node.SIGNAL) {
                  /**
                   * 將狀態置爲 0。如果設置失敗,表示其他線程已經將狀態設置爲其他狀態了,那麼直接重新循環檢查。
                   */
                  if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                      continue;            // loop to recheck cases
                  /**
                   * 如果狀態設置成功,喚醒後續元素
                   */
                  unparkSuccessor(h);
              }
              else if (ws == 0 &&
                       !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                  /**
                   * ws == 0 意味着第一個元素的狀態已經被其他線程置爲 0,並且後續元素已經被通知喚醒或者沒有後續元素。
                   * 如果設置失敗則重新循環檢查。
                   */
                  continue;                // loop on failed CAS
          }
          if (h == head)                   // loop if head changed
              break;
      }
  }

  /**
   * 將原來的隊列第一個元素出隊列,並將指定的元素設置爲隊列第一個元素
   *
   * @param node the node
   * @param propagate the return value from a tryAcquireShared
   */
  private void setHeadAndPropagate(Node node, int propagate) {
      Node h = head; // Record old head for check below
      setHead(node);
      /*
       * 如果 tryAcquireShared 的結果大於 0(表示共享鎖還被其他的線程持有),或者隊列第一個元素已經被其他線程標記爲空,
       * 或者隊列第一個元素的狀態是傳播或者通知時,需要喚醒後續元素。
       *
       */
      if (propagate > 0 || h == null || h.waitStatus < 0 ||
          (h = head) == null || h.waitStatus < 0) {
          Node s = node.next;
          if (s == null || s.isShared())
              doReleaseShared();
      }
  }

  /**
   * 取消獲取鎖
   *
   * @param node the node
   */
  private void cancelAcquire(Node node) {
      if (node == null)
          return;

      node.thread = null;

      //過濾掉排在當前元素前面的已經被取消的元素
      Node pred = node.prev;
      while (pred.waitStatus > 0)
          node.prev = pred = pred.prev;

      //後續 CAS 修改需要使用到
      Node predNext = pred.next;

      //將當前元素的狀態置爲取消,後續準備從出隊列。
      node.waitStatus = Node.CANCELLED;

      if (node == tail && compareAndSetTail(node, pred)) {
          //如果當前元素在隊尾,那麼將隊列從等待隊列中清除,並將該元素的上一個元素置爲隊尾。
          compareAndSetNext(pred, predNext, null);
      } else {
          // 如果後續元素需要被喚醒,將元素出隊列(重新設置上一個元素和下一個元素的上下元素指針的值)
          //並設置元上一個元素的狀態爲 SIGNAL 或者喚醒後續元素
          int ws;
          if (pred != head &&
              ((ws = pred.waitStatus) == Node.SIGNAL ||
               (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
              pred.thread != null) {
              //只有 head 或者被 cancel 的元素綁定的線程爲 null
              Node next = node.next;
              if (next != null && next.waitStatus <= 0)
                  //如果元素還有後續的元素,那麼元素出隊列。
                  compareAndSetNext(pred, predNext, next);
          } else {
              //喚醒後續的元素
              unparkSuccessor(node);
          }

          node.next = node; // help GC
      }
  }

  /**
   * 如果節點競爭鎖失敗,那麼檢查等待隊列的狀態,並在符合條件的情況下修改狀態的值成 SIGNAL。
   * 如果判斷線程應該被阻塞,那麼返回 true;否則返回 false。
   * 只有前一個元素是隊列的第一個元素才能競爭到鎖。但是隊列第二個元素不一定能夠確定競爭到鎖,
   *
   * @param pred node's predecessor holding status
   * @param node the node
   * @return {@code true} if thread should block
   */
  private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
      //當前節點的上一個節點的狀態
      int ws = pred.waitStatus;
      if (ws == Node.SIGNAL)
          /*
           * 如果前一個元素的狀態是 SIGNAL 表明可以安全掛起,那麼直接返回 true。
           */
          return true;
      if (ws > 0) {
          /*
           * 將取消狀態的元素從隊列中刪除。
           */
          do {
              node.prev = pred = pred.prev;
          } while (pred.waitStatus > 0);
          //node.prev 在循環中已經更新
          pred.next = node;
      } else {
          /*
           * 等待隊列的狀態應該是 0(初始化的狀態)或者是 PROPAGATE(傳播),表示在沒被阻塞錢前等待喚醒通知。
           * 調用方法的線程需要保證在阻塞前不能獲取鎖。
           */
          compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
      }
      return false;
  }

  /**
   * 阻塞當前線程並返回線程中斷標誌。
   *
   * @return {@code true} if interrupted
   */
  private final boolean parkAndCheckInterrupt() {
      LockSupport.park(this);
      return Thread.interrupted();
  }

  /**
   * 獨佔模式下獲取鎖。該方法不響應線程中斷,但是會把線程中斷標誌返回。
   *
   * @param node the node
   * @param arg the acquire argument
   * @return {@code true} if interrupted while waiting
   */
  final boolean acquireQueued(final Node node, int arg) {
      boolean failed = true;
      try {
          boolean interrupted = false;
          //
          for (;;) {
              final Node p = node.predecessor();
              //只有前一元素時隊列第一個元素時纔去獲取鎖。
              //在非公平鎖的情況下,儘管是隊列第二個元素,但是不一定能夠獲取到鎖。如果在獲取鎖的過程中,外部有一個方法調用獲取鎖成功,那麼當前線程還是需要繼續掛起。
              if (p == head && tryAcquire(arg)) {
                  setHead(node);
                  p.next = null; // help GC
                  failed = false;
                  return interrupted;
              }

              if (shouldParkAfterFailedAcquire(p, node) &&
                  parkAndCheckInterrupt())
                  //線程被中斷,將中斷標誌設置爲 true。但是由於立刻自旋重新走 for 循環中的流程,所以線程中斷不會產生影響
                  interrupted = true;
          }
      } finally {
          //如果在獲取鎖時報錯,那麼將當前元素出隊
          if (failed)
              cancelAcquire(node);
      }
  }

  /**
   * 可以響應中斷的獲取鎖
   * @param arg the acquire argument
   */
  private void doAcquireInterruptibly(int arg)
      throws InterruptedException {
      final Node node = addWaiter(Node.EXCLUSIVE);
      boolean failed = true;
      try {
          for (;;) {
              final Node p = node.predecessor();
              if (p == head && tryAcquire(arg)) {
                  setHead(node);
                  p.next = null; // help GC
                  failed = false;
                  return;
              }
              if (shouldParkAfterFailedAcquire(p, node) &&
                  parkAndCheckInterrupt())
                  //LockSuport.park 可以響應中斷。一旦線程中斷後,線程從阻塞中恢復,那麼拋出異常。
                  throw new InterruptedException();
          }
      } finally {
          if (failed)
              cancelAcquire(node);
      }
  }

  /**
   * 在響應中斷獲取鎖的基礎上增加等待超時功能
   *
   * @param arg the acquire argument
   * @param nanosTimeout max wait time
   * @return {@code true} if acquired
   */
  private boolean doAcquireNanos(int arg, long nanosTimeout)
          throws InterruptedException {
      if (nanosTimeout <= 0L)
          return false;
      final long deadline = System.nanoTime() + nanosTimeout;
      final Node node = addWaiter(Node.EXCLUSIVE);
      boolean failed = true;
      try {
          for (;;) {
              final Node p = node.predecessor();
              if (p == head && tryAcquire(arg)) {
                  setHead(node);
                  p.next = null; // help GC
                  failed = false;
                  return true;
              }
              nanosTimeout = deadline - System.nanoTime();
              if (nanosTimeout <= 0L)
                  return false;
              //在沒有超過自旋的時間閥值前一直自旋。
              if (shouldParkAfterFailedAcquire(p, node) &&
                  nanosTimeout > spinForTimeoutThreshold)
                  LockSupport.parkNanos(this, nanosTimeout);
              if (Thread.interrupted())
                  throw new InterruptedException();
          }
      } finally {
          if (failed)
              cancelAcquire(node);
      }
  }

  /**
   * 共享模式下,不響應中斷情況下獲取鎖
   * 獨佔模式下,如果有一個線程從獲取到鎖開始到釋放鎖結束,整個等待隊列只有自旋嘗試獲取鎖的爲入隊狀態和入隊了都是等待獲取鎖的狀態
   * 共享模式下如果獲得鎖,會檢查後續元素是否也是共享鎖,如果是直接喚醒。
   * @param arg the acquire argument
   */
  private void doAcquireShared(int arg) {
      final Node node = addWaiter(Node.SHARED);
      boolean failed = true;
      try {
          boolean interrupted = false;
          for (;;) {
              final Node p = node.predecessor();
              if (p == head) {
                  int r = tryAcquireShared(arg);
                  if (r >= 0) {
                      setHeadAndPropagate(node, r);
                      p.next = null; // help GC
                      if (interrupted)
                          selfInterrupt();
                      failed = false;
                      return;
                  }
              }
              if (shouldParkAfterFailedAcquire(p, node) &&
                  parkAndCheckInterrupt())
                  interrupted = true;
          }
      } finally {
          if (failed)
              cancelAcquire(node);
      }
  }

  /**
   * 獨佔模式下,不響應中斷獲取鎖。
   * 先嚐試獲取鎖(tryAcquire -  具體的子類實現),如果失敗添加元素到隊列中(addWaiter),
   * 然後判斷剛創建的元素能否持有鎖(acquireQueued)。
   * 如果線程被中斷,那麼設置當前線程爲中斷狀態(selfInterrupt)。
   *
   * @param arg the acquire argument.  This value is conveyed to
   *        {@link #tryAcquire} but is otherwise uninterpreted and
   *        can represent anything you like.
   */
  public final void acquire(int arg) {
      if (!tryAcquire(arg) &&
          acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
          selfInterrupt();
  }

  /**
   * 獨佔模式下釋放鎖。
   * 釋放獨佔鎖(tryRelease - 具體的子類實現),喚醒隊列中的後續元素。
   *
   * @param arg the release argument.  This value is conveyed to
   *        {@link #tryRelease} but is otherwise uninterpreted and
   *        can represent anything you like.
   * @return the value returned from {@link #tryRelease}
   */
  public final boolean release(int arg) {
      if (tryRelease(arg)) {
          Node h = head;
          if (h != null && h.waitStatus != 0)
              unparkSuccessor(h);
          return true;
      }
      return false;
  }

  /**
   * 共享模式下獲取鎖。
   * 獲取共享模式下的鎖(tryAcquireShared - 具體的子類實現),如果獲取失敗則在不響應中斷的情況下獲取
   * 共享鎖(doAcquireShared)。
   *
   * @param arg the acquire argument.  This value is conveyed to
   *        {@link #tryAcquireShared} but is otherwise uninterpreted
   *        and can represent anything you like.
   */
  public final void acquireShared(int arg) {
      if (tryAcquireShared(arg) < 0)
          doAcquireShared(arg);
  }

  /**
   * 共享模式下釋放鎖
   * 嘗試釋放鎖(tryReleaseShared),如果成功通知後續元素(doReleaseShared)
   *
   * @param arg the release argument.  This value is conveyed to
   *        {@link #tryReleaseShared} but is otherwise uninterpreted
   *        and can represent anything you like.
   * @return the value returned from {@link #tryReleaseShared}
   */
  public final boolean releaseShared(int arg) {
      if (tryReleaseShared(arg)) {
          doReleaseShared();
          return true;
      }
      return false;
  }
}

AbstractQueuedSynchronizer 子類實現的方法介紹

AbstractQueuedSynchronizer 的子類非常多,比如 ReentrantLock.Sync,ThreadPoolExecutor.Worker 等。這裏只羅列實現了 AbstractQueuedSynchronizer 本身不提供實現細節要求子類實現的 ReentrantLock.FairSync 類,關於這部分方法的實現說明。

tryAcquire

代碼如下:

/**
 * 獨佔策略下,以公平的方式(先到先得)獲取鎖
 */
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //c == 0表示鎖可以被獲取
    if (c == 0) {
        //判斷是否有前驅元素,如果有則獲取鎖失敗。hasQueuedPredecessors是保證公平鎖的關鍵功能。
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //重入鎖的特定,可以被多個線程持有。
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

tryRelease

ReentrantLock.Sync 的 tryRelease實現如下:

/**
 * 釋放鎖。只有當所有線程都釋放鎖纔會返回true,否則只是減少狀態的值。
 */
protected final boolean tryRelease(int releases) {
     int c = getState() - releases;
     if (Thread.currentThread() != getExclusiveOwnerThread())
         throw new IllegalMonitorStateException();
     boolean free = false;
     if (c == 0) {
         free = true;
         setExclusiveOwnerThread(null);
     }
     setState(c);
     return free;
 }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章