咱們查看源碼發現 arraylist 的 CRUD 操做,並無涉及到鎖之類的東西。底層是數組,初始大小爲 10。插入時會判斷數組容量是否足夠,不夠的話會進行擴容。所謂擴容就是新建一個新的數組,而後將老的數據裏面的元素複製到新的數組裏面(因此增長較慢)。java
它是 List 接口的一個實現類,在 java.util.concurrent(簡稱 JUC,後面我所有改爲 juc,你們注意下)。node
內部持有一個 ReentrantLock lock = new ReentrantLock(); 對於增刪改操做都是先加鎖再釋放鎖,線程安全。而且鎖只有一把,而讀操做不須要得到鎖,支持併發。算法
讀寫分離,寫時複製出一個新的數組,完成插入、修改或者移除操做後將新數組賦值給 array。api
Vector 是增刪改查方法都加了 synchronized,保證同步,可是每一個方法執行的時候都要去得到鎖,性能就會大大降低,而 CopyOnWriteArrayList 只是在增刪改上加鎖,可是讀不加鎖,在讀方面的性能就好於 Vector,CopyOnWriteArrayList 支持讀多寫少的併發狀況。數組
Vector 和 CopyOnWriteArrayList 都是 List 接口的一個實現類。安全
咱們看源碼不難發現他每次增長一個元素都要進行一次拷貝,此時嚴重影響了增刪改的性能,其中和 arraylist 差了好幾百倍。數據結構
因此對於讀多寫少的操做 CopyOnWriteArrayList 更加適合,並且線程安全。多線程
DriverManager 這個類就使用到了CopyOnWriteArrayList。併發
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 底層實現就是數組,數組優勢就是因爲內存地址是順序的,屬於一塊整的,此時遍歷起來很快,添加刪除的話,他會複製數組,當數組長度特別大時所消耗的時間會很長。這是一張圖,你們能夠看一下:
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()方法 ,他就是不折不扣的不可修改的
1. 使用 Collections這個工具類 List<Integer> integers1 = Collections.synchronizedList(integers); 2. java5+ 變成 CopyOnWriteArrayList CopyOnWriteArrayList<Integer> integers2 = (CopyOnWriteArrayList<Integer>) integers; 3. java9+ ,使用 List.of() 變成只讀對象
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,那麼就要用 HashSet,HashSet 底層是 HashMap 實現的,其中的元素沒有按順序排列。
若是你要一個可排序 Set,那麼你應該用 TreeSet,TreeSet 的底層實現是 TreeMap。
若是你要記錄下插入時的順序時,你應該使用 LinedHashSet。
Set 集合中不能包含重複的元素,每一個元素必須是惟一的,你只要將元素加入 set 中,重複的元素會自動移除。因此能夠去重,不少狀況下都須要使用(可是去重方式不一樣)。
LinkedHashSet 正好介於 HashSet 和 TreeSet 之間,它也是一個基於 HashMap 和雙向鏈表的集合,但它同時維護了一個雙鏈表來記錄插入的順序,基本方法的複雜度爲 O(1)。
三者都是線程不安全的,須要使用 Collections.synchronizedSet(new HashSet(…));。
會先去執行 hashCode() 方法,判斷是否重複。若是 hashCode() 返回值相同,就會去判斷 equals 方法。若是 equals() 方法仍是相同,那麼就認爲重複。
TreeSet 的元素必須是實現了 java.lang.Comparable<T> 接口,因此他是根據此個接口的方法 compareTo 方法進行判斷重複的,當返回值同樣的時認定重複。
5. CopyOnWriteArraySet 的實現?
1 public CopyOnWriteArraySet() { 2 al = new CopyOnWriteArrayList<E>(); 3 }
很顯然翻源碼咱們發現他實現了 CopyOnWriteArrayList()。
取消 segments 字段,直接採用 transient volatile HashEntry<K,V>[] table 保存數據。
採用 table 數組元素做爲鎖,從而實現了對每一行數據進行加鎖,進一步減小併發衝突的機率。
把 Table 數組+單向鏈表的數據結構變成爲 Table 數組 + 單向鏈表 + 紅黑樹的結構。
當鏈表長度超過 8 之後,單向鏈表變成了紅黑數;在哈希表擴容時,若是發現鏈表長度小於 6,則會由紅黑樹從新退化爲鏈表。
對於其餘詳細我不吹,看懂的麼幾個,他比 HashMap 還要難。
對於線程安全環境下介意使用 ConcurrentHashMap 而不去使用 Hashtable。
HashTable 容器使用 synchronized 來保證線程安全,但在線程競爭激烈的狀況下 HashTable 的效率很是低下。由於當一個線程訪問 HashTable 的同步方法時,其餘線程訪問 HashTable 的同步方法時,可能會進入阻塞或輪詢狀態。如線程 1 使用 put 進行添加元素,線程 2 不但不能使用 put 方法添加元素,而且也不能使用 get 方法來獲取元素,因此競爭越激烈效率越低。
ConcurrentSkipListMap 提供了一種線程安全的併發訪問的排序映射表。內部是 SkipList(跳錶)結構實現,利用底層的插入、刪除的 CAS 原子性操做,經過死循環不斷獲取最新的結點指針來保證不會出現競態條件。在理論上可以在 O(log(n)) 時間內完成查找、插入、刪除操做。調用 ConcurrentSkipListMap 的 size 時,因爲多個線程能夠同時對映射表進行操做,因此映射表須要遍歷整個鏈表才能返回元素個數,這個操做是個 O(log(n)) 的操做。
在 JDK1.8 中,ConcurrentHashMap 的性能和存儲空間要優於 ConcurrentSkipListMap,可是 ConcurrentSkipListMap 有一個功能:它會按照鍵的天然順序進行排序。
故須要對鍵值排序,則咱們可使用 TreeMap,在併發場景下可使用 ConcurrentSkipListMap。
因此咱們並不會去糾結 ConcurrentSkipListMap 和 ConcurrentHashMap 二者的選擇。
主要是爲了解決讀取的有序性。基於 HashMap 實現的。
咱們都知道隊列 (Queue) 是一種先進先出 (FIFO) 的數據結構,Java 中定義了 java.util.Queue 接口用來表示隊列。Java 中的 Queue 與 List、Set 屬於同一個級別接口,它們都是實現了 Collection 接口。
注意:HashMap 沒有實現 Collection 接口。
它是一個雙端隊列。咱們用到的 linkedlist 就是實現了 deque 的接口。支持在兩端插入和移除元素。
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
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% 起步。
BlockingQueue 能夠是限定容量的。
BlockingQueue 實現主要用於生產者-使用者隊列,但它另外還支持 collection 接口。
BlockingQueue 實現是線程安全的。
BlockingQueue 是阻塞隊列(看你使用的方法),ConcurrentLinkedQueue 是非阻塞隊列。
LinkedBlockingQueue 是一個線程安全的阻塞隊列,基於鏈表實現,通常用於生產者與消費者模型的開發中。採用鎖機制來實現多線程同步,提供了一個構造方法用來指定隊列的大小,若是不指定大小,隊列採用默認大小(Integer.MAX_VALUE,即整型最大值)。
ConcurrentLinkedQueue 是一個線程安全的非阻塞隊列,基於鏈表實現。java 並無提供構造方法來指定隊列的大小,所以它是無界的。爲了提升併發量,它經過使用更細的鎖機制,使得在多線程環境中只對部分數據進行鎖定,從而提升運行效率。他並無阻塞方法,take 和 put 方法,注意這一點。
添加的元素必須實現 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
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)
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}
棧結構屬於一種先進者後出,相似於一個瓶子,先進去的會壓到棧低(push 操做),出去的時候只有一個出口就是棧頂,返回棧頂元素,這個操做稱爲 pop。
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