Fork me on Gitee

Java并发容器类综述

0) 出现原因:

   起初在对程序性能要求不高的年代,基本都是单线程编程,那个时候CPU基本是单核,随着计算机硬件技术的发展,出现了多核CPU,过去的单核编程已经不能更好的使用我们的CPU核心数目,加上对程序的速度性能的要求的提高,出现了并发编程技术. 单核程序的性能的瓶颈就是频繁的上下文切换,那么我们就可以通过减少上下文的切换来提高性能。让更多的CPU协同 异步工作.

1)发展过程:

        Java在 Java5 添加的一个并发工具包。这个包包含了一系列能够让 Java 的并发编程变得更加简单轻松的类。在这之前,你需要自己手动去实现相关的工具类. 是以工具类的形式提供 包路径为  java.util.concurrent

        提供的类:

    • 阻塞队列 BlockingQueue
    • 数组阻塞队列 ArrayBlockingQueue
    • 延迟队列 DelayQueue
    • 链阻塞队列 LinkedBlockingQueue
    • 具有优先级的阻塞队列 PriorityBlockingQueue
    • 同步队列 SynchronousQueue
    • 阻塞双端队列 BlockingDeque
    • 链阻塞双端队列 LinkedBlockingDeque
    • 并发 Map ConcurrentMap
    • 并发导航映射 ConcurrentNavigableMap
    • 闭锁 ConutDownLatch
    • 栅栏 CyclicBarrier
    • 交换机 Exchanger
    • 信号量 Semaphore
    • 执行器服务 ExecutorService
    • 线程池执行者 ThreadPoolExecutor
    • 定时执行者服务 ScheduledExecutorService
    • 使用 ForkJoinPool 进行分叉和合并
    • 锁 Lock
    • 读写锁 ReadWriteLock
    • 原子性布尔 AtomicBoolean
    • 原子性整型 AtomicInteger
    • 原子性长整型 AtomicLong
    • 原子性引用型 AtomicReference

      我们选择常用的进行概述.

2)必要理论知识

悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试

3)  目前最新发展:

           Java类库中出现的第一个关联的集合类是 Hashtable ,它是JDK 1.0的一部分。 Hashtable 提供了一种易于使用的、线程安全的、关联的map功能,这当然也是方便的。然而,线程安全性是凭代价换来的―― Hashtable 的所有方法都是同步的。 此时,无竞争的同步会导致可观的性能代价。 Hashtable 的后继者 HashMap 是作为JDK1.2中的集合框架的一部分出现的,它通过提供一个不同步的基类和一个同步的包装器 Collections.synchronizedMap ,解决了线程安全性问题.

          但是: 其结果是尽管表面上这些程序在负载较轻的时候能够正常工作,但是一旦负载较重,它们就会开始抛出 NullPointerExceptionConcurrentModificationException 异常

随后出现了 ConcurrentMap

ConcurrentMap 的实现

concurrent 包里面就一个类实现了 ConcurrentMap 接口

  • ConcurrentHashMap
ConcurrentHashMap

ConcurrentHashMap 和 HashTable 类很相似,但 ConcurrentHashMap 能提供比 HashTable 更好的并发性能。在你从中读取对象的时候,ConcurrentHashMap 并不会把整个 Map 锁住。此外,在你向其写入对象的时候,ConcurrentHashMap 也不会锁住整个 Map,它的内部只是把 Map 中正在被写入的部分锁定。
其实就是把 synchronized 同步整个方法改为了同步方法里面的部分代码。

另外一个不同点是,在被遍历的时候,即使是 ConcurrentHashMap 被改动,它也不会抛 ConcurrentModificationException。尽管 Iterator 的设计不是为多个线程同时使用。


使用例子:

public class ConcurrentHashMapExample {

    public static void main(String[] args) {
//        HashMap<String, String> map = new HashMap<>();
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
        map.put("1", "a");
        map.put("2", "b");
        map.put("3", "c");
        map.put("4", "d");
        map.put("5", "e");
        map.put("6", "f");
        map.put("7", "g");
        map.put("8", "h");
        new Thread1(map).start();
        new Thread2(map).start();

    }

}

class Thread1 extends Thread {

    private final Map map;

    Thread1(Map map) {
        this.map = map;
    }

    @Override
    public void run() {
        super.run();
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        map.remove("6");
    }
}

class Thread2 extends Thread {

    private final Map map;

    Thread2(Map map) {
        this.map = map;
    }

    @Override
    public void run() {
        super.run();
        Set set = map.keySet();
        for (Object next : set) {
            System.out.println(next + ":" + map.get(next));
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}复制代码

CopyOnWriteArrayList

在那些遍历操作大大地多于插入或移除操作的并发应用程序中,一般用 CopyOnWriteArrayList 类替代 ArrayList 。如果是用于存放一个侦听器(listener)列表,例如在AWT或Swing应用程序中,或者在常见的JavaBean中,那么这种情况很常见(相关的 CopyOnWriteArraySet 使用一个 CopyOnWriteArrayList 来实现 Set 接口) 。

原理实现:

如果您正在使用一个普通的 ArrayList 来存放一个侦听器列表,那么只要该列表是可变的,而且可能要被多个线程访问,您 就必须要么在对其进行迭代操作期间,要么在迭代前进行的克隆操作期间,锁定整个列表,这两种做法的开销都很大。当对列表执行会引起列表发生变化的操作时, CopyOnWriteArrayList 并不是为列表创建一个全新的副本,它的迭代器肯定能够返回在迭代器被创建时列表的状态,而不会抛出 ConcurrentModificationException 。在对列表进行迭代之前不必克隆列表或者在迭代期间锁 定列表,因为迭代器所看到的列表的副本是不变的。换句话说, CopyOnWriteArrayList 含有对一个不可变数组的一个可变的引用,因此,只要保留好那个引用,您就可以获得不可变的线程安全性的好处,而且不用锁 定列表。


4)目前最新应用情况:

以ConcurrentHashMap为例子:

jdk7版本

ConcurrentHashMap和HashMap设计思路差不多,但是为支持并发操作,做了一定的改进,ConcurrentHashMap引入Segment 的概念,目的是将map拆分成多个Segment(默认16个)。操作ConcurrentHashMap细化到操作某一个Segment。在多线程环境下,不同线程操作不同的Segment,他们互不影响,这便可实现并发操作

最新JDk8 实现:

jdk8版本的ConcurrentHashMap相对于jdk7版本,发送了很大改动,jdk8直接抛弃了Segment的设计,采用了较为轻捷的Node + CAS + Synchronized设计,保证线程安全。

807144-6264960638978dff.webp

ConcurrentHashMap结构图

看上图ConcurrentHashMap的大体结构,一个node数组,默认为16,可以自动扩展,扩展速度为0.75

private static finalint DEFAULT_CONCURRENCY_LEVEL = 16;
private static final float LOAD_FACTOR = 0.75f;

每一个节点,挂载一个链表,当链表挂载数据大于8时,链表自动转换成红黑树

static final int TREEIFY_THRESHOLD = 8;

部分代码:

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {
    transient volatile Node<K,V>[] table;
}

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
}

static final class TreeNode<K,V> extends Node<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
}

static final class TreeBin<K,V> extends Node<K,V> {
        TreeNode<K,V> root;
        volatile TreeNode<K,V> first;
        volatile Thread waiter;
        volatile int lockState;
 }
posted @ 2020-04-21 14:01  ---dgw博客  阅读(...)  评论(...编辑  收藏