集合容器

七、集合容器

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实现:

    image-20210119142011013

    因为Set内部都是使用对应的Map存放数据,因此数据特性与对应Map的key的特性相同。HashMap key可以为空,因此HashSet的Key也可以为空。

Map:

Map的常见实现有:

HashMap:使用hash表实现,常用,线程不安全,效率高。Key,Value都允许空值。

image-20210119145930092

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三种队列的超集。

    它通过链表存储数据,因此可以当作LinkedBLockingQueueConcurrentLinkedQueue使用,而且它也支持数据的传递,像SynchronousQueue。不过不同的是,SynchronousQueue只支持线程一对一的传递元素,而LinkedTransferQueue支持线程多对多的传递元素。因为SynchronousQueue是没有容量的,当一个线程放入元素,必须有另一个线程取走这个元素,就好像线程将一个元素手把手交给了另一个元素。而LinkedTransferQueue是使用链表存放数据的。他是可以存放数据的。

    当调用transfer方法存入数据时,他会检测是否有消费者等待获取数据,如果有的话,他就会直接将数据给这个线程而不放入队列。

posted @ 2021-01-20 15:49  Zs夏至  阅读(107)  评论(2编辑  收藏  举报