Java面試題解析

calong發表於2021-04-15

1. volatile

  1. 記憶體可見性:主記憶體中變數在多執行緒環境中被使用時會被複制到執行緒的私有記憶體中,volatile關鍵字修飾的變數可以保證主記憶體和執行緒私有記憶體的同一變數的一致性。
  2. 不保證原子性:volatile關鍵字修飾的變數可以保證單次運算的原子性(禁止指令重排),但是無法保證多次運算(例如++或–)的原子性。
  3. 禁止指令重排:JVM編譯後的位元組碼指令並不一定按照編碼順序執行,但是使用volatile關鍵字修飾的變數在執行運算時會禁止位元組碼指令重排。

2. CAS(CompareAndSet)

CompareAndSetUnsafe類的一個native方法,在rt.jar中,native修飾的方法可以像C語言中的指標一樣直接操作特定記憶體。
CAS是一條CPU的併發原語(原語的執行必須是連續的,且執行過程不能被中斷)

public class AtomicInteger extends Number implements java.io.Serializable {
    private volatile int value;
    public final boolean compareAndSet(int expect, int update) {
            return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
        }

CompareAndSet的作用為以執行緒私有記憶體的身份獲取當前主記憶體中的變數值value與預期值expect進行比較,如果相等則更新value值為update並返回true,否則不更新並返回false。
CompareAndSet的缺點:

  1. 在於如果比較結果不相等則會一直進行嘗試,可能會給CPU帶來很大的開銷。
  2. 只能保證一個共享變數的原子操作。

3. ABA

ABA問題是指兩個或多個執行頻率相差較大的執行緒中,執行頻率高的執行緒在其他執行緒未知曉的情況下多次修改主記憶體的值並最終修改為原值,導致其他執行緒認為主記憶體中的值沒有改變。
ABA問題會被發現於注重執行過程的程式當中,對於只注重呼叫結果的程式,可以忽略該問題。

// ABA問題演示及解決方案
AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(100, 1);
        new Thread(() -> {
            boolean res = reference.compareAndSet(100, 101, reference.getStamp(), reference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t" + res + "\t" + reference.getStamp() + "\t" + reference.getReference());
            res = reference.compareAndSet(101, 100, reference.getStamp(), reference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t" + res + "\t" + reference.getStamp() + "\t" + reference.getReference());
        }, "t1").start();
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean res = reference.compareAndSet(100, 101, 1, reference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t" + res + "\t" + reference.getStamp() + "\t" + reference.getReference());
        }, "t2").start();

解決方案:原子引用
原子引用AtomicReference<V>可以將一個java類封裝為原子物件,利用該特性將時間戳與原子引用結合使用就可以解決ABA問題了,java中已經提供了封裝好的時間戳類AtomicStampedReference<V>

4. 集合類的執行緒安全

ArrayList併發異常舉例

ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 30; i++) {
    new Thread(()-> {
        list.add(UUID.randomUUID().toString().substring(0, 8));
        System.out.println(list);
    }, "t" + i).start();
}

異常名稱:java.util.ConcurrentModificationException

解決方案:

  1. 使用Vector<E>Vector<E>的操作會加鎖,可以保證資料一致性。
  2. Collections.synchronizedList(new ArrayList<>()),將執行緒不安全的集合封裝為執行緒安全的集合。
  3. 使用CopyOnWriteArrayList<E>實現讀寫分離。

CopyOnWriteArray原始碼

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

在對集合進行寫操作時會先加鎖,對當前集合進行復制,在複製的集合上進行寫操作,最後再將寫完的集合覆蓋到原集合上再解鎖。

5. 指標引用問題

class Person() {
    public int age;
    public String name;
    public void setAge(int age) {
        age = 30;
    }
    public void setName(Person person) {
        person.name = "xxx";
    }
    public void setName(String name) {
        name = "str";
    }
}

演示案例1=3

// 案例1: setAge方法中改變的是形參的值,形參是實參變數的副本,因此改變副本的值並不影響變數本身的值
Person person = new Person("test", 10);
int age = 20;
person.setAge(age);
System.out.println("age=" + age);
// 結果: age=20
// 案例2: 目前有兩個Person指標分別指向(test,10)和(abc,10),setName方法中將person1指標指向的Person物件的name值改變為"xxx",因此值被真的改變了
Person person1 = new Person("abc", 10);
person.setName(person1)
System.out.println("name=" + person1.name);
// 結果: name=xxx
// 案例3: 目前有兩個String指標同時指向xxx(實參和形參),setName方法中將形參指向了str,實參並沒有改變,因此str=xxx
String str = "xxx";
person.setName(str);
System.out.println("name=" + str);
// 結果: name=xxx

6. 執行緒鎖

6.1. 公平鎖/非公平鎖

// ReentrantLock預設為非公平鎖
public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

公平鎖是指多個執行緒按照申請鎖的先後順序來獲取鎖,遵守先來後到原則;而非公平鎖有可能會出現先申請的後獲取到鎖的現象。非公平鎖的優點是吞吐量比較大。
注:synchronized屬於非公平鎖。

6.2. 可重入鎖(遞迴鎖)

當程式碼存在巢狀鎖時,同一執行緒在外部程式碼獲取鎖以後進入內部程式碼時會自動獲取鎖。
synchronizedReentrantLock都是典型的可重入鎖

6.3. 自旋鎖

獲取鎖的執行緒獲取失敗時不會立即阻塞,而是會採用迴圈的方式去嘗試獲取鎖

AtomicReference<Thread> reference = new AtomicReference<>();
// 使用自旋鎖加鎖
public void lock () {
    Thread thread = Thread.currentThread();
    System.out.println(Thread.currentThread().getName() + "\t come in (+_+)?");
    while (!reference.compareAndSet(null, thread)) {
        //TODO: 獲取鎖之後的操作
    }
}
// 解鎖自旋鎖
public void unlock () {
    Thread thread = Thread.currentThread();
    reference.compareAndSet(thread, null);
}

6.4. 獨佔鎖(寫鎖)/共享鎖(讀鎖)/互斥鎖

ReentrantLock和synchronized屬於獨佔鎖
讀寫鎖的原理在於讀寫分離,寫操作要保證原子性,操作過程不可被打斷;讀操作可以多個執行緒共享鎖。

ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
// 寫鎖-加鎖
lock.writeLock().lock()
// 寫鎖-解鎖
lock.writeLock().unlock()
// 讀鎖-加鎖
lock.readLock().lock()
// 讀鎖-解鎖
lock.readLock().unlock()

6.4. Lock和Condition

多執行緒中判斷條件時應該使用while而不是if,因為while可以線上程被喚醒以後再次判斷條件是否滿足,而if會直接往下執行

// 通過ReentrantLock建立Condition
Condition condition = lock.newCondition();
  1. 一個ReentrantLock可以建立多個Condition
  2. 當線上程A呼叫condition.await()函式時可以讓當前執行緒處於阻塞狀態;當執行緒B呼叫condition.signal()函式時可以把執行緒A喚醒。
  3. 一個Condition可以繫結在多個執行緒中,使用condition.signalAll()可以喚醒所有處於阻塞狀態的執行緒,如果呼叫condition.signal()則會隨機喚醒其中一個。

7. CountDownLatch/CyclicBarrier/Semaphore

7.1. CountDownLatch(倒數計時)

倒數計時鎖,在CountDownLatch初始化時會指定從幾開始倒數計時,countDown()函式每執行一次會將計數器的值減一,當倒數計時為0時await()的阻塞狀態才會結束,否則會一直等待countDown()函式執行,直到計時到0。

int count = 5;
CountDownLatch latch = new CountDownLatch(count);
// 倒數計時執行緒
for (int i = 0; i < count; i++) {
    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "執行完畢...");
        latch.countDown();
    }, "t" + i).start();
}
latch.await();
System.out.println("主執行緒執行...");

7.2. CyclicBarrier(收集器)

收集器的初始化引數中包含兩項,第一個時滿足條件的收集個數,第二個時滿足條件以後執行的執行緒。
當線上程中呼叫await()函式時收集個數會加一,然後收集器會判斷當前收集個數是否滿足條件,當滿足個數以後將會執行初始化引數中的執行緒。

int count = 5;
CyclicBarrier barrier = new CyclicBarrier(count, () -> {
    System.out.println("收集器執行...");
});
// 收集執行緒
for (int i = 0; i < count; i++) {
    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "收集");
        try {
            barrier.await();
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }).start();
}

7.3. Semaphore(訊號燈)

當多個執行緒搶佔多個資源時,在資源不足以分配給所有執行緒的情況下為了保證所有所有執行緒能夠分配到資源可以使用類似於訊號燈的控制器來限流。Semaphore的建構函式需要傳入有限的資源數量個數。該類會維持這個資源數量,當執行緒呼叫acquire()函式時資源數減一;當執行緒呼叫release()函式時資源數加一;當執行緒請求資源時資源數為0則會被阻塞。

Semaphore semaphore = new Semaphore(3);
// 搶佔執行緒
for (int i = 0; i < 6; i++) {
    new Thread(() -> {
    try {
        semaphore.acquire();
        System.out.println(Thread.currentThread().getName() + "\t搶到");
        TimeUnit.SECONDS.sleep(3);
        System.out.println(Thread.currentThread().getName() + "\t離開");
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        semaphore.release();
    }
    }).start();
}

8. 阻塞佇列

  1. ArrayBlockingQueue:陣列構成的有限阻塞佇列
  2. LinkedBlockingQueue:連結串列構成的有限阻塞佇列(預設大小為Integer.MAX_VALUE)
  3. PriorityBlockingQueue:支援優先順序的無界阻塞佇列
  4. DelayQueue:支援優先順序的延遲無界阻塞佇列
  5. SynchronousQueue:不儲存元素的阻塞佇列,單個元素的佇列
  6. LinkedTransferQueue:連結串列組成的無界阻塞佇列
  7. LinkedBlockingDeque:連結串列組成的雙向阻塞佇列

    異常組函式

    BlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
    // 新增
    System.out.println(queue.add("one"));
    System.out.println(queue.add("two"));
    // 當佇列溢位時丟擲異常: java.lang.IllegalStateException
    // System.out.println(queue.add("three"));
    // 獲取下一個將被取出的元素
    System.out.println(queue.element());
    // 刪除
    System.out.println(queue.remove());
    System.out.println(queue.remove());
    // 刪除空佇列時丟擲異常: java.util.NoSuchElementException
    System.out.println(queue.remove());

    返回bool值組

    BlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
    // 新增
    // offer(e, time, unit): 可指定阻塞時間
    System.out.println(queue.offer("one"));
    System.out.println(queue.offer("two"));
    // 當佇列溢位時返回: false
    System.out.println(queue.offer("three"));
    // 獲取下一個將被取出的元素
    System.out.println(queue.peek());
    // 刪除
    // poll(time, unit): 可指定阻塞時間
    System.out.println(queue.poll());
    System.out.println(queue.poll());
    // 刪除空佇列時返回: null
    System.out.println(queue.poll());

    阻塞組

    BlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
    // 新增
    queue.put("one");
    queue.put("two");
    // 當佇列溢位時阻塞
    queue.put("three");
    queue.forEach(System.out::println);
    // 刪除
    System.out.println(queue.take());
    System.out.println(queue.take());
    // 刪除空佇列時阻塞
    System.out.println(queue.take());

    SynchronousQueue

    BlockingQueue<String> queue = new SynchronousQueue<>();
    // 新增
    queue.add("one");
    // 佇列溢位丟擲異常: java.lang.IllegalStateException
    queue.add("two");
    // 取出
    System.out.println(queue.take());

9. 執行緒池

9.1. 執行緒池7大引數

  1. corePoolSize: 執行緒池中的常駐核心執行緒數
  2. maximumPoolSize: 執行緒池能夠同時容納的最大執行緒數量
  3. keepAliveTime: 多餘的空閒執行緒的存活時間
  4. unit: keepAliveTime的時間單位
  5. workQueue: 任務佇列,在所有核心執行緒處於忙碌狀態時,新加入的執行緒會被放入任務佇列;當任務佇列溢位時,執行緒池會開啟新的執行緒直到執行緒數達到最大(任務佇列溢位時新加入的執行緒會搶先任務佇列裡的執行緒執行)
  6. threadFactory: 生成執行緒的工廠
  7. handler: 當執行緒池溢位時的拒絕策略

9.2. 拒絕策略

  1. AbortPolicy(預設):直接丟擲RejectedExecutionException異常
  2. CallerRunsPolicy:將任務回退到呼叫者,不會丟擲異常,也不會丟棄任務
  3. DiscardOldestPolicy:拋棄佇列中等待最久的任務,然後把當前任務加入任務佇列嘗試再次提交任務
  4. DiscardPolicy:直接丟棄任務,也不丟擲異常

9.3. 執行緒池的建立

阿里巴巴Java開發手冊中命令禁止使用Executors返回的執行緒池物件建立執行緒池:
Java面試題解析

// 手動建立執行緒池
int CPU_COUNT = Runtime.getRuntime().availableProcessors();
ExecutorService executorService = new ThreadPoolExecutor(
        CPU_COUNT / 2,
        CPU_COUNT * 2,
        1L,
        TimeUnit.SECONDS,
        new LinkedBlockingDeque<Runnable>(CPU_COUNT),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy()
);

如何確定合理的執行緒池個數?

  1. CPU密集型業務(計算量比較大,IO操作比較少):CPU核數 + 1
  2. IO密集型(阻塞係數:0.8~0.9)
    1. CPU核數 / (1 - 阻塞係數)
    2. CPU核數 * 2

10. 死鎖問題

Java死鎖演示

死鎖問題解決方案

calong > jps -l
10540
11436 jdk.jcmd/sun.tools.jps.Jps
12508 club.calong.jvm.demo.DeadLock
5788 org.jetbrains.jps.cmdline.Launcher
calong > jstack 12508
Java stack information for the threads listed above:
===================================================
"t3":
        at club.calong.jvm.demo.SyncThread.run(DeadLock.java:22)
        - waiting to lock <0x000000076b577cf8> (a java.lang.Object)
        - locked <0x000000076b577d18> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)
"t1":
        at club.calong.jvm.demo.SyncThread.run(DeadLock.java:22)
        - waiting to lock <0x000000076b577d08> (a java.lang.Object)
        - locked <0x000000076b577cf8> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)
"t2":
        at club.calong.jvm.demo.SyncThread.run(DeadLock.java:22)
        - waiting to lock <0x000000076b577d18> (a java.lang.Object)
        - locked <0x000000076b577d08> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.

11. JVM引數

  1. java -XX:+PrintFlagsInitial -version: 檢視JVM引數預設初始值
  2. java -XX:+PrintFlagsFinal -version: 檢視被修改過的JVM引數以及修改後的值
  3. java -XX:+PrintFlagsFinal -XX:引數=值 T: 修改JVM引數
  4. jinfo -flags PID: 檢視Java程式的JVM引數
  5. jinfo -flag 引數 PID: 檢視Java程式的指定JVM引數
  6. java -XX:+PrintCommandLineFlags: 檢視Java執行時命令列指定JVM引數

常用引數:

  1. -Xss(-XX:ThreadStackSize): 單個執行緒棧大小,預設為512~1024k
  2. -Xms(-XX:InitialHeapSize): 初始大小記憶體,預設為實體記憶體的1/64
  3. -Xmx(-XX:MaxHeapSize): 最大分配記憶體,預設為實體記憶體的1/4
  4. -XX: MetaspaceSize: 元空間大小,不在虛擬記憶體中,大小僅受本地記憶體限制,預設大小為20.8M
  5. -XX: PrintGCDetails: 檢視Java執行緒的CG執行日誌和記憶體佔用情況
  6. -XX: SurvivorRatio: 設定新生代伊甸園中S0/S1的空間比例
  7. -XX: NewRatio: 設定新生代和老年代堆記憶體結構佔比
  8. -XX: MaxTenuringThreshold: 設定垃圾最大年齡

12. 引用問題

  1. 強引用:Object obj = new Object();, 強引用無論發生任何情況都不會被回收。
  2. 軟引用:SoftReference<Object> obj = new SoftReference<>(new Object()), 當系統記憶體充足時不會被回收,不足時會被回收。
  3. 弱引用:WeakReference<Object> obj = new WeakReference<>(new Object()), 只要執行GC該物件就會被回收。在WeakHashMap<T>中如果將Key指向null後執行GC,則KeyValue都會被回收。
  4. 虛引用:PhantomReference<Object> obj = new PhantomRefence<>(new Object()), 形同虛設得引用物件。該物件必須配合引用佇列ReferenceQueue<T>使用。
    弱引用和虛引用物件在被GC回收之前會被放入引用佇列當中,使用引用佇列的方法poll()可以獲得被放入的物件。
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章