淺析:線程安全

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"思考:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一共有"},{"type":"text","marks":[{"type":"strong"}],"text":"哪幾類"},{"type":"text","text":"線程安全問題"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那些場景需要額外注意線程安全問題 "}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"什麼是多線程帶來的上下文切換?"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"什麼是多線程的上下文切換? "}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"線程安全"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"什麼是線程安全"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/70/70f27f1bd67ef33c24ba0f9f13fff2e1.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不管業務中遇到怎樣的多個線程訪問某對象或某方法的情況,而在編程這個業務邏輯的時候,"},{"type":"text","marks":[{"type":"strong"}],"text":"都不需要額外做任何額外的處理"},{"type":"text","text":"(也就是可以像單線程編程一樣),程序也可以正常運行(不會因爲多線程而出錯),就可以稱爲線程安全。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"主要是兩個問題"}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"數據爭用:數據讀寫由於同時寫,會造成錯誤數據"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"競爭條件:即使不是同時寫造成的錯誤數據,由於順序原因依然會造成錯誤,例如在寫入前就讀取了"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"如何避免線程安全問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"運行結果錯誤:a++ 多線程下出現消失的請求現象"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"活躍性問題:死鎖、活鎖、飢餓"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對象發佈和初始化的時候的安全問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"a++ 問題"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class MultiThreadsError implements Runnable {\n\n int index = 0;\n static MultiThreadsError instance = new MultiThreadsError();\n\n public static void main(String[] args) throws InterruptedException {\n Thread thread1 = new Thread(instance);\n Thread thread2 = new Thread(instance);\n\n thread1.start();\n thread2.start();\n\n thread1.join();\n thread2.join();\n\n System.out.println(instance.index);\n }\n\n @Override\n public void run() {\n for (int i = 0; i < 10000; i++) {\n index++;\n }\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"運行結果錯誤:沒有原子性"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"a++"}]}]}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d1/d17c428a9a1676d2b5cc6da1dc019bef.png","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"思考:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"如何找到上一個案例中出錯的值"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class MultiThreadsError implements Runnable {\n\n int index = 0;\n static MultiThreadsError instance = new MultiThreadsError();\n\n // 原子計數器功能\n static AtomicInteger realIndex = new AtomicInteger();\n static AtomicInteger wrongIndex = new AtomicInteger();\n\n // 由於線程的執行的先後順序無法確定,所以加入柵欄,讓他們同時出發\n static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);\n // 同時釋放\n static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);\n\n // 使用 boolean 數組標記到錯誤的值\n final boolean[] marked = new boolean[1000000];\n\n public static void main(String[] args) throws InterruptedException {\n\n Thread thread1 = new Thread(instance);\n Thread thread2 = new Thread(instance);\n\n thread1.start();\n thread2.start();\n\n thread1.join();\n thread2.join();\n\n System.out.println(\"表面上運行結果是 \" + instance.index);\n System.out.println(\"真正運行的次數 \" + realIndex.get());\n System.out.println(\"錯誤的次數 \" + wrongIndex.get());\n }\n\n @Override\n public void run() {\n marked[0] = true;\n for (int i = 0; i < 10000; i++) {\n\n try {\n cyclicBarrier1.await(); // 柵欄(當有兩個線程執行過它,放行)\n } catch (Exception e) {\n e.printStackTrace();\n }\n\n index++;\n\n try {\n cyclicBarrier1.reset();\n cyclicBarrier2.await();\n } catch (Exception e) {\n e.printStackTrace();\n }\n\n realIndex.incrementAndGet();\n\n synchronized (instance) {\n if (marked[index] && marked[index - 1]) {\n System.out.println(\"發生了錯誤\" + index);\n wrongIndex.incrementAndGet();\n }\n }\n\n marked[index] = true;\n }\n }\n\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"死鎖問題"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class MultiThreadError implements Runnable {\n\n int flag = 1;\n\n static Object object1 = new Object();\n static Object object2 = new Object();\n\n public static void main(String[] args) {\n MultiThreadError r1 = new MultiThreadError();\n MultiThreadError r2 = new MultiThreadError();\n\n r1.flag = 1;\n r2.flag = 0;\n\n new Thread(r1).start();\n new Thread(r2).start();\n }\n\n @Override\n public void run() {\n System.out.println(\"flag = \" + flag);\n\n if (flag == 1) {\n synchronized (object1) {\n try {\n Thread.sleep(500);\n } catch (InterruptedException e) {\n e.printStackTrace();\n }\n synchronized (object2) {\n System.out.println(\"object 1\");\n }\n }\n }\n\n if (flag == 0) {\n synchronized (object2) {\n try {\n Thread.sleep(500);\n } catch (InterruptedException e) {\n e.printStackTrace();\n }\n synchronized (object1) {\n System.out.println(\"object 2\");\n }\n }\n }\n }\n}"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"對象發佈和初始化的時候的安全問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"什麼是發佈"}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"聲明爲 public"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"return 一個對象"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"把對象作爲參數傳遞到其他類的方法中"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"什麼是逸出"}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"方法返回一個 private 對象(private 的本意是不讓外部訪問)"}]}]}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class MultiThreadsError3 {\n\n private Map states;\n\n public MultiThreadsError3() {\n states = new HashMap<>();\n states.put(\"1\", \"週一\");\n states.put(\"2\", \"週二\");\n states.put(\"3\", \"週三\");\n states.put(\"4\", \"週四\");\n }\n\n public Map getStates() {\n return states;\n }\n\n public Map getStatesImproved() {\n return new HashMap<>(states);\n }\n\n public static void main(String[] args) {\n MultiThreadsError3 multiThreadsError3 = new MultiThreadsError3();\n Map states = multiThreadsError3.getStates();\n\n System.out.println(states.get(\"1\"));\n states.remove(\"1\");\n System.out.println(states.get(\"1\"));\n\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"還未完成初始化(構造函數還沒完全執行完畢)就把對象提供個外界"}]}]}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在構造函數中未初始化完畢就 this 賦值"}]}]}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class MultiThreadsError4 {\n\n static Point point;\n\n public static void main(String[] args) throws InterruptedException {\n new PointMaker().start();\n\n Thread.sleep(505);\n\n if (point != null) {\n System.out.println(point);\n }\n }\n\n}\n\nclass Point {\n\n private final int x, y;\n\n public Point(int x, int y) throws InterruptedException {\n this.x = x;\n MultiThreadsError4.point = this;\n Thread.sleep(100); // MultiThreadsError4 中會根據 sleep 的大於或小於的阻塞時間而變化\n this.y = y;\n }\n\n @Override\n public String toString() {\n return \"Point{\" +\n \"x=\" + x +\n \", y=\" + y +\n '}';\n }\n}\n\nclass PointMaker extends Thread {\n\n @Override\n public void run() {\n try {\n new Point(1, 1);\n } catch (Exception e) {\n e.printStackTrace();\n }\n }\n}"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隱式逸出 —— 註冊監聽事件(觀察者模式)"}]}]}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class MultiThreadsError5 {\n\n int count;\n\n public MultiThreadsError5(MySource source) {\n source.registerListener(new EventListener() {\n @Override\n public void onEvent(Event e) {\n System.out.println(\"\\n我得到的數字是\" + count);\n }\n\n });\n for (int i = 0; i < 10000; i++) {\n System.out.print(i);\n }\n count = 100;\n }\n\n public static void main(String[] args) {\n MySource mySource = new MySource();\n new Thread(new Runnable() {\n @Override\n public void run() {\n try {\n Thread.sleep(10);\n } catch (InterruptedException e) {\n e.printStackTrace();\n }\n mySource.eventCome(new Event() {\n });\n }\n }).start();\n MultiThreadsError5 multiThreadsError5 = new MultiThreadsError5(mySource);\n }\n\n static class MySource {\n\n private EventListener listener;\n\n void registerListener(EventListener eventListener) {\n this.listener = eventListener;\n }\n\n void eventCome(Event e) {\n if (listener != null) {\n listener.onEvent(e);\n } else {\n System.out.println(\"還未初始化完畢\");\n }\n }\n }\n\n interface EventListener {\n\n void onEvent(Event e);\n }\n\n interface Event {\n\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"解決:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class MultiThreadsError7 {\n\n int count;\n private EventListener listener;\n\n private MultiThreadsError7(MySource source) {\n listener = new EventListener() {\n @Override\n public void onEvent(MultiThreadsError5.Event e) {\n System.out.println(\"\\n我得到的數字是\" + count);\n }\n\n };\n for (int i = 0; i < 10000; i++) {\n System.out.print(i);\n }\n count = 100;\n }\n\n public static MultiThreadsError7 getInstance(MySource source) {\n MultiThreadsError7 safeListener = new MultiThreadsError7(source);\n source.registerListener(safeListener.listener);\n return safeListener;\n }\n\n public static void main(String[] args) {\n MySource mySource = new MySource();\n new Thread(new Runnable() {\n @Override\n public void run() {\n try {\n Thread.sleep(10);\n } catch (InterruptedException e) {\n e.printStackTrace();\n }\n mySource.eventCome(new MultiThreadsError5.Event() {\n });\n }\n }).start();\n MultiThreadsError7 multiThreadsError7 = new MultiThreadsError7(mySource);\n }\n\n static class MySource {\n\n private EventListener listener;\n\n void registerListener(EventListener eventListener) {\n this.listener = eventListener;\n }\n\n void eventCome(MultiThreadsError5.Event e) {\n if (listener != null) {\n listener.onEvent(e);\n } else {\n System.out.println(\"還未初始化完畢\");\n }\n }\n\n }\n\n interface EventListener {\n\n void onEvent(MultiThreadsError5.Event e);\n }\n\n interface Event {\n\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"構造函數中運行線程"}]}]}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class MultiThreadsError6 {\n\n private Map states;\n\n public MultiThreadsError6() {\n new Thread(new Runnable() {\n @Override\n public void run() {\n states = new HashMap<>();\n states.put(\"1\", \"週一\");\n states.put(\"2\", \"週二\");\n states.put(\"3\", \"週三\");\n states.put(\"4\", \"週四\");\n }\n }).start();\n }\n\n public Map getStates() {\n return states;\n }\n\n public static void main(String[] args) throws InterruptedException {\n MultiThreadsError6 multiThreadsError6 = new MultiThreadsError6();\n // 造成時間不同執行不同\n Thread.sleep(1000); \n System.out.println(multiThreadsError6.getStates().get(\"1\"));\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"如何解決逸出"}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"副本"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"工廠模式"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"各種需要考慮線程安全的情況"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"訪問共享變量或資源,會有併發風險,比如對象的屬性、靜態變量、共享緩存、數據庫等"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所有依賴時序的操作,即使每一步操作都是線程安全的,還是存在併發問題"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"read-modify-writer 操作:一個線程讀取了一個共享數據,並在此基礎上更新該數據。該例子在上面的 a++ 已展示。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"check-then-act 操作:一個線程讀取了一個共享數據,並在此基礎上決定其下一個的操作"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不同的數據之間存在綁定關係的時候"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"IP 和端口號"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們使用其他類的時候,如果對方沒有聲明自己是線程安全的,那麼大概率會存在併發問題"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"hashmap 沒有聲明知己是併發安全的,所以我們併發調用 hashmap 的時候會出錯"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"多線程會導致的問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"什麼是性能問題、性能問題有哪些體現?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"爲什麼多線程會帶來性能問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調度:上下文切換"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"協作:內存同步"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"調度:上下文切換"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"什麼是上下文?:保存現場"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"緩存開銷:緩存失效"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"何時會導致密集的上下文切換:搶鎖、IO"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"參考"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://coding.imooc.com/class/362.html"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章