同步容器与并发容器(简要介绍)
多线程笔记(三)
1. 同步容器与并发容器
同步容器
通过synchronized关键字实现线程安全的容器;或通过Collections这个工具类的synchronizedXXX方法创建的容器,都称为同步容器
例如Vector, Stack, Hashtable
Vector是list接口的线程安全实现
Stack是Vector的子类,是一个先进后出的栈,入栈和出栈都是同步的
Hashtable是Map接口的线程安全实现
并发容器
同步容器一次只能允许一个线程去使用,因此性能较差。
允许多线程同时使用容器,并能保证线程安全的容器都是并发容器。
并发容器有两个接口,分别为ConcurrentMap和BlockingQueue
主要的实现
- CopyOnWrite容器
- CopyOnWriteArrayList
- CopyOnWriteArraySet
- ConcurrentMap的实现类
- ConcurrentHashMap
- ConcurrentSkipListMap(支持排序)
- 阻塞队列的实现
- ArrayBlockingQueue:使用数组实现的有界阻塞队列
- LinkedBlockingQueue:使用链表实现的有界阻塞队列
- PriorityBlockingQueue:支持优先级的无界阻塞队列
- DelayQueue:支持延时获取元素的无界阻塞队列
- SyncronousQueue:不存储元素的阻塞队列
- LinkedTransferQueue:使用链表实现的无界阻塞队列
- LinkedBlockingDeque:使用链表实现的双向阻塞队列
- 非阻塞队列的实现:
- ConcurrentLinkedQueue:使用链表实现的无界非阻塞队列
- ConcurrentLinkedDeque:使用链表实现的双向非阻塞队列
2. CopyOnWriteArrayList
CopyOnWriteArrayList是一个允许多线程使用,能够保证线程安全,底层使用数组实现的并发容器。
基本设计思想:
内部还是使用数组来存放数据,CopyOnWrite指的是写时复制,一个数组在读的时候使用原数组,写的时候,假如添加一个元素进来,先copy原来的数组,添加一个元素的位置,然后把新的元素放进来。这时内存里面同时存在两个数组,原数组支持读请求,新数组支持读请求。同时将指向原数组的变量改为指向新数组。
缺点:
每次写的时候,都去cpoy一份数据出来,如果数据比较大的话,比较耗费内存
只能保证数据最终一致,不能保证实时一致,当数据在修改的时候,读取到的数据是”旧“的值。
适用场景:读多写少,对实时性要求不是特别高。
3. ConcurrentHashMap
概述
ConcurrentHashMap是一个实现Map功能的并发容器,也可以认为是一个线程安全的HashMap
不同JDK版本里面的实现机制不一样的。JDK1.8之前是数组加链表,JDK1.8及其之后是数组加链表/红黑树。
ConcurrentHashMap继承了AbstractMap,实现了ConcurrentMap接口
AbstractMap实现了Map接口,提供了Map接口的骨干实现,如果我们自己想要实现一个Map,可以继承AbstractMap,这样可以最大限度的减少自己实现Map这类数据结构所需要的工作量。
ConcurrentMap主要提供了一些针对Map的原子操作
内部结构
使用Node<K, V>[]
(Node类型的数组)来存放数据
Node节点类型
Node:
用来存放k-v数据的node,如果发生了哈希冲突,那么就使用链表法解决
TreeBin:
它是一个指向红黑树的代理节点,用来存放数据,树上的节点是TreeNode,TreeNode继承了Node节点。TreeBin的作用是方便对红黑树的操作(左旋,右旋,删除,平衡等等),TreeBin还包含了加锁解锁等操作。
ForwardingNode
是一种临时节点,扩容的时候才会使用。不存储数据。
ReservationNode
保留节点,给ConcurrentHashMap中的一些特殊方法使用,不存储数据。只在computeIfAbsent和compute这两个方法里面使用。
扩容和数据迁移的思路
扩容:
-
数组扩容:创建一个新数组,通常长度为原来的两倍
-
数据迁移:把旧的数组里的数据拷贝到新的数组里面
扩容部分大家可以看看这一篇 https://blog.csdn.net/zzu_seu/article/details/106698150
4. BlockingQueue
阻塞队列(BlockingQueue):在并发环境下,调用队列的过程中,会根据情况去阻塞调用线程,实现这样带阻塞功能的队列,就是阻塞队列。
阻塞队列是通过“锁”?来实现的,主要用在生产者-消费者模式,用于线程间的数据交换和系统解耦。
阻塞队列的作用:
- 如果线程向队列插入元素,而这个时候队列满了,就会阻塞这个线程,直到队列有空闲。
- 如果线程从队列中获取元素,而这个时候,队列为空,就会阻塞这个线程,直到队列里面有数据
BlockingQueue接口中的一些方法
操作成功返回true, 如果操作失败抛异常:add(E e)
, remove(Object o)
操作成功返回true,操作失败返回false:offer(E e)
队列满了阻塞调用线程:put(E e)
, take()
阻塞+超时:offer(E e, long timeout, TimeUnit unit)
, poll(long timeout, TimeUnit unit)
阻塞队列的特点:
- 不能包含null元素
- 实现这个接口的类都必须是线程安全的
- 可以限定容量大小
5. ArrayBlockingQueue
ArrayBlockingQueue是BlockingQueue接口的典型实现。
ArrayBlockingQueue是基于数组来实现的,有界的阻塞队列
ArrayBlockingQueue特点:
- 队列容量在创建的时候指定,之后不可更改
- 插入元素在队尾,删除元素在队首
- 队列满了,对阻塞插入元素的线程,队列为空,会阻塞删除元素的线程
- 支持公平/非公平的册罗,默认是非公平的
- 加的锁是全局锁,如果在处理出队的时候,是处理不了入队的,反之同理。在超高并发环境下,可能会有性能问题
6. LinkedBlockingQueue
LinkedBlockingQueue是BlockingQueue接口的典型实现。
LinkedBlockingQueue是基于链表实现的,一种近似有界阻塞队列。
LinkedBlockingQueue特点:
- 与ArrayBlockingQueue的全局锁不同的是,LinkedBlockingQueue有两把锁,一把是控制入队的putLock,一把是控制出队的takeLock。
- 与ArrayBlockingQueue初始必须指定队列大小不同的是,其可以在初始化时指定队列的容量,如果不指定,容量大小默认为Integer的最大值。
- 与ArrayBlockingQueue可以指定公平/非公平策略不同的是,LinkedBlockingQueue不可以指定公平/非公平策略。
7. ConcurrentLinkedQueue
ConcurrentLinkedQueue是Queue接口的实现
ConcurrentLinkedQueue是基于链表实现的,无界的非阻塞队列
与阻塞队列最大的不同是,该队列不再基于“锁”来保证队列的并发安全性,而是通过自旋+CAS的方式来保证