面試必備!就憑藉着這份Java 高頻面試題,我拿下了阿里,字節的offer!

List

1. 爲何 arraylist 不安全?

咱們查看源碼發現 arraylist 的 CRUD 操做,並無涉及到鎖之類的東西。底層是數組,初始大小爲 10。插入時會判斷數組容量是否足夠,不夠的話會進行擴容。所謂擴容就是新建一個新的數組,而後將老的數據裏面的元素複製到新的數組裏面(因此增長較慢)。java

2. CopyOnWriteArrayList 有什麼特色?

它是 List 接口的一個實現類,在 java.util.concurrent(簡稱 JUC,後面我所有改爲 juc,你們注意下)。node

內部持有一個 ReentrantLock lock = new ReentrantLock(); 對於增刪改操做都是先加鎖再釋放鎖,線程安全。而且鎖只有一把,而讀操做不須要得到鎖,支持併發。算法

讀寫分離,寫時複製出一個新的數組,完成插入、修改或者移除操做後將新數組賦值給 array。api

3. CopyOnWriteArrayList 與 Vector 的選擇?

Vector 是增刪改查方法都加了 synchronized,保證同步,可是每一個方法執行的時候都要去得到鎖,性能就會大大降低,而 CopyOnWriteArrayList 只是在增刪改上加鎖,可是讀不加鎖,在讀方面的性能就好於 Vector,CopyOnWriteArrayList 支持讀多寫少的併發狀況。數組

Vector 和 CopyOnWriteArrayList 都是 List 接口的一個實現類。安全

4. CopyOnWriteArrayList 適用於什麼狀況?

咱們看源碼不難發現他每次增長一個元素都要進行一次拷貝,此時嚴重影響了增刪改的性能,其中和 arraylist 差了好幾百倍。數據結構

因此對於讀多寫少的操做 CopyOnWriteArrayList 更加適合,並且線程安全。多線程

DriverManager 這個類就使用到了CopyOnWriteArrayList。併發

5. LinkedList  和 ArrayList  對比?

LinkedList<Integer> lists = new LinkedList<>();
 
 lists.addFirst(1);
 lists.push(2);
 lists.addLast(3);
 lists.add(4);
 lists.addFirst(5);
 
 lists.forEach(System.out::println);
// 5 2 1 3 4

addFirst 和 addLast 方法很清楚。push 方法默認是 addFirst 實現。add 方法默認是 addLast 實現。因此總結一下就是 add 和 last,push 和 first。ide

其實咱們要明白一下,鏈表相對於數組來講,鏈表的添加和刪除速度很快,是順序添加刪除很快,由於一個 linkedList 會保存第一個節點和最後一個節點,時間複雜度爲O(1),可是你要指定位置添加 add(int index, E element) ,那麼此時他會先遍歷,而後找到改位置的節點,將你的節點添加到他前面,此時時間複雜度最大值爲 O(n)。

數組呢?咱們知道 ArrayList 底層實現就是數組,數組優勢就是因爲內存地址是順序的,屬於一塊整的,此時遍歷起來很快,添加刪除的話,他會複製數組,當數組長度特別大時所消耗的時間會很長。這是一張圖,你們能夠看一下:

6. Arrays.asList() 方法返回的數組是不可變得嗎?

List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
integers.set(2, 5); // 這個操做能夠
//integers.add(6);  這個會拋出異常
integers.forEach(System.out::println); // 1 2 5 4 5

1. 很顯然咱們是能夠修改 list集合的 可使用set方法
2. 可是當咱們嘗試去使用add() 方法時,會拋出 java.lang.UnsupportedOperationException 的異常,
不支持操做的異常
3.當咱們使用 java9+時  可使用 List.of()方法 ,他就是不折不扣的不可修改的

7. 怎麼將一個不安全數組換成安全數組?

1. 使用 Collections這個工具類
List<Integer> integers1 = Collections.synchronizedList(integers);

2. java5+ 變成 CopyOnWriteArrayList
CopyOnWriteArrayList<Integer> integers2 = (CopyOnWriteArrayList<Integer>) integers;

3. java9+ ,使用 List.of() 變成只讀對象

8. Collections 工具類?

1. 建立一個安全的空集合,防止NullPointerException異常
 List<String> list = Collections.<String>emptyList();
 
 2. 拷貝集合
 Collections.addAll(list, 2,3, 4, 5, 6);
 
 3. 構建一個安全的集合
 List<Integer> safeList = Collections.synchronizedList(list);
 
 4. 二分查找
 Collections.binarySearch(list, 2);

 5.翻轉數組
 Collections.reverse(list);

Set

1. HashSet、TreeSet 和 LinkedHashSet 三種類型何時使用它們?

如你的需求是要一個能快速訪問的 Set,那麼就要用 HashSet,HashSet 底層是 HashMap 實現的,其中的元素沒有按順序排列。

若是你要一個可排序 Set,那麼你應該用 TreeSet,TreeSet 的底層實現是 TreeMap。

若是你要記錄下插入時的順序時,你應該使用 LinedHashSet。

Set 集合中不能包含重複的元素,每一個元素必須是惟一的,你只要將元素加入 set 中,重複的元素會自動移除。因此能夠去重,不少狀況下都須要使用(可是去重方式不一樣)。

LinkedHashSet 正好介於 HashSet 和 TreeSet 之間,它也是一個基於 HashMap 和雙向鏈表的集合,但它同時維護了一個雙鏈表來記錄插入的順序,基本方法的複雜度爲 O(1)。

三者都是線程不安全的,須要使用 Collections.synchronizedSet(new HashSet(…));。

2. HashSet 和 LinkedHashSet 斷定元素重複的原則是相同的?

會先去執行 hashCode() 方法,判斷是否重複。若是 hashCode() 返回值相同,就會去判斷 equals 方法。若是 equals() 方法仍是相同,那麼就認爲重複。

3. TreeSet 判斷元素重複原則?

TreeSet 的元素必須是實現了 java.lang.Comparable<T> 接口,因此他是根據此個接口的方法 compareTo 方法進行判斷重複的,當返回值同樣的時認定重複。

4. 怎麼實現一個線程安全的 hashset?

咱們看源碼會發現他裏面有一個 HashMap(用 transient 關鍵字標記的成員變量不參與序列化過程,由於 HashMap 已經實現 Serializable)。

5. CopyOnWriteArraySet 的實現?

1 public CopyOnWriteArraySet() {
2     al = new CopyOnWriteArrayList<E>();
3 }

很顯然翻源碼咱們發現他實現了 CopyOnWriteArrayList()。

Map

1. Hashtable 特色?

Hashtable 和 ConcurrentHashMap 以及 ConcurrentSkipListMap 以及 TreeMap 不容許 key 和 value 值爲空,可是 HashMap 能夠 key 和 value 值均可覺得空。

Hashtable 的方法都加了 Synchronized 關鍵字修飾,因此線程安全。

它是數組+鏈表的實現。

2. ConcurrentHashMap 問題?

取消 segments 字段,直接採用 transient volatile HashEntry<K,V>[] table 保存數據。

採用 table 數組元素做爲鎖,從而實現了對每一行數據進行加鎖,進一步減小併發衝突的機率。

把 Table 數組+單向鏈表的數據結構變成爲 Table 數組 + 單向鏈表 + 紅黑樹的結構。

當鏈表長度超過 8 之後,單向鏈表變成了紅黑數;在哈希表擴容時,若是發現鏈表長度小於 6,則會由紅黑樹從新退化爲鏈表。

對於其餘詳細我不吹,看懂的麼幾個,他比 HashMap 還要難。

對於線程安全環境下介意使用 ConcurrentHashMap 而不去使用 Hashtable。

3. 爲何不去使用 Hashtable 而去使用 ConcurrentHashMap?

HashTable 容器使用 synchronized 來保證線程安全,但在線程競爭激烈的狀況下 HashTable 的效率很是低下。由於當一個線程訪問 HashTable 的同步方法時,其餘線程訪問 HashTable 的同步方法時,可能會進入阻塞或輪詢狀態。如線程 1 使用 put 進行添加元素,線程 2 不但不能使用 put 方法添加元素,而且也不能使用 get 方法來獲取元素,因此競爭越激烈效率越低。

4. ConcurrentSkipListMap 與 TreeMap 的選擇?

ConcurrentSkipListMap 提供了一種線程安全的併發訪問的排序映射表。內部是 SkipList(跳錶)結構實現,利用底層的插入、刪除的 CAS 原子性操做,經過死循環不斷獲取最新的結點指針來保證不會出現競態條件。在理論上可以在 O(log(n)) 時間內完成查找、插入、刪除操做。調用 ConcurrentSkipListMap 的 size 時,因爲多個線程能夠同時對映射表進行操做,因此映射表須要遍歷整個鏈表才能返回元素個數,這個操做是個 O(log(n)) 的操做。

在 JDK1.8 中,ConcurrentHashMap 的性能和存儲空間要優於 ConcurrentSkipListMap,可是 ConcurrentSkipListMap 有一個功能:它會按照鍵的天然順序進行排序。

故須要對鍵值排序,則咱們可使用 TreeMap,在併發場景下可使用 ConcurrentSkipListMap。

因此咱們並不會去糾結 ConcurrentSkipListMap 和 ConcurrentHashMap 二者的選擇。

5. LinkedHashMap 的使用?

主要是爲了解決讀取的有序性。基於 HashMap 實現的。

Queue

1. 隊列是什麼?

咱們都知道隊列 (Queue) 是一種先進先出 (FIFO) 的數據結構,Java 中定義了 java.util.Queue 接口用來表示隊列。Java 中的 Queue 與 List、Set 屬於同一個級別接口,它們都是實現了 Collection 接口。

注意:HashMap 沒有實現 Collection 接口。

2. Deque 是什麼?

它是一個雙端隊列。咱們用到的 linkedlist 就是實現了 deque 的接口。支持在兩端插入和移除元素。

3. 常見的幾種隊列實現?

LinkedList 是鏈表結構,隊列呢也是一個列表結構,繼承關係上 LinkedList 實現了 Queue,因此對於 Queue 來講,添加是 offer(obj),刪除是 poll(),獲取隊頭(不刪除)是 peek() 。

1public static void main(String[] args) {
 2    Queue<Integer> queue = new LinkedList<>();
 3
 4    queue.offer(1);
 5    queue.offer(2);
 6    queue.offer(3);
 7
 8    System.out.println(queue.poll());
 9    System.out.println(queue.poll());
10    System.out.println(queue.poll());
11}
12// 1, 2 , 3

PriorityQueue 維護了一個有序列表,插入或者移除對象會進行 Heapfy 操做,默認狀況下能夠稱之爲小頂堆。固然,咱們也能夠給它指定一個實現了 java.util.Comparator 接口的排序類來指定元素排列的順序。PriorityQueue 是一個無界隊列,當你設置初始化大小仍是不設置都不影響他繼續添加元素。

ConcurrentLinkedQueue 是基於連接節點的而且線程安全的隊列。由於它在隊列的尾部添加元素並從頭部刪除它們,因此只要不須要知道隊列的大小 ConcurrentLinkedQueue 對公共集合的共享訪問就能夠工做得很好。收集關於隊列大小的信息會很慢,須要遍歷隊列。

4. ArrayBlockingQueue 與 LinkedBlockingQueue 的區別,哪一個性能好呢?

ArrayBlockingQueue 是有界隊列。LinkedBlockingQueue 看構造方法區分,默認構造方法最大值是 2^31-1。可是當 take 和 put 操做時,ArrayBlockingQueue 速度要快於 LinkedBlockingQueue。

ArrayBlockingQueue 中的鎖是沒有分離的,即生產和消費用的是同一個鎖。LinkedBlockingQueue 中的鎖是分離的,即生產用的是 putLock,消費是 takeLock;ArrayBlockingQueue 基於數組,在生產和消費的時候,是直接將枚舉對象插入或移除的,不會產生或銷燬任何額外的對象實例;LinkedBlockingQueue 基於鏈表,在生產和消費的時候,須要把枚舉對象轉換爲 Node 進行插入或移除,會生成一個額外的 Node 對象,這在長時間內須要高效併發地處理大批量數據的系統中,其對於 GC 的影響仍是存在必定的區別。

LinkedBlockingQueue 的消耗是 ArrayBlockingQueue 消耗的 10 倍左右,即 LinkedBlockingQueue 消耗在 1500 毫秒左右,而 ArrayBlockingQueue 只需 150 毫秒左右。

按照實現原理來分析,ArrayBlockingQueue 徹底能夠採用分離鎖,從而實現生產者和消費者操做的徹底並行運行。Doug Lea 之因此沒這樣去作,也許是由於 ArrayBlockingQueue 的數據寫入和獲取操做已經足夠輕巧,以致於引入獨立的鎖機制,除了給代碼帶來額外的複雜性外,其在性能上徹底佔不到任何便宜。

在使用 LinkedBlockingQueue 時,若用默認大小且當生產速度大於消費速度時候,有可能會內存溢出。

在使用 ArrayBlockingQueue 和 LinkedBlockingQueue 分別對 1000000 個簡單字符作入隊操做時,咱們測試的是 ArrayBlockingQueue 會比 LinkedBlockingQueue 性能好 , 好差很少 50% 起步。

5. BlockingQueue 的問題以及 ConcurrentLinkedQueue 的問題?

BlockingQueue 能夠是限定容量的。

BlockingQueue 實現主要用於生產者-使用者隊列,但它另外還支持 collection 接口。

BlockingQueue 實現是線程安全的。

BlockingQueue 是阻塞隊列(看你使用的方法),ConcurrentLinkedQueue 是非阻塞隊列。

LinkedBlockingQueue 是一個線程安全的阻塞隊列,基於鏈表實現,通常用於生產者與消費者模型的開發中。採用鎖機制來實現多線程同步,提供了一個構造方法用來指定隊列的大小,若是不指定大小,隊列採用默認大小(Integer.MAX_VALUE,即整型最大值)。

ConcurrentLinkedQueue 是一個線程安全的非阻塞隊列,基於鏈表實現。java 並無提供構造方法來指定隊列的大小,所以它是無界的。爲了提升併發量,它經過使用更細的鎖機制,使得在多線程環境中只對部分數據進行鎖定,從而提升運行效率。他並無阻塞方法,take 和 put 方法,注意這一點。

6. 簡要概述 BlockingQueue 經常使用的七個實現類?

ArrayBlockingQueue 構造函數必須傳入指定大小,因此他是一個有界隊列。

LinkedBlockingQueue 分爲兩種狀況,第一種構造函數指定大小,他是一個有界隊列,第二種狀況不指定大小他能夠稱之爲無界隊列,隊列最大值爲 Integer.MAX_VALUE。

PriorityBlockingQueue(還有一個雙向的 LinkedBlockingDeque)他是一個無界隊列,無論你使用什麼構造函數。一個內部由優先級堆支持的、基於時間的調度隊列。隊列中存放 Delayed 元素,只有在延遲期滿後才能從隊列中提取元素。當一個元素的 getDelay() 方法返回值小於等於 0 時才能從隊列中 poll 中元素,不然 poll() 方法會返回 null。 

SynchronousQueue 這個隊列相似於 Golang的channel,也就是 chan,跟無緩衝區的 chan 很類似。好比 take 和 put 操做就跟 chan 如出一轍。可是區別在於他的 poll 和 offer 操做能夠設置等待時間。

DelayQueue 延遲隊列提供了在指定時間才能獲取隊列元素的功能,隊列頭元素是最接近過時的元素。沒有過時元素的話,使用 poll() 方法會返回 null 值,超時斷定是經過 getDelay(TimeUnit.NANOSECONDS) 方法的返回值小於等於 0 來判斷。延時隊列不能存放空元素。

添加的元素必須實現 java.util.concurrent.Delayed 接口:

1@Test
 2public void testLinkedList() throws InterruptedException {
 3
 4    DelayQueue<Person> queue = new DelayQueue<>();
 5
 6    queue.add(new Person());
 7
 8    System.out.println("queue.poll() = " + queue.poll(200,TimeUnit.MILLISECONDS));
 9}
10
11
12static class Person implements Delayed {
13
14    @Override
15    public long getDelay(TimeUnit unit) {
16        // 這個對象的過時時間
17        return 100L;
18    }
19
20    @Override
21    public int compareTo(Delayed o) {
22        //比較
23        return o.hashCode() - this.hashCode();
24    }
25}
26
27輸出 : 
28queue.poll() = null

LinkedTransferQueue 是 JDK1.7 加入的無界隊列,亮點就是無鎖實現的,性能高。Doug Lea 說這個是最有用的 BlockingQueue 了,性能最好的一個。Doug Lea 說從功能角度來說,LinkedTransferQueue 其實是 ConcurrentLinkedQueue、SynchronousQueue(公平模式)和 LinkedBlockingQueue 的超集。他的 transfer 方法表示生產必須等到消費者消費纔會中止阻塞。生產者會一直阻塞直到所添加到隊列的元素被某一個消費者所消費(不只僅是添加到隊列裏就完事)。同時咱們知道上面那些 BlockingQueue 使用了大量的 condition 和 lock,這樣子效率很低,而 LinkedTransferQueue 則是無鎖隊列。他的核心方法其實就是 xfer() 方法,基本全部方法都是圍繞着這個進行的,通常就是 SYNC、ASYNC、NOW 來區分狀態量。像 put、offer、add 都是 ASYNC,因此不會阻塞。下面幾個狀態對應的變量。

1private static final int NOW   = 0; // for untimed poll, tryTransfer(不阻塞)
2private static final int ASYNC = 1; // for offer, put, add(不阻塞)
3private static final int SYNC  = 2; // for transfer, take(阻塞)
4private static final int TIMED = 3; // for timed poll, tryTransfer (waiting)

7. (小頂堆) 優先隊列 PriorityQueue 的實現?

小頂堆是什麼:任意一個非葉子節點的權值都不大於其左右子節點的權值。

PriorityQueue 是非線程安全的,PriorityBlockingQueue 是線程安全的。

二者都使用了堆,算法原理相同。

PriorityQueue 的邏輯結構是一棵徹底二叉樹,就是由於徹底二叉樹的特色,他實際存儲確實能夠爲一個數組的,因此他的存儲結構實際上是一個數組。

首先 java 中的 PriorityQueue 是優先隊列,使用的是小頂堆實現,所以結果不必定是徹底升序。

8. 本身實現一個大頂堆?

1/**
 2 * 構建一個 大頂堆
 3 *
 4 * @param tree
 5 * @param n
 6 */
 7static void build_heap(int[] tree, int n) {
 8
 9    // 最後一個節點
10    int last_node = n - 1;
11
12    // 開始遍歷的位置是 : 最後一個堆的堆頂 , (以最小堆爲單位)
13    int parent = (last_node - 1) / 2;
14
15    // 遞減向上遍歷
16    for (int i = parent; i >= 0; i--) {
17        heapify(tree, n, i);
18    }
19}
20
21
22/**
23 * 遞歸操做
24 * @param tree 表明一棵樹
25 * @param n    表明多少個節點
26 * @param i    對哪一個節點進行 heapify
27 */
28static void heapify(int[] tree, int n, int i) {
29
30    // 若是當前值 大於 n 直接返回了 ,通常不會出現這種問題 .....
31    if (i >= n) {
32        return;
33    }
34
35    // 子節點
36    int c1 = 2 * i + 1;
37    int c2 = 2 * i + 2;
38
39    // 假設最大的節點 爲 i (父節點)
40    int max = i;
41
42    // 若是大於  賦值給 max
43    if (c1 < n && tree[c1] > tree[max]) {
44        max = c1;
45    }
46
47    // 若是大於  賦值給 max
48    if (c2 < n && tree[c2] > tree[max]) {
49        max = c2;
50    }
51
52    // 若是i所在的就是最大值咱們不必去作交換
53    if (max != i) {
54
55        // 交換最大值 和 父節點 的位置
56        swap(tree, max, i);
57
58        // 交換完之後 , 此時的max其實就是 i原來的數 ,就是最小的數字 ,因此須要遞歸遍歷
59        heapify(tree, n, max);
60    }
61
62}
63
64// 交換操做
65static void swap(int[] tree, int max, int i) {
66    int temp = tree[max];
67    tree[max] = tree[i];
68    tree[i] = temp;
69}

Stack

棧結構屬於一種先進者後出,相似於一個瓶子,先進去的會壓到棧低(push 操做),出去的時候只有一個出口就是棧頂,返回棧頂元素,這個操做稱爲 pop。

Stack 類繼承自 Vector,全部方法都加入了 sync 修飾,使得效率很低,線程安全。

1@Test
 2public void testStack() {
 3
 4    Stack<Integer> stack = new Stack<>();
 5
 6    // push 添加
 7    stack.push(1);
 8
 9    stack.push(2);
10
11    // pop 返回棧頂元素 , 並移除
12    System.out.println("stack.pop() = " + stack.pop());
13
14    System.out.println("stack.pop() = " + stack.pop());
15
16}
17
18輸出 : 
192 , 1
相關文章
相關標籤/搜索