java經典面試題

小咖啡111發表於2019-04-16

1)Java 中能建立 volatile 陣列嗎?
能,Java 中可以建立 volatile 型別陣列,不過只是一個指向陣列的引用,而不是整個陣列。我的意思是,如果改變引用指向的陣列,將會受到 volatile 的保護,但是如果多個執行緒同時改變陣列的元素,volatile 標示符就不能起到之前的保護作用了。

2)volatile 能使得一個非原子操作變成原子操作嗎?
一個典型的例子是在類中有一個 long 型別的成員變數。如果你知道該成員變數會被多個執行緒訪問,如計數器、價格等,你最好是將其設定為 volatile。為什麼?因為 Java 中讀取 long 型別變數不是原子的,需要分成兩步,如果一個執行緒正在修改該 long 變數的值,另一個執行緒可能只能看到該值的一半(前 32 位)。但是對一個 volatile 型的 long 或 double 變數的讀寫是原子。

3)volatile 修飾符的有過什麼實踐?
一種實踐是用 volatile 修飾 long 和 double 變數,使其能按原子型別來讀寫。double 和 long 都是64位寬,因此對這兩種型別的讀是分為兩部分的,第一次讀取第一個 32 位,然後再讀剩下的 32 位,這個過程不是原子的,但 Java 中 volatile 型的 long 或 double 變數的讀寫是原子的。volatile 修復符的另一個作用是提供記憶體屏障(memory barrier),例如在分散式框架中的應用。簡單的說,就是當你寫一個 volatile 變數之前,Java 記憶體模型會插入一個寫屏障(write barrier),讀一個 volatile 變數之前,會插入一個讀屏障(read barrier)。意思就是說,在你寫一個 volatile域時,能保證任何執行緒都能看到你寫的值,同時,在寫之前,也能保證任何數值的更新對所有執行緒是可見的,因為記憶體屏障會將其他所有寫的值更新到快取。

4)volatile 型別變數提供什麼保證?
volatile 變數提供順序和可見性保證,例如,JVM 或者 JIT為了獲得更好的效能會對語句重排序,但是 volatile 型別變數即使在沒有同步塊的情況下賦值也不會與其他語句重排序。 volatile 提供 happens-before 的保證,確保一個執行緒的修改能對其他執行緒是可見的。某些情況下,volatile 還能提供原子性,如讀 64 位資料型別,像 long 和 double 都不是原子的,但 volatile 型別的 double 和 long 就是原子的。

5) 10 個執行緒和 2 個執行緒的同步程式碼,哪個更容易寫?
從寫程式碼的角度來說,兩者的複雜度是相同的,因為同步程式碼與執行緒數量是相互獨立的。但是同步策略的選擇依賴於執行緒的數量,因為越多的執行緒意味著更大的競爭,所以你需要利用同步技術,如鎖分離,這要求更復雜的程式碼和專業知識。

6)你是如何呼叫 wait()方法的?使用 if 塊還是迴圈?為什麼?
wait() 方法應該在迴圈呼叫,因為當執行緒獲取到 CPU 開始執行的時候,其他條件可能還沒有滿足,所以在處理前,迴圈檢測條件是否滿足會更好。下面是一段標準的使用 wait 和 notify 方法的程式碼:

// The standard idiom for using the wait method
synchronized (obj) {
while (condition does not hold)
obj.wait(); // (Releases lock, and reacquires on wakeup)
... // Perform action appropriate to condition
}

7)Java 中 sleep 方法和 wait 方法的區別?(答案)
雖然兩者都是用來暫停當前執行的執行緒,但是 sleep() 實際上只是短暫停頓,因為它不會釋放鎖,而 wait() 意味著條件等待,這就是為什麼該方法要釋放鎖,因為只有這樣,其他等待的執行緒才能在滿足條件時獲取到該鎖。

8)什麼是不可變物件(immutable object)?Java 中怎麼建立一個不可變物件?
不可變物件指物件一旦被建立,狀態就不能再改變。任何修改都會建立一個新的物件,如 String、Integer及其它包裝類。詳情參見答案,一步一步指導你在 Java 中建立一個不可變的類。

9)我們能建立一個包含可變物件的不可變物件嗎?
是的,我們是可以建立一個包含可變物件的不可變物件的,你只需要謹慎一點,不要共享可變物件的引用就可以了,如果需要變化時,就返回原物件的一個拷貝。最常見的例子就是物件中包含一個日期物件的引用。

10)在多執行緒環境中使用HashMap有什麼問題?

HashMap 在併發時可能出現的問題主要是兩方面,首先如果多個執行緒同時使用put方法新增元素,而且假設正好存在兩個 put 的 key 發生了碰撞(根據 hash 值計算的 bucket 一樣),那麼根據 HashMap 的實現,這兩個 key 會新增到陣列的同一個位置,這樣最終就會發生其中一個執行緒的 put 的資料被覆蓋。第二就是如果多個執行緒同時檢測到元素個數超過陣列大小* loadFactor ,這樣就會發生多個執行緒同時對 Node 陣列進行擴容,都在重新計算元素位置以及複製資料,但是最終只有一個執行緒擴容後的陣列會賦給 table,也就是說其他執行緒的都會丟失,並且各自執行緒 put 的資料也丟失。

關於 HashMap 執行緒不安全這一點,《Java併發程式設計的藝術》一書中是這樣說的:
HashMap 在併發執行 put 操作時會引起死迴圈,導致 CPU 利用率接近100%。因為多執行緒會導致 HashMap 的 Node 連結串列形成環形資料結構,一旦形成環形資料結構,Node 的 next 節點永遠不為空,就會在獲取 Node 時產生死迴圈。

多執行緒環境下應該使用:Hashtable或者ConcurrentHashMap

> HashMap 在 new 後並不會立即分配bucket陣列,而是第一次 put 時初始化,類似 ArrayList 在第一次add時分配空間。
> HashMap 的 bucket 陣列大小一定是2的冪,如果 new 的時候指定了容量且不是2的冪,實際容量會是最接近(大於)指定容量的2的冪,比如 new HashMap<>(19),比19大且最接近的2的冪是32,實際容量就是32。
> HashMap 在 put 的元素數量大於 Capacity * LoadFactor(預設16 * 0.75) 之後會進行擴容。
> JDK8在雜湊碰撞的連結串列長度達到TREEIFY_THRESHOLD(預設8)後,會把該連結串列轉變成樹結構,提高了效能。
> JDK8在 resize 的時候,通過巧妙的設計,減少了 rehash 的效能消耗。
> JDK7 中的 HashMap 還是採用大家所熟悉的陣列+連結串列的結構來儲存資料。

> JDK8 中的 HashMap 採用了陣列+連結串列或樹的結構來儲存資料。

11)多重繼承會帶來哪些問題?
混淆。比如B和C繼承於A,然後B和C分別覆蓋(override)了A中的同一個方法。最後,D又分別繼承於B和C,那D到底該使用B和C中的哪一個方法呢?

 12)String物件為什麼被設計成不可變的?

>字串常量池的需要
>允許String物件快取hashcode(String物件的雜湊碼被頻繁地使用, 比如在hashMap等容器中。)
>安全性

13)HashTable和ConCurrentHashMap的對比
>Hashtable是對其put方法通過synchronized關鍵字對整個方法加鎖,每執行一次put方法都會阻塞當前執行緒,所以當Hashtable的大小增加到一定的時候,效能會急劇下降,因為迭代時需要被鎖定很長的時間。
>ConcurrentHashMap引入了分割(Segment),可以理解為把一個大的Map拆分成N個小的HashTable,在put方法中,會根據hash(paramK.hashCode())來決定具體存放進哪個Segment,如果檢視Segment的put操作,我們會發現內部使用的同步機制是基於lock操作的,這樣就可以對Map的一部分(Segment)進行上鎖,這樣影響的只是將要放入同一個Segment的元素的put操作,保證同步的時候,鎖住的不是整個Map(HashTable就是這麼做的),相對於HashTable提高了多執行緒環境下的效能,因此HashTable已經被淘汰了。

 14)String.intern()方法的用法

如果類的字串常量池中沒有這個字串,則先放到類的字串常量池中,然後返回這個字串的引用;如果類的字串常量池中有這個字串,則直接返回其引用。
使用intern()方法可以在程式執行期間動態擴充類的字串常量池,這個方法的最大好處是可以避免建立不必要的字串例項物件。

import java.util.Random;

public class Test {
    static final int MAX = 10000;
    static final String[] arr = new String[MAX];

    public static void main(String[] args) {
        Integer[] DB_DATA = new Integer[10];
        Random random = new Random(10 * 10000);
        for (int i = 0; i < DB_DATA.length; i++) {
            DB_DATA[i] = random.nextInt(99 * 10000);
        }
        long t = System.currentTimeMillis();
        for (int i = 0; i < MAX; i++) {
            //  這個寫法不管字串的值是否相同,都會建立1w個字串物件
            // arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));

            // 下面這個會先看字串常量池中是否有這個字串,有的話直接返回引用,不用重新建立例項了。
            arr[i] = String.valueOf(DB_DATA[i % DB_DATA.length]).intern();
        }
        System.out.println((System.currentTimeMillis() - t) + "ms");
        System.gc();
    }
}

15)List和Set的區別

List是有序&線性地儲存,可存放重複的物件;Set無序存放,不允許存放重複的物件,null值只允許存放1次。

16)ArrayList和LinkedList的區別

ArrayList內部採用的的是資料結構是陣列,陣列預設的capacity為10。陣列裡面的元素中間插入或刪除時都可能會進行arraycopy操作,所以插入和刪除操作效能較低(慢);但查詢速度快。

LinkedList內部的資料結構是雙向連結串列,中間插入或刪除開銷不大(只需要改變相鄰node的pre和next引用即可),但隨即查詢較慢(分段遍歷查詢,把node的數量分成前後2部分,看索引落在哪個部分,落在哪段就在哪段遍歷查詢。落在前半部分則往後遍歷,落在後半部分則往前遍歷)。

下圖是ArrayList和LinkedList的在各場景下的效能比較:

17)存取相同的資料ArrayList和LinkedList誰佔的空間更大?

空間佔用上,ArrayList完勝。可以參考:https://www.cnblogs.com/frankyou/p/9598439.html

18)TreeSet對存入資料有什麼要求?

①不能存入null值 ②資料型別必須相同

19)常見的Set實現有哪些?

①HashSet:基於HashMap,通過HashMap的key來存放物件,HashMap的value是一個固定的object物件。可以設定這個HashMap的initialCapacity。

②TreeSet:通過傳入型別的compareTo方法進行比較排序(前提是這些指定的型別實現了compareTo方法),也可以實現自己的compare方法來自定義排序。

20)class.forName()和ClassLoader.loadClass()的區別是什麼?

1️⃣Class.forName(className)裝載的class已經被初始化;
2️⃣ClassLoader.loadClass(className)裝載的class還沒有被link。

 21)獲取一個例項物件的Class物件的3種方式

①通過例項變數的getClass()方法

Dog dog = new Dog();
Class d = dog.getClass();

②通過類Class的靜態方法forName()

try {
    Class dog1 = Class.forName("Dog");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

③直接給出物件類檔案的.class

Class dog2 = Dog.class;

 22)JDK和CGLIB生成動態代理類的區別

①JDK動態代理是利用反射機制生成一個實現代理目標物件的介面的匿名類,在呼叫具體方法前呼叫InvokeHandler來處理。

②CGLIB動態代理是利用asm開源包,把目標代理物件類的class檔案載入進來,通過修改其位元組碼生成子類來處理。

1、如果目標物件實現了介面,預設情況下會採用JDK的動態代理實現AOP
2、如果目標物件實現了介面,可以強制使用CGLIB實現AOP
3、如果目標物件沒有實現了介面,必須採用CGLIB庫,spring會自動在JDK動態代理和CGLIB之間轉換

 <!--強制使用CGLIB實現代理-->
 <aop:aspectj-autoproxy proxy-target-class="true"/>

 動態代理在Spring中的應用主要體現在AOP上。

23)淺拷貝和深拷貝的區別

①淺拷貝:對於基本資料型別進行值傳遞,對引用型別進行引用拷貝。

 

②深拷貝:對於基本資料型別進行值傳遞,對於引用型別先建立一個新的物件並複製其內容。

 24)Class.forName和ClassLoader.loadClass的區別

Class.forName載入的類已做了初始化;ClassLoader.loadClass載入的class還沒有被link。

相關文章