概述
- 是JDK1.5出现的专门应对高并发的包
- 内容:BlockingQueue、ConcurrentMap、ExecutorService、Lock、原子性操作
BlockingQueue - 阻塞式队列
概述
- 依然遵循**“先进先出”(FIFO)**的原则
- 所有的阻塞式队列都是有界的,即队列的大小是固定的
- 如果队列已满,则添加操作会被阻塞;如果队列为空,则获取操作会被阻塞
- BlockingQueue是阻塞式队列的顶级接口,用的是其实现类
- 重要方法
表头 | 抛出异常 | 返回值 | 阻塞 | 定时阻塞 |
---|---|---|---|---|
添加 | add | offer - true/false | put | offer |
获取 | remove | poll - null | take | poll |
- 阻塞式队列的适用场景:生产消费模型
ArrayBlockingQueue - 阻塞式顺序队列
- 底层基于数组存储
- 使用时需要指定容量
LinkedBlockingQueue - 阻塞式链式队列
- 底层基于链表存储
- 使用的时候容量可指定也可不指定,如果指定容量,则此容量不可变。如果不指定容量,则默认Integer.MAX_VALUE–>2的31次方-1,一般会认为这个队列是无界的
PriorityBlockingQueue - 具有优先级的阻塞式队列
- 在使用的时候可以指定也可不指定容量,如果不指定容量,则默认为11
- 在遍历队列的时候,会对放入其中的元素进行自然排序。但是注意,如果是迭代遍历,则是无序的
- 要求放入的元素所对应的类型必须实现Comarable接口
SynchronousQueue - 同步队列
- 在使用的时候不需要指定容量,容量默认为1,并且只能为1
- 会将这个队列称之为汇合点
BlockingQueue - 阻塞式双端队列
- 两个方向都可以进行添加和移除的操作
ConcurrentMap -并发映射
概述
- JDK1.5提供用于高并发以及保证安全性的一类映射
ConcurrentHashMap - 并发哈希映射
-
底层结构依然是依靠数组+链表
-
默认初始容量是16,默认加载因子是0.75,默认扩容增加一倍
-
底层采用了叫分桶锁(分段锁)机制,来保证并发性,在后续的版本中,在concurrenthashmap分段锁的基础上引入了读写锁机制:
- 读锁:允许多个线程同时读,不允许线程写
- 写锁:只允许一个线程写,不允许线程读
-
在JDK1.8中,采用了无锁算法CAS(Compare And Swap - 比较和交换)
对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS比较与交换的伪代码可以表示为:
do{
备份旧数据;
基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))
注:t1,t2线程是同时更新同一变量56的值
因为t1和t2线程都同时去访问同一变量56,所以他们会把主内存的值完全拷贝一份到自己的工作内存空间,所以t1和t2线程的预期值都为56。
假设t1在与t2线程竞争中线程t1能去更新变量的值,而其他线程都失败。(失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次发起尝试)。t1线程去更新变量值改为57,然后写到内存中。此时对于t2来说,内存值变为了57,与预期值56不一致,就操作失败了(想改的值不再是原来的值)。
(上图通俗的解释是:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。)
就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。 -
在JDK1.8中引入了红黑树机制。当桶中额元素个数超过8个,则扭转成红黑树;如果节点个数不足7个,则会扭转成链表。扭转成红黑树的初始容量为64
-
红黑树:
a. 本质上一棵自平衡二叉查找树
b. 查询效率的时间复杂度:O(logn)
c. 二叉查找树:
i. 基于二叉树
ii.左子树都小于根,右子树都大于根
d. 特点:
i. 所有的节点非红即黑
ii. 根节点一定是黑的
iii. 红节点的子节点一定是黑节点,黑节点的子节点可以是红节点也可以是黑节点
iv. 最底层的叶子节点一定是黑色的空节点
v. 从根节点到任意一个叶子节点警告的黑色节点的个数一定相同,即黑色节点高度一致
vi. 新添加的节点颜色一定是红节点
e. 修正:红黑树的修正过程是一个链式过程(涂色–左旋–右旋)
i. 当前节点为红,父节点也为红,并且叔父节点也为红,那么需要将父节点以及叔父节点涂黑,然后将祖父节点涂红
ii. 当前节点为红父节点为红,叔父节点为黑,并且当前节点为右子叶,那么需要以当前节点为轴,进行左旋
iii.当前节点为红父节点为红,叔父节点为黑,并且当前节点为左子叶,那么需要以父节点为轴进行右旋
ConcurrentNavigableMap—并发导航映射
- 这类映射中提供了截取子映射的方法
- ConcurrentNavigableMap本身是一个接口,更常使用是它的实现类ConcurrentSkipListMal - 并发跳跃表映射 - 底层是基于跳跃表来存储的
- 跳跃表:
- 使用前提:元素有序
- 实际使用过程,跳跃表往往不止一层,最上层的跳跃表的元素个数不少于2个
- 跳跃表适用于查询多,增删少的场景
- 跳跃表是典型的以空间换时间的产物
- 当新添元素时,这个元素是否要提取到上一次跳跃表中,遵循“抛硬币原则”
- 查询的时间复杂度是:O(logn)
ExecutorService - 执行器服务
概述
- 线程池的意义:减少线程的创建和销毁,做到线程的重用,以提高资源的利用率
- 当线程定义好之后,线程池中没有任何线程
- 每过来一个请求,会创建一个线程去处理这个请求,直到线程数达到指定的数量不在创建。这些线程称之为核心线程
- 在核心线程数量达到指定数量之前,每次的请求都会创建一个新的核心线程
- 核心线程使用完成,不会被销毁,而是会等待下一个任务
- 如果核心线程被占满,则后续的请求会放入该线程的工作队列中。工作队列本质上是阻塞式队列
- 如果工作队列被全部占用,则信赖的请求会交给临时线程处理
- 临时线程再使用完成之后不会立即销毁而是会存活一段指定的时间,如果这段时间内没有请求处理,则该临时线程才会被销毁
- 如果临时线程被全部占用,则后续的请求会交给 拒绝执行处理器来进行拒绝处理
Callable线程
- Callable执行完成之后会有返回结果,所以泛型限定结果类型
- Runable和Callable的区别:
a. 返回值:Runable没有返回值;Callable有返回值
b. 启动方式:Runable可以通过Thread启动也可以通过线程池启动;Callable只能通过线程池启动
c. 容错机制:Runableo不允许抛出异常,所以不能以全局方式(例如Spring中的异常通知)处理;但是Callable允许以全局方式处理
ScheduledExecutorService - 定时执行器服务
- 起到定时执行的效果
- 是很多定时器底层的实现
ForkJoinPool - 分叉合并池
- 分叉:将任务进行分解,将一个大任务拆分成多个小的任务分配到不同的核上来执行
- 合并:将分叉的执行结果进行合并
- 分叉合并的目的:提高CPU的利用率
- 分叉合并是适用于任务量比较大的场景
- 为了防止因为慢任务导致效率降低,采取了work-stealing(工作窃取)策略:即当一个核上的任务执行完成之后,不会空闲下来而是会随机的去其他核上去“偷”一个任务过来执行
Lock
概述
- Lock是JDK1.5提供的一个接口:有效的降低死锁产生的几率,Lock相对 synchronized而言更加的灵活
- 用的更多的是它的实现类:ReentrantLock
- 公平和非公平策略:锁如果不指定,默认是非公平策略
- 读写锁:
a. 读锁:允许多个线程读但是不允许写
b. 写锁:只允许一个线程写但是不允许读
其他
- CountDownLatch - 闭锁/线程递减锁。用于进行线程的计数,当达到指定的 计数的时候,会放开阻塞允许后续线程执行
- CyclicBarrier:栅栏。用于进行线程的计数
- Exchanger:交换机。用于交换两个线程之间的信息
- Semaphore:信号量。如果有空余的信号,则来的线程可以取得信号执行,
如果没有空余信号,来的线程就会被阻塞直到有信号被空出 - 实际过程中往往 用于进行"限流"