咱们查看源码发现 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