集合容器
七、集合容器
java中的集合主要结构如下:
集合容器的分类
集合容器从接口类型上可以Collection和Map类型,从安全性分为线程安全和线程不安全。
Collection:
Collection接口可以分为List和Set两个接口。
List是有序的,可重复的;而Set是无序的,不可重复的。
List的实现有:
- ArrayList: 线程不安全,底层使用数组存储数据。允许存空值。
- LinkedList:线程不安全,底层使用链表存储数据。允许空值。
- Vector:线程安全,底层也是使用数组存放数据。通过在方法上添加
synchronized
关键字,保证安全性,因此效率非常低,不经常使用。允许空值。 - CopyOnWriteArrayList:线程安全,使用数组存放数据,写时复制,内部通过
ReentrantLock
加锁保证线程安全。因为在执行写操作时,会加锁并复制数组,因此当数据非常大或频繁写操作时,效率会比较低,适合读操作多写操作少的场景。允许空值。
Set的实现有:
-
HashSet:hashset内部使用的hashmap,hashset的元素都存放在hashmap的key上。hashset的add方法:
public boolean add(E e) { // PERSENT 为常量new Object();所有的key公用一个value return map.put(e, PRESENT)==null; }
因为hashset使用hashmap做存储的数据结构,因此它延续了hashmap的无序性。hashst的数据是无序的。而且是唯一不可重复的。
-
LinkedHashSet:linkedhashset继承自hashset,
public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable {
它与hashset的区别是,它是有序的。有序的原因是hashset在创建时使用的是hashmap。
public HashSet() { map = new HashMap<>(); }
而LinkedHashSet在创建时使用的是LinkedHashMap
public LinkedHashSet() { super(16, .75f, true);}
点进super方法:
HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
而LinkedHashMap内部重写了Entry类,添加了before和after属性使结构链表化保证有序性。
static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
-
TreeSet: 内部使用TreeMap存储数据。
-
CopyOnWriteArraySet
内部使用的仍是CopyOnWriteArrayList,
-
ConcurrentSkipListSet
内部使用ConcurrentSkipListMap实现:
因为Set内部都是使用对应的Map存放数据,因此数据特性与对应Map的key的特性相同。HashMap key可以为空,因此HashSet的Key也可以为空。
Map:
Map的常见实现有:
HashMap:使用hash表实现,常用,线程不安全,效率高。Key,Value都允许空值。
Hashtable:jdk早期的类,内部使用synchronized锁方法来保证线程安全性。效率非常低,基本不用。key,value都不能为空。
LinkedHashMap:内部重写Entry节点继承hashmap的Node节点,添加了头节点和尾节点,使用链表结构。HashMap的Hash桶是纯数组结构:transient Node<K,V>[] table;
而LinkedHashMap在Hash桶层因为继承自HashMap,仍然是数组结构,put时,仍是计算hash值无序存储。只不过因为重写内部类及方法,因此数组中的每一个元素,都有头和尾节点,因此从某种程度上说又是有序的。使用的是HashMap,数据存放在Node节点的k,v中,因此Key,value都可以为空。
ConcurrentHashMap:线程安全的HashMap,使用CAS+分段锁保证线程的安全性。效率比Hashtable高很多。key和value都不能为空。
ConcurrentSkipListMap:使用跳表实现的线程安全的Map类型。因为使用跳表,所以查询效率非常高。key和value都不能为空。
TreeMap:通过红黑树实现。因为红黑树的特性,插入和删除效率低。key不能为空,值可以为空。
JUC下的容器
java.util.concurrent
包下,添加了一个BlockingQueue
接口,这个接口有很多线程安全的实现。BlockingQueue
接口又继承自Queue
接口。Queue
接口继承自Collection
接口,Queue
队列,先进先出FIFO,Queue
接口的方法有:
boolean add(E e):
向队列中添加一个元素,如果失败会抛出异常。boolean offer(E e)
:向队列中添加一个元素,如果失败则返回false。E poll()
:移除一个元素,如果队列为空则抛出异常E remove()
: 移除一个元素,如果队列为空则返回null。E element()
: 检查队列头,如果队列为空则抛出异常。E peek()
: 检查队列头,如果队列为空则返回null。
BlockingQueue
除了继承Queue
的方法之外,还添加了两个方法:
put
: 添加一个元素,如果队列已满的话,则阻塞等待直到添加成功。take
: 从队列中取出一个元素,如果队列为空,则阻塞等待,直到队列中有元素,取出元素。
这两个方法都是阻塞等待的。也是常用的方法。BlockingQueue
的常见实现有:
-
ArrayBlockingQueue
: 有界阻塞队列,内部使用数组存储数据,初始化时需要指定数组长度。内部使用一把锁ReentrantLock
,拆分为两个等待队列。因为入队和出队用的是一把锁,因此生产者和消费者无法并发执行。package com.xiazhi.queue; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; /** * @author 赵帅 * @date 2021/1/19 */ public class ArrayBlockingQueueDemo { static BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(2); static void consumer() { for (int i = 0; i < 50; i++) { try { String take = blockingQueue.take(); System.out.println(Thread.currentThread().getName() + "取出元素:" + take); TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { new Thread(() -> { for (int i = 0; i < 100; i++) { String element = "element" + i; System.out.println(Thread.currentThread().getName() + "存入元素:" + element); try { blockingQueue.put(element); TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } }, "provider").start(); new Thread(ArrayBlockingQueueDemo::consumer, "consumer-1").start(); new Thread(ArrayBlockingQueueDemo::consumer, "consumer-2").start(); } }
-
LinkedBLockingQueue
: 无界阻塞队列,内部使用链表结构,因为使用的是链表,无界,因此需要注意资源耗尽的风险。内部入队和出队是两把锁,因此可以实现生产者和消费者并发执行。package com.xiazhi.queue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; /** * @author 赵帅 * @date 2021/1/19 */ public class LinkedBlockingQueueDemo { static BlockingQueue<byte[]> blockingQueue = new LinkedBlockingQueue<>(); // 设置jvm参数 -Xms10M,-Xmx10M public static void main(String[] args) { // 生产者线程,每隔1秒存入1M数据 new Thread(() -> { try { while (true) { blockingQueue.put(new byte[1024 * 1024]); TimeUnit.SECONDS.sleep(1); } } catch (InterruptedException e) { e.printStackTrace(); } }).start(); // 消费者线程,每隔两秒取出一个元素。 new Thread(() -> { while (true) { try { blockingQueue.take(); TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); // 上面由于消费者的消费速度低于生产者的生产速度,因此就会造成队列中的元素越来越多,最终造成内存溢出 } }
-
SynchronousQueue
: 同步阻塞队列,这个队列没有容量,当它接收一个元素时,必须有一个消费者消费掉这个元素,它才能接收下一个元素。package com.xiazhi.queue; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; /** * @author 赵帅 * @date 2021/1/19 */ public class SynchronousQueueDemo { static SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>(); public static void main(String[] args) { // 生产者线程 new Thread(() -> { try { // 当有线程调用put方法时,必须等待另一个线程调用take方法取走数据,否则线程就会在此阻塞。 System.out.println(Thread.currentThread().getName() + "存入数据"); synchronousQueue.put("element"); } catch (InterruptedException e) { e.printStackTrace(); } }, "provider").start(); // 消费者线程 ,可以尝试注释消费者线程查看阻塞状态 new Thread(() -> { try { String take = synchronousQueue.take(); System.out.println(Thread.currentThread().getName() + "取出数据:" + take); } catch (InterruptedException e) { e.printStackTrace(); } }, "consumer").start(); } }
-
PriorityBlockingQueue
: 排序阻塞队列,队列中的元素需要实现Comparable
接口,如果没有实现Comparable
接口,那么在创建队列时就需要指定排序的方法。内部使用ReentrantLock一把锁,无法实现消费者生产者并发。package com.xiazhi.queue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.PriorityBlockingQueue; /** * @author 赵帅 * @date 2021/1/19 */ public class PriorityBlockingQueueDemo { /** * 因为Integer类实现了Comparable接口,因此会调用 compareTo 方法排序 */ static BlockingQueue<Integer> integerBlockingQueue = new PriorityBlockingQueue<>(); /** * 如果队列中的对象是自定义类型,那么类型可以通过实现 Comparable 接口 */ static BlockingQueue<M> customBLockingQueue = new PriorityBlockingQueue<>(); /** * 如果自定义对象没有实现Comparable接口,那么在调用构造方法是需要指定排序方式,如果类型既不实现Comparable接口,构造方法又没有指定比较方法,那么会抛出异常 */ static BlockingQueue<N> customNonImplComparableBlockingQueue = new PriorityBlockingQueue<>(10, (a, b) -> { if (a.getAge() == b.getAge()) { return 0; } return a.getAge() > b.getAge() ? 1 : -1; }); static BlockingQueue<N> customQueue = new PriorityBlockingQueue<>(); static class M implements Comparable<M> { int age; public M(int age) { this.age = age; } @Override public int compareTo(M obj) { return Integer.compare(this.age, obj.age); } @Override public String toString() { return "M{" + "age=" + age + '}'; } } static class N { int age; public N(int age) { this.age = age; } public int getAge() { return age; } @Override public String toString() { return "N{" + "age=" + age + '}'; } } static void dequeue(BlockingQueue<?> blockingQueue) { while (true) { try { Object take = blockingQueue.take(); System.out.println("take = " + take); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { // 系统已经实现Comparable接口的类型 new Thread(() -> dequeue(integerBlockingQueue)).start(); integerBlockingQueue.put(10); integerBlockingQueue.put(2); integerBlockingQueue.put(8); integerBlockingQueue.put(6); // 自定义实现Comparable接口的类 new Thread(() -> dequeue(customBLockingQueue)).start(); customBLockingQueue.put(new M(8)); customBLockingQueue.put(new M(93)); customBLockingQueue.put(new M(27)); customBLockingQueue.put(new M(48)); // 构造方法指定比较方法 new Thread(() -> dequeue(customNonImplComparableBlockingQueue)).start(); customNonImplComparableBlockingQueue.put(new N(39)); customNonImplComparableBlockingQueue.put(new N(19)); customNonImplComparableBlockingQueue.put(new N(5)); // 既不实现 Comparable接口,又不再构造方法指定比较方式,会抛出异常 new Thread(() -> dequeue(customQueue)).start(); customQueue.put(new N(29)); customQueue.put(new N(2)); customQueue.put(new N(18)); } }
-
DelayBlockingQueue
: 延时阻塞队列,每一个元素都要设置过期时间,先过期的元素会被放到队列头,如果没有过期元素,那么无法取出元素。package com.xiazhi.queue; import java.time.LocalTime; import java.util.concurrent.BlockingQueue; import java.util.concurrent.DelayQueue; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; /** * @author 赵帅 * @date 2021/1/19 */ public class DelayQueueDemo { /** DelayQueue中的元素需要继承自Delayed接口 */ static BlockingQueue<M> blockingQueue = new DelayQueue<>(); static class M implements Delayed{ private final LocalTime executeTime; private final String value; public M(LocalTime executeTime, String value) { this.executeTime = executeTime; this.value = value; } @Override public long getDelay(TimeUnit unit) { return unit.convert(executeTime.toSecondOfDay() - LocalTime.now().toSecondOfDay(), TimeUnit.SECONDS); } @Override public int compareTo(Delayed o) { long diffTime = this.getDelay(TimeUnit.SECONDS) - o.getDelay(TimeUnit.SECONDS); return diffTime == 0 ? 0 : diffTime > 0 ? 1 : -1; } @Override public String toString() { return "M{" + "value='" + value + '\'' + '}'; } } public static void main(String[] args) throws InterruptedException { blockingQueue.put(new M(LocalTime.of(23, 6, 18), "zhangsan")); blockingQueue.put(new M(LocalTime.of(23, 5, 50), "里斯")); blockingQueue.put(new M(LocalTime.of(23, 5, 20), "王武")); // 如果元素没有过期,那么线程处于阻塞状态 for (int i = 0; i < 3; i++) { M take = blockingQueue.take(); System.out.println("take = " + take); } } }
-
ConcurrentLinkedDeque
: 基于链表实现的双端队列。可以理解为线程安全的LinkedList
,因为LinkedList
也实现了Deque
接口。Deque
: 双端队列,队列是从一端进入,从另一端出去。而双端队列是指两端都可以进,两端也都可以出。package com.xiazhi.queue; import java.util.Deque; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedQueue; /** * @author 赵帅 * @date 2021/1/20 */ public class ConcurrentLinkedDequeDemo { /** * Queue是一端入队,另一端出队 */ static Queue<String> queue = new ConcurrentLinkedQueue<>(); /** * Deque是双端队列,两边都可以进,两边都可以出。 */ static Deque<String> deque = new ConcurrentLinkedDeque<>(); public static void main(String[] args) { // LinkedQueue严格遵循队列先进先出的特性 queue.add("1"); queue.add("2"); queue.add("3"); queue.add("4"); for (int i = 0; i < 4; i++) { String poll = queue.poll(); System.out.println("poll = " + poll); } deque.addFirst("1"); deque.addLast("2"); String first = deque.removeFirst(); System.out.println("first = " + first); String last = deque.removeLast(); System.out.println("last = " + last); // push向栈中压入元素 deque.push("1"); deque.push("2"); deque.push("3"); // pop 弹出栈顶元素 System.out.println("pop = " + deque.pop()); System.out.println("pop = " + deque.pop()); System.out.println("pop = " + deque.pop()); } }
-
LinkedTransferQueue
: 链式传递队列。它更像是
SynchronousQueue,LinkedBLockingQueue,ConcurrentLinkedQueue
三种队列的超集。它通过链表存储数据,因此可以当作
LinkedBLockingQueue
或ConcurrentLinkedQueue
使用,而且它也支持数据的传递,像SynchronousQueue
。不过不同的是,SynchronousQueue
只支持线程一对一的传递元素,而LinkedTransferQueue
支持线程多对多的传递元素。因为SynchronousQueue
是没有容量的,当一个线程放入元素,必须有另一个线程取走这个元素,就好像线程将一个元素手把手交给了另一个元素。而LinkedTransferQueue
是使用链表存放数据的。他是可以存放数据的。当调用
transfer
方法存入数据时,他会检测是否有消费者等待获取数据,如果有的话,他就会直接将数据给这个线程而不放入队列。