多线程(5):Java多线程中的并发容器

一、JDK提供的并发容器都有哪些

JDK提供的容器大多在 java.util.concurrent 包中。

  • ConcurrentHashMap:线程安全的HashMap
  • CopyOnWriteArrayList:线程安全的List。在读多写少的场合性能非常好,远远好于Vector
  • ConcurrentLinkedQueue(非阻塞队列):线程安全的LinkedList。并发队列,非阻塞队列,使用链表实现
  • BlockingQueue(阻塞队列):是一个接口,阻塞队列。通过链表、数组等方式实现,非常适合做数据共享的通道
  • ConcurrentSkipListMap:调表的实现。是一个Map,使得调表的数据结构能快速查找

 

1.彻底搞懂ConcurrentHashMap(线程安全HashMap)

对读和写都能保证高性能:读操作时(几乎)不加锁,写操作时通过 锁分段技术 ,只对操作的段加锁而不影响客户端对其他段的访问。

(1)ConcurrentHashMap与Hashtable的区别

底层数据结构不同:

①JDK1.7的ConcurrentHashMap底层采用 分段数组+链表实现、JDK1.8采用数据结构跟HashMap一样的 数组+链表/红黑二叉树 实现。

②Hashtable和JDK1.8之前的HashMap一样都是 数组+链表 实现。

        注意:数组是HashMap主体,链表则是为了解决哈希冲突而存在

实现线程安全方式不同

①JDK1.7的ConcurrentHashMap(分段锁)对整个桶数组进行分段(Segment),每把锁只锁容器一部分数据,多线程访问不同段数据不会存在锁竞争、JDK1.8摒弃了Segment直接采用 数组+链表+红黑树 的数据结构实现,并发控制使用 synchronized 和CAS操作。

②Hashtable(同一把锁)只使用synchronized 来保证线程安全,效率低。

(2)ConcurrentHashMap线程安全具体实现方式 / 底层原理

JDK1.7

将数据分为一段段存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段数据也能被其他线程访问。

ConcurrentHashMap 由Segment数组结构(数组+链表)和HashEntry数组结构组成。Segment实现了ReentrantLock所以Segment是可重入锁,扮演锁的角色;HashEntry用于存储键值对数据。

多线程(5):Java多线程中的并发容器

JDK1.8

取消了Segment分段锁,采用CAS和synchronized来保证并发安全,数据结构和HashMap类似,采用数组+链表/红黑二叉树。

synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,提高了效率。

多线程(5):Java多线程中的并发容器

2.彻底搞懂 CopyOnWriteArraylist(线程安全List)

读操作完全不加锁,写入也不会阻塞读取操作,只有写入和写入需要同步ReentrantReadWriteLock 读写锁是,读读共享、写写互斥、读写和写读互斥)

(1)CopyOnWriteArraylist底层原理

CopyOnWriteArraylist的所有可变操作(add、set等)都是通过创建底层数组的副本实现。

当List需要被修改时并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本,写完后再将副本替换原来数据。这样可保证写操作不会影响读操作。

读取操作:没有任何同步控制和锁操作,因为内部数组array不会发生修改,只会被另一个array替换,可以保证数据安全。

写入操作:add()方法在添加集合的时候加了锁,保证了同步,避免了所线程写的时候会copy出多个副本出来。

 

3.ConcurrentLinkedQueue(非阻塞队列)

底层原理:使用 链表 作为数据结构,主要使用CAS非阻塞算法实现线程安全,高性能、无锁

4.BlockingQueue(阻塞队列)

(1)底层原理:主要应用于“生产者——消费者”问题中,原因是其提供了可阻塞的插入和移除的方法。当队列已满 生产者 线程会被阻塞,直到队列未满;当队列为空 消费者 线程被阻塞,直到队列非空

(它是一个接口,继承Queue,而Queue又继承Collention接口)

(2)BlockingQueue接口的3个实现类ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue

——ArrayBlockingQueue(底层是: 数组)

有界队列(FIFO)实现类,一旦创建容量不能改变。其并发控制采用可重入锁实现,不管插入还是读取都需要先获取到锁,当队列满时插入队列会阻塞,从空队列读取元素也会阻塞。不保证公平性。

——LinkedBlockingQueue(底层是:单项链表)

可以是有界队列也可以试*队列(FIFO),为防止容量迅速增加一般在建立时会指定大小,默认大小是Integer.MAX_VALUE,相比ArrayBlockingQueue有更高的吞吐量。

——PriorityBlockingQueue

支持优先级的 *队列。默认采用自然顺序排序,也可以通过自定义类实现compareTo()方法来指定元素排序规则,还可以初始化时通过构造器参数Comparator来指定排序规则。并发控制采用ReentrantLock可重入锁,只能指定初始大小,后面插入容量时空间不够会自动扩容。

(是PriorityQueue 的线程安全版本,不可以插入null,插入的对象必须是可比较大小的)

 

什么是有界队列和*队列:

有界队列:有固定大小的队列

*队列:没有设置固定大小的队列