小馬哥Java面試題課程總結

lcc發表於2021-09-09

前段時間在慕課網直播上聽面試勸退(“面試虐我千百遍,Java 併發真討厭”),發現講得東西比自己拿到offer還要高興,於是自己線上下做了一點小筆記,供各位參考。

課程地址:

原始碼文件地址:

Java 多執行緒

1、執行緒建立

基本版

有哪些方法建立執行緒?

僅僅只有new thread這種方法建立執行緒

public class ThreadCreationQuestion {

    public static void main(String[] args) {
        // main 執行緒 -> 子執行緒
        Thread thread = new Thread(() -> {
        }, "子執行緒-1");

    }

    /**
     * 不鼓勵自定義(擴充套件) Thread
     */
    private static class MyThread extends Thread {

        /**
         * 多型的方式,覆蓋父類實現
         */
        @Override
        public void run(){
            super.run();
        }
    }

}

與執行執行緒方法區分:
java.lang.Runnable()java.lang.Thread類

進階版

如何透過Java 建立程式?

public class ProcessCreationQuestion {

    public static void main(String[] args) throws IOException {

        // 獲取 Java Runtime
        Runtime runtime = Runtime.getRuntime();
        Process process = runtime.exec("cmd /k start ");
        process.exitValue();
    }
}

勸退版

如何銷燬一個執行緒?

public class ThreadStateQuestion {


    public static void main(String[] args) {

        // main 執行緒 -> 子執行緒
        Thread thread = new Thread(() -> { // new Runnable(){ public void run(){...}};
            System.out.printf("執行緒[%s] 正在執行...n", Thread.currentThread().getName());  // 2
        }, "子執行緒-1");

        // 啟動執行緒
        thread.start();

        // 先於 Runnable 執行
        System.out.printf("執行緒[%s] 是否還活著: %sn", thread.getName(), thread.isAlive()); // 1
        // 在 Java 中,執行執行緒 Java 是沒有辦法銷燬它的,
        // 但是當 Thread.isAlive() 返回 false 時,實際底層的 Thread 已經被銷燬了
    }

Java程式碼中是無法實現的,只能表現一個執行緒的狀態。

而CPP是可以實現的。

2、執行緒執行

基本版

如何透過 Java API 啟動執行緒?

thread.start();

進階版

當有執行緒 T1、T2 以及 T3,如何實現T1 -> T2 -> T3的執行順序?

private static void threadJoinOneByOne() throws InterruptedException {
        Thread t1 = new Thread(ThreadExecutionQuestion::action, "t1");
        Thread t2 = new Thread(ThreadExecutionQuestion::action, "t2");
        Thread t3 = new Thread(ThreadExecutionQuestion::action, "t3");

        // start() 僅是通知執行緒啟動
        t1.start();
        // join() 控制執行緒必須執行完成
        t1.join();

        t2.start();
        t2.join();

        t3.start();
        t3.join();
    }

    private static void action() {
        System.out.printf("執行緒[%s] 正在執行...n", Thread.currentThread().getName());  // 2
    }
}

CountDownLatch也可以實現;

調整優先順序並不能保證優先順序高的執行緒先執行。

勸退版

以上問題請至少提供另外一種實現?(1.5)

1、spin 方法

    private static void threadLoop() {

        Thread t1 = new Thread(ThreadExecutionQuestion::action, "t1");
        Thread t2 = new Thread(ThreadExecutionQuestion::action, "t2");
        Thread t3 = new Thread(ThreadExecutionQuestion::action, "t3");

        t1.start();

        while (t1.isAlive()) {
            // 自旋 Spin
        }

        t2.start();

        while (t2.isAlive()) {

        }

        t3.start();

        while (t3.isAlive()) {

        }
    }

2、sleep 方法

 private static void threadSleep() throws InterruptedException {

        Thread t1 = new Thread(ThreadExecutionQuestion::action, "t1");
        Thread t2 = new Thread(ThreadExecutionQuestion::action, "t2");
        Thread t3 = new Thread(ThreadExecutionQuestion::action, "t3");

        t1.start();

        while (t1.isAlive()) {
            // sleep
            Thread.sleep(0);
        }

        t2.start();

        while (t2.isAlive()) {
            Thread.sleep(0);
        }

        t3.start();

        while (t3.isAlive()) {
            Thread.sleep(0);
        }

    }

3、while 方法

    private static void threadWait() throws InterruptedException {

        Thread t1 = new Thread(ThreadExecutionQuestion::action, "t1");
        Thread t2 = new Thread(ThreadExecutionQuestion::action, "t2");
        Thread t3 = new Thread(ThreadExecutionQuestion::action, "t3");

        threadStartAndWait(t1);
        threadStartAndWait(t2);
        threadStartAndWait(t3);
    }

    private static void threadStartAndWait(Thread thread) {

        if (Thread.State.NEW.equals(thread.getState())) {
            thread.start();
        }

        while (thread.isAlive()) {
            synchronized (thread) {
                try {
                    thread.wait(); // 到底是誰通知 Thread -> thread.notify();  JVM幫它喚起
                                  // LockSupport.park(); 
                                 // 死鎖發生
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

3、執行緒終止

基本版

如何停止一個執行緒?

public class HowToStopThreadQuestion {

    public static void main(String[] args) throws InterruptedException {

        Action action = new Action();

        // 方法一
        Thread t1 = new Thread(action, "t1");

        t1.start();

        // 改變 action stopped 狀態
        action.setStopped(true);

        t1.join();

        // 方法二
        Thread t2 = new Thread(() -> {
            if (!Thread.currentThread().isInterrupted()) {
                action();
            }
        }, "t2");

        t2.start();
        // 中斷操作(僅僅設定狀態,而並非中止執行緒)
        t2.interrupt();
        t2.join();
    }


    private static class Action implements Runnable {

        // 執行緒安全問題,確保可見性(Happens-Before)
        private volatile boolean stopped = false;

        @Override
        public void run() {
            if (!stopped) {
                // 執行動作
                action();
            }
        }

        public void setStopped(boolean stopped) {

            this.stopped = stopped;
        }
    }

    private static void action() {
        System.out.printf("執行緒[%s] 正在執行...n", Thread.currentThread().getName());  // 2
    }
}

想要停止一個執行緒是不可能的,真正的只能停止邏輯。

進階版

為什麼 Java 要放棄 Thread 的 stop()方法?

Because it is inherently unsafe. Stopping a thread causes it to unlock all the monitors that it has locked.(The monitors are unlocked as the ThreadDeath exception propagates up the stack.) If any of the objects previously protected by these monitors were in an inconsistent state, other threads may now view these objects in an inconsistent state. Such objects are said to be damaged. When threads operate on damaged objects, arbitrary behavior can result. This behavior may be subtle and difficult to detect, or it may be pronounced. Unlike other unchecked exceptions, ThreadDeath kills threads silently; thus, the user has no warning that his program may be corrupted. The corruption can manifest itself at any time after the actual damage occurs, even hours or days in the future.

該方法具有固有的不安全性。用 Thread.stop 來終止執行緒將釋放它已經鎖定的所有監視器(作為沿堆疊向上傳播的未檢查 ThreadDeath 異常的一個自然後果)。如果以前受這些監視器保護的任何物件都處於一種不一致的狀態,則損壞的物件將對其他執行緒可見,這有可能導致任意的行為。stop 的許多使用都應由只修改某些變數以指示目標執行緒應該停止執行的程式碼來取代。目標執行緒應定期檢查該變數,並且如果該變數指示它要停止執行,則從其執行方法依次返回。如果目標執行緒等待很長時間(例如基於一個條件變數),則應使用 interrupt 方法來中斷該等待。

簡單的說,防止死鎖,以及狀態不一致的情況出現。

勸退版

請說明 Thread interrupt()、isInterrupted() 以及 interrupted()的區別以及意義?

Thread interrupt(): 設定狀態,調JVM的本地(native)interrupt0()方法。

    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();  // Just to set the interrupt flag
                              //--> private native void interrupt0();
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }

isInterrupted(): 調的是靜態方法isInterrupted(),當且僅當狀態設定為中斷時,返回false,並不清除狀態。

  public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

    /**
     * Tests whether this thread has been interrupted.  The <i>interrupted
     * status</i> of the thread is unaffected by this method.
     *
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     *
     * @return  <code>true</code> if this thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see     #interrupted()
     * @revised 6.0
     */
     
    public boolean isInterrupted() {
        return isInterrupted(false);
    }

interrupted(): 私有本地方法,即判斷中斷狀態,又清除狀態。

 private native boolean isInterrupted(boolean ClearInterrupted);

4、執行緒異常

基本版

當執行緒遇到異常時,到底發生了什麼?

執行緒會掛

public class ThreadExceptionQuestion {

    public static void main(String[] args) throws InterruptedException {
        //...
        // main 執行緒 -> 子執行緒
        Thread t1 = new Thread(() -> {
            throw new RuntimeException("資料達到閾值");
        }, "t1");

        t1.start();
        // main 執行緒會中止嗎?
        t1.join();

        // Java Thread 是一個包裝,它由 GC 做垃圾回收
        // JVM Thread 可能是一個 OS Thread,JVM 管理,
        // 當執行緒執行完畢(正常或者異常)
        System.out.println(t1.isAlive());
    }
}

進階版

當執行緒遇到異常時,如何捕獲?

...
        Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
            System.out.printf("執行緒[%s] 遇到了異常,詳細資訊:%sn",
                    thread.getName(),
                    throwable.getMessage());
        });
...

勸退版

當執行緒遇到異常時,ThreadPoolExecutor 如何捕獲異常?

public class ThreadPoolExecutorExceptionQuestion {

    public static void main(String[] args) throws InterruptedException {

//        ExecutorService executorService = Executors.newFixedThreadPool(2);

        ThreadPoolExecutor executorService = new ThreadPoolExecutor(
                1,
                1,
                0,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>()
        ) {

            /**
             * 透過覆蓋 {@link ThreadPoolExecutor#afterExecute(Runnable, Throwable)} 達到獲取異常的資訊
             * @param r
             * @param t
             */
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                System.out.printf("執行緒[%s] 遇到了異常,詳細資訊:%sn",
                        Thread.currentThread().getName(),
                        t.getMessage());
            }

        };

        executorService.execute(() -> {
            throw new RuntimeException("資料達到閾值");
        });

        // 等待一秒鐘,確保提交的任務完成
        executorService.awaitTermination(1, TimeUnit.SECONDS);

        // 關閉執行緒池
        executorService.shutdown();

    }
}

5、執行緒狀態

基本版

Java 執行緒有哪些狀態,分別代表什麼含義?

NEW: Thread state for a thread which has not yet started.

未啟動的。不會出現在Dump中。

RUNNABLE: Thread state for a runnable thread. A thread in the runnable state is executing in the Java virtual machine, but it may be waiting for other resources from the operating system such as processor.

在虛擬機器內執行的。執行中狀態,可能裡面還能看到locked字樣,表明它獲得了某把鎖。

BLOCKE: Thread state for a thread blocked waiting for a monitor lock. A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter a synchronized block/method after calling {@link Object#wait() Object.wait}.

受阻塞並等待監視器鎖。被某個鎖(synchronizers)給block住了。

WAITING: Thread state for a waiting thread. A thread is in the waiting state due to calling one of the following methods:

<ul>
    <li>{@link Object#wait() Object.wait} with no timeout</li>
    <li>{@link #join() Thread.join} with no timeout</li>
    <li>{@link LockSupport#park() LockSupport.park}</li>
</ul>

A thread in the waiting state is waiting for another thread to perform a particular action.

For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() or Object.notifyAll() on that object. A thread that has called Thread.join() is waiting for a specified thread to terminate.

無限期等待另一個執行緒執行特定操作。等待某個condition或monitor發生,一般停留在park(), wait(), sleep(),join() 等語句裡。

TIMED_WAITING: Thread state for a waiting thread with a specified waiting time. A thread is in the timed waiting state due to calling one of the following methods with a specified positive waiting time:

<ul>
     <li>{@link #sleep Thread.sleep}</li>
     <li>{@link Object#wait(long) Object.wait} with timeout</li>
     <li>{@link #join(long) Thread.join} with timeout</li>
     <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
     <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
</ul>

有時限的等待另一個執行緒的特定操作。和WAITING的區別是wait() 等語句加上了時間限制 wait(timeout)。

TERMINATED: 已退出的; Thread state for a terminated thread. The thread has completed execution.

進階版

如何獲取當前JVM 所有的現場狀態?

方法一:命令

jstack: jstack用於列印出給定的java程式ID或core file或遠端除錯服務的Java堆疊資訊。主要用來檢視Java執行緒的呼叫堆疊的,可以用來分析執行緒問題(如死鎖)。

jsp

jsp [option/ -l] pid

方法二:ThreadMXBean


public class AllThreadStackQuestion {

    public static void main(String[] args) {
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] threadIds = threadMXBean.getAllThreadIds();

        for (long threadId : threadIds) {
            ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId);
            System.out.println(threadInfo.toString());
        }

    }
}

勸退版

如何獲取執行緒的資源消費情況?

public class AllThreadInfoQuestion {

    public static void main(String[] args) {
        ThreadMXBean threadMXBean = (ThreadMXBean) ManagementFactory.getThreadMXBean();
        long[] threadIds = threadMXBean.getAllThreadIds();

        for (long threadId : threadIds) {
//            ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId);
//            System.out.println(threadInfo.toString());
            long bytes = threadMXBean.getThreadAllocatedBytes(threadId);
            long kBytes = bytes / 1024;
            System.out.printf("執行緒[ID:%d] 分配記憶體: %s KBn", threadId, kBytes);
        }

    }
}

6、執行緒同步

基本版

請說明 synchronized 關鍵字在修飾方法與程式碼塊中的區別?

位元組碼的區別 (一個monitor,一個synchronized關鍵字)

public class SynchronizedKeywordQuestion {

    public static void main(String[] args) {

    }

    private static void synchronizedBlock() {
        synchronized (SynchronizedKeywordQuestion.class) {
        }
    }

    private synchronized static void synchronizedMethod() {
    }
}

進階版

請說明 synchronized 關鍵字與 ReentrantLock 之間的區別?

  • 兩者都是可重入鎖
  • synchronized 依賴於 JVM 而 ReentrantLock 依賴於 API
  • ReentrantLock 比 synchronized 增加了一些高階功能

相比synchronized,ReentrantLock增加了一些高階功能。主要來說主要有三點:①等待可中斷;②可實現公平鎖;③可實現選擇性通知(鎖可以繫結多個條件)

  • 兩者的效能已經相差無幾

勸退版

請解釋偏向鎖對 synchronized 與 ReentrantLock 的價值?

偏向鎖只對 synchronized 有用,而 ReentrantLock 已經實現了偏向鎖。

7、執行緒通訊

基本版

為什麼 wait() 和 notify() 以及 notifyAll() 方法屬於 Object ,並解釋它們的作用?

Java所有物件都是來自 Object

wait():

notify():

notifyAll():

進階版

為什麼 Object wait() notify() 以及 notifyAll() 方法必須 synchronized 之中執行?

wait(): 獲得鎖的物件,釋放鎖,當前執行緒又被阻塞,等同於Java 5 LockSupport 中的park方法

notify(): 已經獲得鎖,喚起一個被阻塞的執行緒,等同於Java 5 LockSupport 中的unpark()方法

notifyAll():

勸退版

請透過 Java 程式碼模擬實現 wait() 和 notify() 以及 notifyAll() 的語義?

8、執行緒退出

基本版

當主執行緒退出時,守護執行緒會執行完畢嗎?

不一定執行完畢

public class DaemonThreadQuestion {

    public static void main(String[] args) {
        // main 執行緒
        Thread t1 = new Thread(() -> {
            System.out.println("Hello,World");
//            Thread currentThread = Thread.currentThread();
//            System.out.printf("執行緒[name : %s, daemon:%s]: Hello,Worldn",
//                    currentThread.getName(),
//                    currentThread.isDaemon()
//            );
        }, "daemon");
        // 程式設計守候執行緒
        t1.setDaemon(true);
        t1.start();

        // 守候執行緒的執行依賴於執行時間(非唯一評判)
    }
}

進階版

請說明 ShutdownHook 執行緒的使用場景,以及如何觸發執行?

public class ShutdownHookQuestion {

    public static void main(String[] args) {

        Runtime runtime = Runtime.getRuntime();

        runtime.addShutdownHook(new Thread(ShutdownHookQuestion::action, "Shutdown Hook Question"));

    }

    private static void action() {
        System.out.printf("執行緒[%s] 正在執行...n", Thread.currentThread().getName());  // 2
    }
}

使用場景:Spring 中 AbstractApplicationContext 的 registerShutdownHook()

勸退版

如何確保主執行緒退出前,所有執行緒執行完畢?

public class CompleteAllThreadsQuestion {

    public static void main(String[] args) throws InterruptedException {

        // main 執行緒 -> 子執行緒
        Thread t1 = new Thread(CompleteAllThreadsQuestion::action, "t1");
        Thread t2 = new Thread(CompleteAllThreadsQuestion::action, "t2");
        Thread t3 = new Thread(CompleteAllThreadsQuestion::action, "t3");

        // 不確定 t1、t2、t3 是否呼叫 start()

        t1.start();
        t2.start();
        t3.start();

        // 建立了 N Thread

        Thread mainThread = Thread.currentThread();
        // 獲取 main 執行緒組
        ThreadGroup threadGroup = mainThread.getThreadGroup();
        // 活躍的執行緒數
        int count = threadGroup.activeCount();
        Thread[] threads = new Thread[count];
        // 把所有的執行緒複製 threads 陣列
        threadGroup.enumerate(threads, true);

        for (Thread thread : threads) {
            System.out.printf("當前活躍執行緒: %sn", thread.getName());
        }
    }

    private static void action() {
        System.out.printf("執行緒[%s] 正在執行...n", Thread.currentThread().getName());  // 2
    }

}

Java 併發集合框架

1、執行緒安全集合

基本版

請在 Java 集合框架以及 J.U.C 框架中各列舉出 List、Set 以及 Map 的實現?

Java 集合框架: LinkedList、ArrayList、HashSet、TreeSet、HashMap

J.U.C 框架: CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentSkipListSet、ConcurrentSkipListMap、ConcurrentHashMap

進階版

如何將普通 List、Set 以及 Map 轉化為執行緒安全物件?

public class ThreadSafeCollectionQuestion {

    public static void main(String[] args) {

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

        Set<Integer> set = Set.of(1, 2, 3, 4, 5);

        Map<Integer, String> map = Map.of(1, "A");

        // 以上實現都是不變物件,不過第一個除外

        // 透過 Collections#sychronized* 方法返回

        // Wrapper 設計模式(所有的方法都被 synchronized 同步或互斥)
        list = Collections.synchronizedList(list);

        set = Collections.synchronizedSet(set);

        map = Collections.synchronizedMap(map);

    }
}

勸退版

如何在 Java 9+ 實現以上問題?

public class ThreadSafeCollectionQuestion {

    public static void main(String[] args) {

        // Java 9 的實現
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

        // Java 9 + of 工廠方法,返回 Immutable 物件

        list = List.of(1, 2, 3, 4, 5);

        Set<Integer> set = Set.of(1, 2, 3, 4, 5);

        Map<Integer, String> map = Map.of(1, "A");

        // 以上實現都是不變物件,不過第一個除外

        // 透過 Collections#sychronized* 方法返回

        // Wrapper 設計模式(所有的方法都被 synchronized 同步或互斥)
        list = Collections.synchronizedList(list);

        set = Collections.synchronizedSet(set);

        map = Collections.synchronizedMap(map);

        //
        list = new CopyOnWriteArrayList<>(list);
        set = new CopyOnWriteArraySet<>(set);
        map = new ConcurrentHashMap<>(map);

    }
}

2、執行緒安全 LIST

基本版

請說明 List、Vector 以及 CopyOnWriteArrayList 的相同點和不同點?

相同點:

Vector、CopyOnWriteArrayList 是 List 的實現。

不同點:

Vector 是同步的,任何時候不加鎖。並且在設計中有個 interator ,返回的物件是 fail-fast

CopyOnWriteArrayList 讀的時候是不加鎖;弱一致性,while true的時候不報錯。

進階版

請說明 Collections#synchromizedList(List) 與 Vector 的相同點和不同點?

相同點:

都是synchromized 的實現方式。

不同點:

synchromized 返回的是list, 實現原理方式是 Wrapper 實現;

而 Vector 是 List 的實現。實現原理方式是非 Wrapper 實現。

勸退版

Arrays#asList(Object…)方法是執行緒安全的嗎?如果不是的話,如果實現替代方案?

public class ArraysAsListMethodQuestion {

    public static void main(String[] args) {

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        // 調整第三個元素為 9
        list.set(2, 9);
        // 3 -> 9
        // Arrays.asList 並非執行緒安全
        list.forEach(System.out::println);
        // Java < 5 , Collections#synchronizedList
        // Java 5+ , CopyOnWriteArrayList
        // Java 9+ , List.of(...) 只讀
    }
}

3、執行緒安全 SET

基本版

請至少舉出三種執行緒安全的 Set 實現?

synchronizedSet、CopyOnWriteArraySet、ConcurrentSkipListSet

進階版

在 J.U.C 框架中,存在HashSet 的執行緒安全實現?如果不存在的話,要如何實現?

不存在;

public class ConcurrentHashSetQuestion {


    public static void main(String[] args) {

    }

    private static class ConcurrentHashSet<E> implements Set<E> {

        private final Object OBJECT = new Object();

        private final ConcurrentHashMap<E, Object> map = new ConcurrentHashMap<>();

        private Set<E> keySet() {
            return map.keySet();
        }

        @Override
        public int size() {
            return keySet().size();
        }

        @Override
        public boolean isEmpty() {
            return keySet().isEmpty();
        }

        @Override
        public boolean contains(Object o) {
            return keySet().contains(o);
        }

        @Override
        public Iterator<E> iterator() {
            return keySet().iterator();
        }

        @Override
        public Object[] toArray() {
            return new Object[0];
        }

        @Override
        public <T> T[] toArray(T[] a) {
            return null;
        }

        @Override
        public boolean add(E e) {
            return map.put(e, OBJECT) == null;
        }

        @Override
        public boolean remove(Object o) {
            return map.remove(o) != null;
        }

        @Override
        public boolean containsAll(Collection<?> c) {
            return false;
        }

        @Override
        public boolean addAll(Collection<? extends E> c) {
            return false;
        }

        @Override
        public boolean retainAll(Collection<?> c) {
            return false;
        }

        @Override
        public boolean removeAll(Collection<?> c) {
            return false;
        }

        @Override
        public void clear() {

        }
    }
}

勸退版

當 Set#iterator() 方法返回 Iterator 物件後,能否在其迭代中,給 Set 物件新增新的元素?

不一定;Set 在傳統實現中,會有fail-fast問題;而在J.U.C中會出現弱一致性,對資料的一致性要求較低,是可以給 Set 物件新增新的元素。

4、執行緒安全 MAP

基本版

請說明 Hashtable、HashMap 以及 ConcurrentHashMap 的區別?

Hashtable: key、value值都不能為空; 陣列結構上,透過陣列和連結串列實現。

HashMap: key、value值都能為空;陣列結構上,當閾值到達8時,透過紅黑樹實現。

ConcurrentHashMap: key、value值都不能為空;陣列結構上,當閾值到達8時,透過紅黑樹實現。

進階版

請說明 ConcurrentHashMap 在不同的JDK 中的實現?

JDK 1.6中,採用分離鎖的方式,在讀的時候,部分鎖;寫的時候,完全鎖。而在JDK 1.7、1.8中,讀的時候不需要鎖的,寫的時候需要鎖的。並且JDK 1.8中在為了解決Hash衝突,採用紅黑樹解決。

勸退版

請說明 ConcurrentHashMap 與 ConcurrentSkipListMap 各自的優勢與不足?

在 java 6 和 8 中,ConcurrentHashMap 寫的時候,是加鎖的,所以記憶體佔得比較小,而 ConcurrentSkipListMap 寫的時候是不加鎖的,記憶體佔得相對比較大,透過空間換取時間上的成本,速度較快,但比前者要慢,ConcurrentHashMap 基本上是常量時間。ConcurrentSkipListMap 讀和寫都是log N實現,高效能相對穩定。

5、執行緒安全 QUEUE

基本版

請說明 BlockingQueue 與 Queue 的區別?

BlockingQueue 繼承了 Queue 的實現;put 方法中有個阻塞的操作(InterruptedException),當佇列滿的時候,put 會被阻塞;當佇列空的時候,put方法可用。take 方法中,當資料存在時,才可以返回,否則為空。

進階版

請說明 LinkedBlockingQueue 與 ArrayBlockingQueue 的區別?

LinkedBlockingQueue 是連結串列結構;有兩個構造器,一個是(Integer.MAX_VALUE),無邊界,另一個是(int capacity),有邊界;ArrayBlockingQueue 是陣列結構;有邊界。

勸退版

請說明 LinkedTransferQueue 與 LinkedBlockingQueue 的區別?

LinkedTransferQueue 是java 7中提供的新介面,效能比後者更最佳化。

6、PRIORITYBLOCKINGQUEUE

請評估以下程式的執行結果?

public class priorityBlockingQueueQuiz{
    public static void main(String[] args) throw Exception {
        BlockingQueue<Integer> queue = new PriorityBlockingQueue<>(2);
        // 1. PriorityBlockingQueue put(Object) 方法不阻塞,不拋異常
        // 2. PriorityBlockingQueue offer(Object) 方法不限制,允許長度變長
        // 3. PriorityBlockingQueue 插入物件會做排序,預設參照元素 Comparable 實現,
        //    或者顯示地傳遞 Comparator
        queue.put(9);
        queue.put(1);
        queue.put(8);
        System.out.println("queue.size() =" + queue.size());
        System.out.println("queue.take() =" + queue.take());
        System.out.println("queue =" + queue);
    }
}

執行結果:

queue.size() = 3
queue.take() = 1
queue = [8,9]

7、SYNCHRONOUSQUEUE

請評估以下程式的執行結果?

public class SynchronusQueueQuiz{
    
    public static void main(String[] args) throws Exception {
        BlockingQueue<Integer> queue = new SynchronousQueue<>();
        // 1. SynchronousQueue 是無空間,offer 永遠返回 false
        // 2. SynchronousQueue take() 方法會被阻塞,必須被其他執行緒顯示地呼叫 put(Object);
        System.out.pringln("queue.offer(1) = " + queue.offer(1));
        System.out.pringln("queue.offer(2) = " + queue.offer(2));
        System.out.pringln("queue.offer(3) = " + queue.offer(3));
        System.out.println("queue.take() = " + queue.take());
        System.out.println("queue.size = " + queue.size());
    }
}

執行結果:

queue.offer(1) = false
queue.offer(2) = false
queue.offer(3) = false

SynchronousQueue take() 方法會被阻塞

8、BLOCKINGQUEUE OFFER()

請評估以下程式的執行結果?

public class BlockingQueueQuiz{
    public static void main(String[] args) throws Exception {
        offer(new ArrayBlockingQueue<>(2));
        offer(new LinkedBlockingQueue<>(2));
        offer(new PriorityBlockingQueue<>(2));
        offer(new SynchronousQueue<>());
    }
}

private static void offer(BlockingQueue<Integer> queue) throws Exception {
    System.out.println("queue.getClass() = " +queue.getClass().getName());
    System.out.println("queue.offer(1) = " + queue.offer(1));
    System.out.println("queue.offer(2) = " + queue.offer(2));
    System.out.println("queue.offer(3) = " + queue.offer(3));
    System.out.println("queue.size() = " + queue.size());
    System.out.println("queue.take() = " + queue.take());
    }
}

執行結果:

queue.getClass() = java.util.concurrent.ArrayBlockingQueue
queue.offer(1) = true
queue.offer(2) = true
queue.offer(3) = false
queue.size() = 2
queue.take() = 1

queue.getClass() = java.util.concurrent.LinkedBlockingQueue
queue.offer(1) = true
queue.offer(2) = true
queue.offer(3) = false
queue.size() = 2
queue.take() = 1

queue.getClass() = java.util.concurrent.PriorityBlockingQueue
queue.offer(1) = true
queue.offer(2) = true
queue.offer(3) = false
queue.size() = 3
queue.take() = 1

queue.getClass() = java.util.concurrent.SynchronousQueue
queue.offer(1) = false
queue.offer(2) = false
queue.offer(3) = false
queue.size() = 0

queue.take() 方法會被阻塞

Java 併發框架

1、鎖 LOCK

基本版

請說明 ReentranLock 與 ReentrantReadWriteLock 的區別?

jdk 1.5 以後,ReentranLock(重進入鎖)與 ReentrantReadWriteLock 都是可重進入的鎖,ReentranLock 都是互斥的,而 ReentrantReadWriteLock 是共享的,其中裡面有兩個類,一個是 ReadLock(共享,並行,強調資料一致性或者說可見性),另一個是 WriteLock(互斥,序列)。

進階版

請解釋 ReentrantLock 為什麼命名為重進入?

public class ReentrantLockQuestion {

    /**
     * T1 , T2 , T3
     *
     * T1(lock) , T2(park), T3(park)
     * Waited Queue -> Head-> T2 next -> T3
     * T1(unlock) -> unpark all
     * Waited Queue -> Head-> T2 next -> T3
     * T2(free), T3(free)
     *
     * -> T2(lock) , T3(park)
     * Waited Queue -> Head-> T3
     * T2(unlock) -> unpark all
     * T3(free)
     */


    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        // thread[main] ->
        // lock     lock           lock
        // main -> action1() -> action2() -> action3()
        synchronizedAction(ReentrantLockQuestion::action1);
    }


    private static void action1() {
        synchronizedAction(ReentrantLockQuestion::action2);

    }

    private static void action2() {
        synchronizedAction(ReentrantLockQuestion::action3);
    }

    private static void action3() {
        System.out.println("Hello,World");
    }

    private static void synchronizedAction(Runnable runnable) {
        lock.lock();
        try {
            runnable.run();
        } finally {
            lock.unlock();
        }
    }
}

勸退版

請說明 Lock#lock() 與 Lock#lockInterruptibly() 的區別?

    /**
     * java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued
     * 如果當前執行緒已被其他執行緒呼叫了 interrupt() 方法時,這時會返回 true
     * acquireQueued 執行完時,interrupt 清空(false)
     * 再透過 selfInterrupt() 方法將狀態恢復(interrupt=true)
     */
         public static void main(String[] args) {
         lockVsLockInterruptibly();
     }
     
        private static void lockVsLockInterruptibly() {

        try {
            lock.lockInterruptibly();
            action1();
        } catch (InterruptedException e) {
            // 顯示地恢復中斷狀態
            Thread.currentThread().interrupt();
            // 當前執行緒並未消亡,執行緒池可能還在存活
        } finally {
            lock.unlock();
        }
    }

lock() 優先考慮獲取鎖,待獲取鎖成功後,才響應中斷。

**lockInterruptibly() ** 優先考慮響應中斷,而不是響應鎖的普通獲取或重入獲取。

ReentrantLock.lockInterruptibly 允許在等待時由其它執行緒呼叫等待執行緒的 Thread.interrupt 方法來中斷等待執行緒的等待而直接返回,這時不用獲取鎖,而會丟擲一個 InterruptedException。

ReentrantLock.lock 方法不允許 Thread.interrupt 中斷,即使檢測到 Thread.isInterrupted ,一樣會繼續嘗試獲取鎖,失敗則繼續休眠。只是在最後獲取鎖成功後再把當前執行緒置為 interrupted 狀態,然後再中斷執行緒。

2、條件變數 CONDITION

基本版

請舉例說明 Condition 使用場景?

  1. CoutDownLatch (condition 變種)
  2. CyclicBarrier (迴圈屏障)
  3. 訊號量/燈(Semaphore) java 9
  4. 生產者和消費者
  5. 阻塞佇列

進階版

請使用 Condition 實現 “生產者-消費者問題”?

勸退版

請解釋 Condition await() 和 signal() 與 Object wait () 和 notify() 的相同與差異?

相同:阻塞和釋放

差異:Java Thread 物件和實際 JVM 執行的 OS Thread 不是相同物件,JVM Thread 回撥 Java Thread.run() 方法,同時 Thread 提供一些 native 方法來獲取 JVM Thread 狀態,當JVM thread 執行後,自動 notify()了。

        while (thread.isAlive()) { // Thread 特殊的 Object
            // 當執行緒 Thread isAlive() == false 時,thread.wait() 操作會被自動釋放
            synchronized (thread) {
                try {
                    thread.wait(); // 到底是誰通知 Thread -> thread.notify();
//                    LockSupport.park(); // 死鎖發生
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }

3、屏障 BARRIERS

基本版

請說明 CountDownLatch 與 CyclicBarrier 的區別?

CountDownLatch : 不可迴圈的,一次性操作(倒數計時)。

public class CountDownLatchQuestion {

    public static void main(String[] args) throws InterruptedException {

        // 倒數計數 5
        CountDownLatch latch = new CountDownLatch(5);

        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 4; i++) {
            executorService.submit(() -> {
                action();
                latch.countDown(); // -1
            });
        }

        // 等待完成
        // 當計數 > 0,會被阻塞
        latch.await();

        System.out.println("Done");

        // 關閉執行緒池
        executorService.shutdown();
    }

    private static void action() {
        System.out.printf("執行緒[%s] 正在執行...n", Thread.currentThread().getName());  // 2
    }

}

CyclicBarrier:可迴圈的, 先計數 -1,再判斷當計數 > 0 時候,才阻塞。

public class CyclicBarrierQuestion {

    public static void main(String[] args) throws InterruptedException {

        CyclicBarrier barrier = new CyclicBarrier(5); // 5

        ExecutorService executorService = Executors.newFixedThreadPool(5); // 3

        for (int i = 0; i < 20; i++) {
            executorService.submit(() -> {
                action();
                try {
                    // CyclicBarrier.await() = CountDownLatch.countDown() + await()
                    // 先計數 -1,再判斷當計數 > 0 時候,才阻塞
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
        }

        // 儘可能不要執行完成再 reset
        // 先等待 3 ms
        executorService.awaitTermination(3, TimeUnit.MILLISECONDS);
        // 再執行 CyclicBarrier reset
        // reset 方法是一個廢操作
        barrier.reset();

        System.out.println("Done");

        // 關閉執行緒池
        executorService.shutdown();
    }

    private static void action() {
        System.out.printf("執行緒[%s] 正在執行...n", Thread.currentThread().getName());  // 2
    }

}

進階版

請說明 Semaphore(訊號量/燈) 的使用場景?

Semaphore 和Lock類似,比Lock靈活。其中有 acquire() 和 release() 兩種方法,arg 都等於 1。acquire() 會丟擲 InterruptedException,同時從 sync.acquireSharedInterruptibly(arg:1)可以看出是讀模式(shared); release()中可以計數,可以控制數量,permits可以傳遞N個數量。

勸退版

請透過 Java 1.4 的語法實現一個 CountDownLatch?

public class LegacyCountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {

        // 倒數計數 5
        MyCountDownLatch latch = new MyCountDownLatch(5);

        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> {
                action();
                latch.countDown(); // -1
            });
        }

        // 等待完成
        // 當計數 > 0,會被阻塞
        latch.await();

        System.out.println("Done");

        // 關閉執行緒池
        executorService.shutdown();
    }

    private static void action() {
        System.out.printf("執行緒[%s] 正在執行...n", Thread.currentThread().getName());  // 2
    }

    /**
     * Java 1.5+ Lock 實現
     */
    private static class MyCountDownLatch {

        private int count;

        private final Lock lock = new ReentrantLock();

        private final Condition condition = lock.newCondition();

        private MyCountDownLatch(int count) {
            this.count = count;
        }

        public void await() throws InterruptedException {
            // 當 count > 0 等待
            if (Thread.interrupted()) {
                throw new InterruptedException();
            }

            lock.lock();
            try {
                while (count > 0) {
                    condition.await(); // 阻塞當前執行緒
                }
            } finally {
                lock.unlock();
            }
        }

        public void countDown() {

            lock.lock();
            try {
                if (count < 1) {
                    return;
                }
                count--;
                if (count < 1) { // 當數量減少至0時,喚起被阻塞的執行緒
                    condition.signalAll();
                }
            } finally {
                lock.unlock();
            }
        }
    }

    /**
     * Java < 1.5 實現
     */
    private static class LegacyCountDownLatch {

        private int count;

        private LegacyCountDownLatch(int count) {
            this.count = count;
        }

        public void await() throws InterruptedException {
            // 當 count > 0 等待
            if (Thread.interrupted()) {
                throw new InterruptedException();
            }

            synchronized (this) {
                while (count > 0) {
                    wait(); // 阻塞當前執行緒
                }
            }
        }

        public void countDown() {
            synchronized (this) {
                if (count < 1) {
                    return;
                }
                count--;
                if (count < 1) { // 當數量減少至0時,喚起被阻塞的執行緒
                    notifyAll();
                }
            }
        }
    }
}

4、執行緒池 THREAD POOL

基本版

請問 J.U.C 中內建了幾種 ExceptionService 實現?

1.5:ThreadPoolExecutor、ScheduledThreadPoolExecutor

1.7:ForkJoinPool

public class ExecutorServiceQuestion {

    public static void main(String[] args) {
        /**
         * 1.5
         *  ThreadPoolExecutor
         *  ScheduledThreadPoolExecutor :: ThreadPoolExecutor
         * 1.7
         *  ForkJoinPool
         */
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        executorService = Executors.newScheduledThreadPool(2);

        // executorService 不再被引用,它會被 GC -> finalize() -> shutdown()
        ExecutorService executorService2 = Executors.newSingleThreadExecutor();
    }
}

進階版

請分別解釋 ThreadPoolExecutor 構造器引數在執行時的作用?

/**
 * Creates a new {@code ThreadPoolExecutor} with the given initial
 * parameters.
 *
 * @param corePoolSize the number of threads to keep in the pool, even
 *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
 * @param maximumPoolSize the maximum number of threads to allow in the
 *        pool
 * @param keepAliveTime when the number of threads is greater than
 *        the core, this is the maximum time that excess idle threads
 *        will wait for new tasks before terminating.
 * @param unit the time unit for the {@code keepAliveTime} argument
 * @param workQueue the queue to use for holding tasks before they are
 *        executed.  This queue will hold only the {@code Runnable}
 *        tasks submitted by the {@code execute} method.
 * @param threadFactory the factory to use when the executor
 *        creates a new thread
 * @param handler the handler to use when execution is blocked
 *        because the thread bounds and queue capacities are reached
 * @throws IllegalArgumentException if one of the following holds:<br>
 *         {@code corePoolSize < 0}<br>
 *         {@code keepAliveTime < 0}<br>
 *         {@code maximumPoolSize <= 0}<br>
 *         {@code maximumPoolSize < corePoolSize}
 * @throws NullPointerException if {@code workQueue}
 *         or {@code threadFactory} or {@code handler} is null
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler);

corePoolSize: 核心執行緒池大小。這個引數是否生效取決於allowCoreThreadTimeOut變數的值,該變數預設是false,即對於核心執行緒沒有超時限制,所以這種情況下,corePoolSize引數是起效的。如果allowCoreThreadTimeOut為true,那麼核心執行緒允許超時,並且超時時間就是keepAliveTime引數和unit共同決定的值,這種情況下,如果執行緒池長時間空閒的話最終存活的執行緒會變為0,也即corePoolSize引數失效。

maximumPoolSize: 執行緒池中最大的存活執行緒數。這個引數比較好理解,對於超出corePoolSize部分的執行緒,無論allowCoreThreadTimeOut變數的值是true還是false,都會超時,超時時間由keepAliveTime和unit兩個引數算出。

keepAliveTime: 超時時間。

unit: 超時時間的單位,秒,毫秒,微秒,納秒等,與keepAliveTime引數共同決定超時時間。

workQueue: 執行緒等待佇列。當呼叫execute方法時,如果執行緒池中沒有空閒的可用執行緒,那麼就會把這個Runnable物件放到該佇列中。這個引數必須是一個實現BlockingQueue介面的阻塞佇列,因為要保證執行緒安全。有一個要注意的點是,只有在呼叫execute方法是,才會向這個佇列中新增任務,那麼對於submit方法呢,難道submit方法提交任務時如果沒有可用的執行緒就直接扔掉嗎?當然不是,看一下AbstractExecutorService類中submit方法實現,其實submit方法只是把傳進來的Runnable物件或Callable物件包裝成一個新的Runnable物件,然後呼叫execute方法,並將包裝後的FutureTask物件作為一個Future引用返回給呼叫者。Future的阻塞特性實際是在FutureTask中實現的,具體怎麼實現感興趣的話可以看一下FutureTask的原始碼。

threadFactory: 執行緒建立工廠。用於在需要的時候生成新的執行緒。預設實現是Executors.defaultThreadFactory(),即new 一個Thread物件,並設定執行緒名稱,daemon等屬性。

handler: 拒絕策略。這個引數的作用是當提交任務時既沒有空閒執行緒,任務佇列也滿了,這時候就會呼叫handler的rejectedExecution方法。預設的實現是丟擲一個RejectedExecutionException異常。

勸退版

如何獲取 ThreadPoolExecutor 正在執行的執行緒?

public class ThreadPoolExecutorThreadQuestion {

    public static void main(String[] args) throws InterruptedException {

        // main 執行緒啟動子執行緒,子執行緒的創造來自於 Executors.defaultThreadFactory()

        ExecutorService executorService = Executors.newCachedThreadPool();
        // 之前瞭解 ThreadPoolExecutor beforeExecute 和 afterExecute 能夠獲取當前執行緒數量

        Set<Thread> threadsContainer = new HashSet<>();

        setThreadFactory(executorService, threadsContainer);
        for (int i = 0; i < 9; i++) { // 開啟 9 個執行緒
            executorService.submit(() -> {
            });
        }

        // 執行緒池等待執行 3 ms
        executorService.awaitTermination(3, TimeUnit.MILLISECONDS);

        threadsContainer.stream()
                .filter(Thread::isAlive)
                .forEach(thread -> {
                    System.out.println("執行緒池創造的執行緒 : " + thread);
                });

        Thread mainThread = Thread.currentThread();

        ThreadGroup mainThreadGroup = mainThread.getThreadGroup();

        int count = mainThreadGroup.activeCount();
        Thread[] threads = new Thread[count];
        mainThreadGroup.enumerate(threads, true);

        Stream.of(threads)
                .filter(Thread::isAlive)
                .forEach(thread -> {
                    System.out.println("執行緒 : " + thread);
                });

        // 關閉執行緒池
        executorService.shutdown();

    }

    private static void setThreadFactory(ExecutorService executorService, Set<Thread> threadsContainer) {

        if (executorService instanceof ThreadPoolExecutor) {
            ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorService;
            ThreadFactory oldThreadFactory = threadPoolExecutor.getThreadFactory();
            threadPoolExecutor.setThreadFactory(new DelegatingThreadFactory(oldThreadFactory, threadsContainer));
        }
    }

    private static class DelegatingThreadFactory implements ThreadFactory {

        private final ThreadFactory delegate;

        private final Set<Thread> threadsContainer;

        private DelegatingThreadFactory(ThreadFactory delegate, Set<Thread> threadsContainer) {
            this.delegate = delegate;
            this.threadsContainer = threadsContainer;
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread thread = delegate.newThread(r);
            // cache thread
            threadsContainer.add(thread);
            return thread;
        }
    }
}

5、FUTURE

基本版

如何獲取 Future 物件?

submit()

進階版

請舉例 Future get() 以及 get(Long,TimeUnit) 方法的使用場景?

超時等待

InterruptedException

ExcutionException

TimeOutException

勸退版

如何利用 Future 優雅地取消一個任務的執行?

public class CancellableFutureQuestion {

    public static void main(String[] args) {

        ExecutorService executorService = Executors.newSingleThreadExecutor();

        Future future = executorService.submit(() -> { // 3秒內執行完成,才算正常
            action(5);
        });

        try {
            future.get(3, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            // Thread 恢復中斷狀態
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        } catch (TimeoutException e) {
            // 執行超時,適當地關閉
            Thread.currentThread().interrupt(); // 設定中斷狀態
            future.cancel(true); // 嘗試取消
        }

        executorService.shutdown();
    }

    private static void action(int seconds) {
        try {
            Thread.sleep(TimeUnit.SECONDS.toMillis(seconds)); // 5 - 3
            // seconds - timeout = 剩餘執行時間
            if (Thread.interrupted()) { // 判斷並且清除中斷狀態
                return;
            }
            action();
        } catch (InterruptedException e) {
        }
    }

    private static void action() {
        System.out.printf("執行緒[%s] 正在執行...n", Thread.currentThread().getName());  // 2
    }
}

6、VOLATILE 變數

基本版

在 Java 中,volatile 保證的是可見性還是原子性?

volatile 既有可見性又有原子性(非我及彼),可見性是一定的,原子性是看情況的。物件型別和原生型別都是可見性,原生型別是原子性。

進階版

在 Java 中,volatile long 和 double 是執行緒安全的嗎?

volatile long 和 double 是執行緒安全的。

勸退版

在 Java 中,volatile 底層實現是基於什麼機制?

記憶體屏障(變數 Lock)機制:一個變數的原子性的保證。

7、原子操作 ATOMIC

基本版

為什麼 AtomicBoolean 內部變數使用 int 實現,而非 boolean?

作業系統有 X86 和 X64,在虛擬機器中,基於boolean 實現就是用 int 實現的,用哪一種實現都可以。虛擬機器只有32位和64位的,所以用32位的實現。

進階版

在變數原子操作時,Atomic* CAS 操作比 synchronized 關鍵字哪個更重?

同執行緒的時候,synchronized 更快;而多執行緒的時候則要分情況討論。

public class AtomicQuestion {

    private static int actualValue = 3;

    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(3);
        // if( value == 3 )
        //     value = 5
        atomicInteger.compareAndSet(3, 5);
        // 偏向鎖 < CAS 操作 < 重鎖(完全互斥)
        // CAS 操作也是相對重的操作,它也是實現 synchronized 瘦鎖(thin lock)的關鍵
        // 偏向鎖就是避免 CAS(Compare And Set/Swap)操作
    }

    private synchronized static void compareAndSet(int expectedValue, int newValue) {
        if (actualValue == expectedValue) {
            actualValue = newValue;
        }
    }
}

勸退版

Atomic* CAS 的底層是如何實現?

彙編指令:cpmxchg (Compare and Exchange)

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2331/viewspace-2823124/,如需轉載,請註明出處,否則將追究法律責任。

相關文章