第七章、核心5:Thread和Object類中線程相關方法(wait/notify、sleep、join、yield)

1、方法概覽

方法名 簡介
Thread sleep相關 本表格的“相關”,指的是重載方法,如sleep有多個重載方法,但實際作用大同小異
. join 主線程等待ThreaA執行完畢(ThreadA.join())
. yield相關 放棄已經獲取到的CPU資源
. currentThread 獲取當前執行線程的引用
. start,run相關 啓動線程相關
. interrupt相關 中斷線程
. stop(),suspend(),resuem()相關 已廢棄
Object wait/notify/notifyAll相關 讓線程暫時休息和喚醒

2、wait,notify,notifyAll方法詳解

2.1、作用、用法:阻塞階段、喚醒階段、遇到中斷

(1)阻塞階段

線程調用wait()方法,則該線程進入到阻塞狀態,直到以下4種情況之一發生時,纔會被喚醒

  • 另一個線程調用這個對象的notify()方法且剛好被喚醒的是本線程
  • 另一個線程調用這個對象的notifyAll()方法且剛好被喚醒的是本線程
  • 過了wait(long timeout)規定的超時時間,如果傳入0就是永久等待
  • 線程自身調用了interrupt

(2)喚醒階段

  • notify會喚起單個在等待某對象monitor的線程,如果有多個線程在等待,則只會喚起其中隨機的一個
  • notifyAll會將所有等待的線程都喚起,而喚起後具體哪個線程會獲得monitor,則看操作系統的調度
  • notify必須在synchronized中調用,否則會拋出異常
java.lang.IllegalMonitorStateException
	at java.lang.Object.notify(Native Method)
	at BlockedWaitingTimedWaiting.run(BlockedWaitingTimedWaiting.java:37)
	at java.lang.Thread.run(Thread.java:748)

(3)遇到中斷

  • 假設線程執行了wait(),在此期間被中斷,則會拋出interruptException,同時釋放已經獲取到的monitor

2.2、代碼演示:4種情況

(1)普通用法

/**
 * Wait
 *
 * @author venlenter
 * @Description: 展示wait和notify的基本用法
 * 1. 研究代碼執行順序
 * 2. 證明wait釋放鎖
 * @since unknown, 2020-04-09
 */
public class Wait {
    public static Object object = new Object();

    static class Thread1 extends Thread {
        @Override
        public void run() {
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + "開始執行了");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("線程" + Thread.currentThread().getName() + "獲取到了鎖");

            }
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            synchronized (object) {
                object.notify();
                System.out.println("線程" + Thread.currentThread().getName() + "調用了notify()");
            }
        }
    }
    
    public static void main(String[] args) {
        Thread thread1 = new Thread1();
        Thread thread2 = new Thread2();
        thread1.start();
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();
    }
}
//輸出結果
Thread-0開始執行了
線程Thread-1調用了notify()
線程Thread-0獲取到了鎖

//解析
①Thread-0進入Thread1類synchronized代碼塊,獲得鎖,輸出“Thread-0開始執行”
②然後Thread-0執行object.wait(),釋放了鎖
③Thread-1獲得鎖,進入Thread2類synchronized,執行object.notify(),輸出“線程Thread-1調用了notify()”,同時Thread-0也被喚醒了
④Thread-0回到object.wait()的位置,執行下面的代碼邏輯,輸出“線程Thread-0獲取到了鎖”

(2)notify和notifyAll展示

/**
 * WaitNotifyAll
 *
 * @author venlenter
 * @Description: 3個線程,線程1和線程2首先被阻塞,線程3喚醒它們。notify,notifyAll
 * start先執行不代表線程先啓動
 * @since unknown, 2020-04-11
 */
public class WaitNotifyAll implements Runnable{
    private static final Object resourceA = new Object();
    @Override
    public void run() {
        synchronized(resourceA) {
            System.out.println(Thread.currentThread().getName() + " get resourceA lock");
            try {
                System.out.println(Thread.currentThread().getName() + " wait to start");
                resourceA.wait();
                System.out.println(Thread.currentThread().getName() + "'s waiting end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable r = new WaitNotifyAll();
        Thread threadA = new Thread(r);
        Thread threadB = new Thread(r);
        Thread threadC = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA) {
                    resourceA.notifyAll();
                    //resourceA.notify();
                    System.out.println("ThreadC notifyed.");
                }
            }
        });
        threadA.start();
        threadB.start();
        Thread.sleep(200);
        threadC.start();
    }
}
//輸出結果
Thread-0 get resourceA lock
Thread-0 wait to start
Thread-1 get resourceA lock
Thread-1 wait to start
ThreadC notifyed.
Thread-1's waiting end
Thread-0's waiting end

(3)只釋放當前monitor展示

/**
 * WaitNotifyReleaseOwnMonitor
 *
 * @author venlenter
 * @Description: 證明wait只釋放當前的那把鎖
 * @since unknown, 2020-04-11
 */
public class WaitNotifyReleaseOwnMonitor {
    private static volatile Object resourceA = new Object();
    private static volatile Object resourceB = new Object();

    public static void main(String[] args) {
        Thread thread1  = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA) {
                    System.out.println("ThreadA got resourceA lock.");
                    synchronized (resourceB) {
                        System.out.println("ThreadA got resourceB lock.");
                        try {
                            System.out.println("ThreadA releases resourceA lock.");
                            resourceA.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resourceA) {
                    System.out.println("ThreadB got resourceA lock.");
                    System.out.println("ThreadB tries to resourceB lock.");
                    synchronized (resourceB) {
                        System.out.println("ThreadB got resourceB lock.");
                    }
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}
//輸出結果
ThreadA got resourceA lock.
ThreadA got resourceB lock.
ThreadA releases resourceA lock.
ThreadB got resourceA lock.
ThreadB tries to resourceB lock.
//沒有打印ThreadB got resourceB lock.(因爲只調用了A.wait,只釋放了lockA,B還沒佔用着)

2.3、特點、性質

  • 使用的時候必須先擁有monitor(synchronized鎖)
  • notify只能喚醒其中一個
  • 屬於Object類

2.4、原理

2.4.1 手寫生產者消費者設計模式

  •  
  •  
  • 什麼是生產者消費者模式
/**
 * ProducerConsumerModel
 *
 * @author venlenter
 * @Description: 用wait/notify來實現
 * @since unknown, 2020-04-11
 */
public class ProducerConsumerModel {
    public static void main(String[] args) {
        EventStorage eventStorage = new EventStorage();
        Producer producer = new Producer(eventStorage);
        Consumer consumer = new Consumer(eventStorage);
        new Thread(producer).start();
        new Thread(consumer).start();
    }
}

class Producer implements Runnable {
    private EventStorage storage;

    public Producer(EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            storage.put();
        }
    }
}

class Consumer implements Runnable {
    private EventStorage storage;

    public Consumer(EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            storage.take();
        }
    }
}

class EventStorage {
    private int maxSize;
    private LinkedList<Date> storage;

    public EventStorage() {
        maxSize = 10;
        storage = new LinkedList<>();
    }

    public synchronized void put() {
        while (storage.size() == maxSize) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        storage.add(new Date());
        System.out.println("倉庫中已經有" + storage.size() + "個產品。");
        notify();
    }

    public synchronized void take() {
        while (storage.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("拿到了" + storage.poll() + ",現在倉庫還剩下" + storage.size());
        notify();
    }
}
//輸出結果
倉庫中已經有1個產品。
倉庫中已經有2個產品。
倉庫中已經有3個產品。
倉庫中已經有4個產品。
倉庫中已經有5個產品。
倉庫中已經有6個產品。
倉庫中已經有7個產品。
倉庫中已經有8個產品。
倉庫中已經有9個產品。
倉庫中已經有10個產品。
拿到了Sun Apr 12 11:07:50 CST 2020,現在倉庫還剩下9
拿到了Sun Apr 12 11:07:50 CST 2020,現在倉庫還剩下8
拿到了Sun Apr 12 11:07:50 CST 2020,現在倉庫還剩下7
拿到了Sun Apr 12 11:07:50 CST 2020,現在倉庫還剩下6
拿到了Sun Apr 12 11:07:50 CST 2020,現在倉庫還剩下5
拿到了Sun Apr 12 11:07:50 CST 2020,現在倉庫還剩下4
拿到了Sun Apr 12 11:07:50 CST 2020,現在倉庫還剩下3
拿到了Sun Apr 12 11:07:50 CST 2020,現在倉庫還剩下2
拿到了Sun Apr 12 11:07:50 CST 2020,現在倉庫還剩下1
拿到了Sun Apr 12 11:07:50 CST 2020,現在倉庫還剩下0
倉庫中已經有1個產品。
拿到了Sun Apr 12 11:07:50 CST 2020,現在倉庫還剩下0

2.5、注意點

2.6、常見面試問題

2.6.1 兩個線程交替打印0~100的奇偶數

  • 基本方式:用synchronized關鍵字實現
/**
 * WaitNotifyPrintOddEvenSyn
 *
 * @author venlenter
 * @Description: 兩個線程交替打印0~100的奇偶數,用synchronized關鍵字實現
 * @since unknown, 2020-04-12
 */
public class WaitNotifyPrintOddEvenSyn {
    public static int count = 0;
    public static final Object lock = new Object();

    //新建2個線程
    //1個只處理偶數,第二個只處理奇數(用位運算)
    //用synchronized來通信
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (count < 100) {
                    synchronized (lock) {
                        if ((count & 1) == 0) {
                            System.out.println((Thread.currentThread().getName() + ":" + count++));
                        }
                    }
                }
            }
        }, "偶數").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (count < 100) {
                    synchronized (lock) {
                        if ((count & 1) == 1) {
                            System.out.println(Thread.currentThread().getName() + ":" + count++);
                        }
                    }
                }
            }
        }, "奇數").start();
    }
}
//輸出結果
//輸出正確,但是實際上如果thread1(偶數線程)一直支持lock,會有不斷循環做無效的操作
偶數:0
奇數:1
偶數:2
奇數:3
...
奇數:99
偶數:100
  • 更好的方法:wait/notify
/**
 * WaitNotifyPrintOddEvenWait
 *
 * @author venlenter
 * @Description: 兩個線程交替打印0~100的奇偶數,用wait和notify
 * @since unknown, 2020-04-12
 */
public class WaitNotifyPrintOddEvenWait {
    private static int count = 0;
    private static Object lock = new Object();
    //1. 拿到鎖,我們就打印
    //2. 打印完,喚醒其他線程,自己就休眠
    static class TurningRunner implements Runnable {
        @Override
        public void run() {
            while (count <= 100) {
                synchronized (lock) {
                    //拿到鎖就打印
                    System.out.println(Thread.currentThread().getName() + ":" + count++);
                    lock.notify();
                    if (count <= 100) {
                        try {
                            //如果任務還沒結束,就讓出當前線程,並休眠
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new TurningRunner(),"偶數").start();
        Thread.sleep(100);
        new Thread(new TurningRunner(),"奇數").start();
    }
}
//輸出結果
偶數:0
奇數:1
偶數:2
奇數:3
...
奇數:99
偶數:100

2.6.2 手寫生產者消費者設計模式

2.6.3 爲什麼wait()需要在同步代碼塊內使用,而sleep()不需要

  • 正常邏輯是先執行wait,後續在執行notify喚醒。如果wait/notify不放同步代碼塊,執行wait的時候,線程切換去執行其他任務如notify,導致notify先於wait,就會導致後續切回wait的時候,一直阻塞着,無法釋放,導致死鎖。
  • 而sleep是針對本身的當前線程的,不影響

2.6.4 爲什麼線程通信的方法wait(),notify()和notifyAll被定義在Object類裏?而sleep定義在Thread類裏?

  • wait、notify、notifyAll是鎖級別的操作,屬於Object對象的,而線程實際上是可以持有多把鎖的,如果把wait定義到Thread裏面,就無法做到這麼靈活的控制了

2.6.5 wait方法是屬於Object對象的,那調用Thread.wait會怎麼樣?

  • Thread線程退出的時候,會自動調用notify,這可能不是我們所期望的,所以最好不要用Thread.wait

2.6.6 如何選擇notify還是notifyAll?

  • 參考2.2、代碼演示(2)notify和notifyAll展示
  • notify是喚起一個線程,選擇哪個是隨機的。而notifyAll是喚起所有線程,然後這些線程再次搶去奪鎖

2.6.7 notifyAll之後所有的線程都會再次搶奪鎖,如果某線程搶奪失敗怎麼辦?

  • 實質就跟初始狀態一樣,多個線程搶奪鎖,搶不到的線程就等待,等待上一個線程釋放鎖

2.6.8 用suspend()和resume()來阻塞線程可以嗎?爲什麼?

  • 這2個方法由於不安全,已經被棄用了。最好還是使用wait和notify

3、sleep方法詳解

3.1 作用:我只想讓線程在預期的時間執行,其他時候不要佔用CPU資源

3.2 不釋放鎖

  • 包括synchronized和lock
/**
 * SleepDontReleaseMonitor
 *
 * @author venlenter
 * @Description: 展示線程sleep的時候不釋放synchronized的monitor,等sleep時間到了以後,正常結束後才釋放鎖
 * @since unknown, 2020-04-15
 */
public class SleepDontReleaseMonitor implements Runnable{
    public static void main(String[] args) {
        SleepDontReleaseMonitor sleepDontReleaseMonitor = new SleepDontReleaseMonitor();
        new Thread(sleepDontReleaseMonitor).start();
        new Thread(sleepDontReleaseMonitor).start();
    }
    @Override
    public void run() {
        syn();
    }

    private synchronized void syn() {
        System.out.println("線程" + Thread.currentThread().getName() + "獲取到了monitor");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("線程" + Thread.currentThread().getName() + "退出了同步代碼塊");
    }
}
//輸出結果
線程Thread-0獲取到了monitor
線程Thread-0退出了同步代碼塊(5s後出現)
線程Thread-1獲取到了monitor
線程Thread-1退出了同步代碼塊(5s後出現)
/**
 * SleepDontReleaseLock
 *
 * @author venlenter
 * @Description: 演示sleep不釋放lock(lock需要手動釋放)
 * @since unknown, 2020-04-15
 */
public class SleepDontReleaseLock implements Runnable {
    private static final Lock lock = new ReentrantLock();

    @Override
    public void run() {
        lock.lock();
        System.out.println("線程" + Thread.currentThread().getName() + "獲取到了lock");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        System.out.println("線程" + Thread.currentThread().getName() + "釋放了lock");
    }

    public static void main(String[] args) {
        SleepDontReleaseLock sleepDontReleaseLock = new SleepDontReleaseLock();
        new Thread(sleepDontReleaseLock).start();
        new Thread(sleepDontReleaseLock).start();
    }
}
//輸出結果
線程Thread-0獲取到了lock
線程Thread-0釋放了lock(5s後)
線程Thread-1獲取到了lock
線程Thread-1釋放了lock(5s後)
  • 和wait不同

3.3 sleep方法響應中斷

  • 拋出InterruptedException
  • 清除中斷狀態
/**
 * SleepInterrupted
 *
 * @author venlenter
 * @Description: 每隔1s輸出當前時間,被中斷,觀察
 * Thread.sleep()
 * TimeUnit.SECONDS.sleep()
 * @since unknown, 2020-04-15
 */
public class SleepInterrupted implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(new Date());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                System.out.println("我被中斷了");
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new SleepInterrupted());
        thread.start();
        Thread.sleep(6500);
        thread.interrupt();
    }
}
//輸出結果
Wed Apr 15 23:09:55 CST 2020
Wed Apr 15 23:09:56 CST 2020
Wed Apr 15 23:09:57 CST 2020
Wed Apr 15 23:09:58 CST 2020
Wed Apr 15 23:09:59 CST 2020
Wed Apr 15 23:10:00 CST 2020
Wed Apr 15 23:10:01 CST 2020
我被中斷了
java.lang.InterruptedException: sleep interrupted
Wed Apr 15 23:10:01 CST 2020
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.threadobjectclasscommonmethods.SleepInterrupted.run(SleepInterrupted.java:21)
	at java.lang.Thread.run(Thread.java:748)
Wed Apr 15 23:10:02 CST 2020
Wed Apr 15 23:10:03 CST 2020

3.4 sleep總結

  • sleep方法可以讓線程進入Waiting狀態,並且不佔用CPU資源
  • 但是不釋放鎖,直到規定時間後再執行
  • 休眠期間如果被中斷,會拋出異常並清除中斷狀態

3.5 sleep常見面試問題

wait/notify、sleep異同(方法屬於哪個對象?線程狀態怎麼切換?)

(1)相同

  • 都會阻塞
  • 都可以響應中斷
外層執行thread.interrupt()
try {
    wait();
    Thread.sleep();
} catch (InterruptedException e) {
    e.printStackTrace();
}

(2)不同

  • wait/notify需要在synchronized方法中,而sleep不需要
  • 釋放鎖:wait會釋放鎖,而sleep不釋放鎖
  • 指定時間:sleep必須傳參時間,而wait有多個構造方法,不傳時間則直到自己被喚醒
  • 所屬類:wait/notify是Object方法,sleep是Thread類的方法

4、join方法

4.1 作用:因爲新的線程加入了“我們”,所以“我們”要等他執行完再出發

4.2 用法:(在main方法中thread1.join)main等待thread1執行完畢,注意誰等誰(父等待子)

4.3 三個例子

  • 普通用法
/**
 * Join
 *
 * @author venlenter
 * @Description: 演示join,注意語句輸出順序,會變化
 * @since unknown, 2020-04-15
 */
public class Join {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "執行完畢");
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "執行完畢");
            }
        });
        thread1.start();
        thread2.start();
        System.out.println("開始等待子線程運行完畢");
        thread1.join();
        thread2.join();
        System.out.println("所有子線程執行完畢");
    }
}
//輸出結果
開始等待子線程運行完畢
Thread-0執行完畢
Thread-1執行完畢
所有子線程執行完畢
  • 遇到中斷
/**
 * JoinInterrupt
 *
 * @author venlenter
 * @Description: 演示join期間被中斷的效果
 * @since unknown, 2020-04-21
 */
public class JoinInterrupt {
    public static void main(String[] args) {
        Thread mainThread = Thread.currentThread();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    mainThread.interrupt();
                    Thread.sleep(5000);
                    System.out.println("Thread1 finished.");
                } catch (InterruptedException e) {
                    System.out.println("子線程中斷");
                }
            }
        });
        thread1.start();
        System.out.println("等待子線程運行完畢");
        try {
            thread1.join();
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "主線程中斷了");
            thread1.interrupt();
        }
        System.out.println("子線程已運行完畢");
    }
}
//輸出結果
等待子線程運行完畢
main主線程中斷了
子線程已運行完畢
子線程中斷
  • 在join期間,線程到底是什麼狀態?:Waiting
/**
 * JoinThreadState
 *
 * @author venlenter
 * @Description: 先join再mainThread.getState()
 * 通過debugger看線程join前後狀態的對比
 * @since unknown, 2020-04-22
 */
public class JoinThreadState {
    public static void main(String[] args) throws InterruptedException {
        Thread mainThread = Thread.currentThread();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                    System.out.println(mainThread.getState());
                    System.out.println("Thread-0運行結束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        System.out.println("等待子線程運行完畢");
        thread.join();
        System.out.println("子線程運行完畢");
    }
}
//輸出結果
等待子線程運行完畢
WAITING
Thread-0運行結束
子線程運行完畢

4.4 可以使用封裝工具類:CountDownLatch或CyclicBarrier

4.5 join原理

  • 源碼
(1)thread.join();
(2)
public final void join() throws InterruptedException {
        join(0);
    }
(3)
 public final synchronized void join(long millis)
    throws InterruptedException {
        ...
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        }
  • 分析:線程在run執行完成後,JVM底層會自動調用一個notifyAll喚醒,所以即使在join()內沒有notify顯示調用,執行完run()後,也會喚醒
  • 等價
//        thread.join();   等價於下面synchronized的代碼
        synchronized (thread) {
            thread.wait();
        }

4.6 常見面試問題

  • 在join期間,線程處於哪種線程狀態?Waiting

5、yield方法

  • 作用:釋放我的CPU時間片。線程狀態仍然是RUNNABLE,不釋放鎖,也不阻塞
  • 定位:JVM不保證遵循yield邏輯
  • yield和sleep區別:yield隨時可能再次被調度

6、獲取當前執行線程的引用:Thread.currentThread()方法

  • 同一個方法,不同線程會打印出各自線程的名稱
/**
 * CurrentThread
 *
 * @author venlenter
 * @Description: 演示打印majn, Thread-0, Thread-1
 * @since unknown, 2020-04-22
 */
public class CurrentThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        new CurrentThread().run();
        new Thread(new CurrentThread()).start();
        new Thread(new CurrentThread()).start();
    }
}
//輸出
main
Thread-0
Thread-1

筆記來源:慕課網悟空老師視頻《Java併發核心知識體系精講》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章