文章目录
目录
前言
主要介绍了Java并发编程的并发工具。
一、线程池
- 创造线程池的原因
- 为每一个任务分配一个线程会造成
- 栈太多了导致OFM
- 每次一个任务执行完会导致频繁的上下文切换
- 原理
- 创造线程池,使任务轮流被固定数量的线程池中线程执行
- 为每一个任务分配一个线程会造成
- 状态+线程数量
- 前三位记录状态,后面位记录线程数量,写在一个整数标记里面可以减少一次CAS
- 五状态
- RUNNING
- 正常运行状态
- SHUTDOWN
- 不接受新任务,已经提交的阻塞队列中剩余任务会处理完
- STOP
- 不接受新任务,已经提交的阻塞队列中剩余任务也不会执行
- TIDYING
- 任务执行完毕,活动线程为0时即将进入终结
- TERMINATED
- 终结状态
- RUNNING
- 构造方法
- 拯救线程
- 有界队列中,最大线程数 - 核心线程数=拯救线程数
- 核心线程满 & 任务队列满时,新任务进入拯救线程执行
- 拒绝策略
- 核心线程满 & 任务队列满 & 拯救线程满时,拒绝策略作为线程构造函数的参数传入(不在构造函数中固定写好,可以由编程人员动态确定)
- 各种拒绝策略的实现
- JDK
- AbortPolicy
- 默认,抛出异常
- CallerRunsPolicy
- 调用者完成任务
- DiscardPolicy
- 放弃当前任务
- DiscardOldestPolicy
- 放弃任务中最早的任务,当前任务取而代之
- AbortPolicy
- Duboo
- 抛出异常前记录日志
- Netty
- 创建新线程执行
- ActiveMQ
- 超时等待放入任务队列
- PinPoint
- 逐一实现拒绝策略链中的各种拒绝策略
- JDK
- 拯救线程
- 类型
- 固定大小newFixedThreadPool
- 特点
- 没有拯救线程(固定数量核心线程),没有超时时间
- 无界阻塞队列,任意数量任务
- 适用场景
- 线程数固定,相对耗时的任务
- 特点
- 缓存池newCachedThreadPool
- 特点
- 每个任务都由拯救线程完成(60s后可回收),拯救线程无限创建
- 必须有线程来取,才能把任务放进去
- 使用场景
- 线程数多,执行时间短
- 特点
- 单线程newSingleThreadExcutor
- 区别
- 普通线程new Thread
- 任务失败,普通线程没有任何补救措施
- 单线程池会新建线程,保证池正常完成任务
- 固定大小=1线程池newFixedThread(1)
- 固定大小线程池返回的是ThreadPoolExecutor对象,强转类型后可以调用setCorePoolSize方法修改池中线程数
- 单线程池返回的是装饰器模式保护的ExecutorService接口,不能调用ThreadPoolExecutor中特有方法,池中永远只有一个线程
- 普通线程new Thread
- 适用场景
- 多任务排队执行(仅有一个任务在被线程执行,其余任务在无界队列中排队),任务执行完成后唯一线程不会被释放
- 区别
- 固定大小newFixedThreadPool
- 提交任务
- Future<T> submit(Callable<T> task)
- 提交一个任务,用返回值Future等待任务执行结果(调用其中的get方法)
- List<Future<T>> invokeAll(Collection<?extends Callable<T>> tasks)
- 可以带超时时间
- 提交所有任务,将每个任务的执行结果存入队列返回
- T invokeAny(Collection<? extends Callable<T>> tasks)
- 可以带超时时间
- 提交所有任务,返回最先执行完成的任务结果,其他任务取消
- Future<T> submit(Callable<T> task)
- 关闭线程池
- void shutdown()
- 线程状态变为SHUTDOWN,不接受新任务,已经提交的任务会执行完
- 不会阻塞调用线程的执行(线程调用完shutdown后,本身的执行不会受影响)
- List<Runnable> shutdownNow()
- 线程状态变为STOP,不接受新任务,已经提交的任务会interrupt中断
- 等待队列中的任务会被返回
- void shutdown()
二、JUC(Java-Util-Concurrent)
- AQS抽象队列同步器
实现Java层面各类锁的底层同步器(C++是synchronize的Monitor)- AbstractQueueSynchronizer,是阻塞式锁和相关的同步器工具的框架,ReentrantLock继承该同步器然后实现其方法
- 底层调用park、unpark方法阻塞线程等待锁,结合cas修改锁状态state
- 内部属性
- state属性表示资源状态(独占模式 / 共享模式)
- 子类实现抽象同步器中的方法(tryAcquire/tryRelease),定义如何维护state状态,控制获取锁和释放锁
- get/set state、compareAndSetState
- 独占模式只允许一个线程访问资源,共享模式允许多个线程访问资源
- 等待队列
- 相当于Monitor的EntryList,Head + Tail组成双向队列,第一个节点Node是Dummp哑元/哨兵
- 条件变量
- 支持多条件变量Condition,相当于Monitor的WaitSet,firstWaiter + lastWaiter组成双向队列,没有哨兵节点
- state属性表示资源状态(独占模式 / 共享模式)
- ReentrantLock原理
独占锁,等待队列实现多线程互斥获得锁,条件变量队列实现多线程同步协作- 继承同步器AQS,实现其内部方法,底层调用park、unpark和cas实现互斥独占锁
- 加锁lock
- 成功
- tryAcquire执行cas设置state=1,设置Owner线程
- 失败
- state=1状态拒绝加锁,循环多次tryAcquire失败后,supportLock调用park,将该线程放入head连接的哨兵节点(状态为-1,可以unpark打断第一个节点对应的等待线程)后的节点中(最后一个状态为0,非最后一个状态为-1,可以unpark打断下一个节点对应的等待线程),进入等待锁的队列
- 成功
- 解锁unlock
- 竞争成功
- tryRelease执行cas设置state=0,清空Owner线程,没有新线程竞争锁,head连接的哨兵节点unpark解锁唤醒第二个节点存放的线程,tryAcquire成功获得锁,cas设置state=1,重设Owner线程,断开初始哨兵,将原本存放自己的节点变为哨兵(Thread=null,状态=-1(后面还有节点,无=0))
- 竞争失败
- tryRelease执行cas设置state=0,清空Owner线程,head连接的哨兵节点unpark解锁唤醒第二个节点存放的线程竞争锁时,有新线程也竞争锁,被唤醒线程tryAcquire失败未获得锁再次park阻塞等待,新线程成功tryAcquire获得锁,cas设置state=1,重设Owner线程,其余不做改变
- 竞争成功
- 锁重入
- lock线程tryAcquire获得自己已经获得的锁时state++,unlock线程TryRelease释放锁时state--到state=0时才能成功释放锁
- 打断
- 不可打断
- 陷入park(不清除打断标记)时,被interrupt后,通过interrupted获得打断标记并清除,线程不结束执行,获得锁后只是知道打断标记为真,自己打断自己
- 可打断
- 线程陷入park时,被interrupt时,直接报出异常,停止运行
- 不可打断
- 公平锁
- 在tryAcquire时,需要确保head连接的哨兵节点之后没有连接等待锁的线程,才去尝试获得锁,若有,则不竞争锁,将锁让给哨兵节点后的第二个之前就请求等待锁的节点存的线程
- 非公平锁,无论head连接的哨兵节点后是否有第二个节点存放之前就等待的线程,新来的线程都直接tryAcquire竞争线程
- 条件变量
- await
- 获得锁的Owner线程中条件变量调用await时,该线程将自己放入调用者Condition的firstWaiter连接的第一个Node节点中(没有哨兵节点),tryRelease解锁执行cas设置state,清空Owner线程,等待队列中head连接的哨兵节点会unpark唤醒其连接的线程开始竞争锁
- signal
- 从条件变量等待队列中取出firstWaiter连接的第一个节点,加入锁等待队列的最后一位(tail前一位),将其状态设置为0,其前一位(本来状态为0)状态变为-1;若加入锁等待队列失败,会重新尝试获得条件变量等待队列中下一个等待节点Node,并将其放入锁等待队列队尾(signalAll会唤醒当前条件变量中所有等待线程)
- await
- reentrantreadwriteLock读写锁
读锁、写锁分开,实现读读并发共享不互斥- 用途
- 对读和写上不同的锁,使得读读操作可并发共享锁执行(读写、写写操作还是要互斥独占锁),因为读操作发生的次数远多于写操作,读写锁可以提高效率
- 注意点
- 读写锁发生锁重入不能锁升级(读->写),只能锁降级(写->读)
- 应用案例
- 上了读锁必须先解读锁再上写锁,解锁之前必须先锁降级,(已经上了写锁的情况下)上读锁后再释放写锁,防止直接释放写锁后没有锁保证读写操作互斥,最后再释放读锁
- 用途
- semaphore信号量
信号量多线程共享锁- 允许一个共享资源被多线程同时访问的共享模式锁
- 原理
- 继承同步器AQS,允许访问共享资源的最多线程数作为构造方法的permit许可参数传入,cas设置state=permit
- 上锁
- 线程tryAcquire尝试获取Semaphore,cas设置state--,只要state>0可以一直上锁成功;当state=0时,线程获得锁失败,park线程,将线程放入head连接哨兵节点(状态=-1,可以唤醒后一个阻塞等待的线程)后的节点中,阻塞等待锁释放
- 释放锁
- 线程tryRelease释放Semaphore,cas设置state++,state>0让哨兵节点unpark唤醒哨兵节点后连接的节点中的等待锁的线程,该线程tryAcquire获得锁,成功cas设置state--,获得锁开始执行,断开初始哨兵,将原本存放自己的节点变为哨兵,只要state>0可以重复让线程尝试获取锁;失败时(锁被别的线程抢到,state=0)tryAcquire失败,再次park等待(被唤醒一次后又再次陷入阻塞)
- countdownlatch倒计时锁
共享锁多线程同步协作,实现互相等待- 功能
- 倒计时锁,相当于线程的join方法(join是底层方法),让线程等待其他线程执行完成后再继续执行,实现多个线程之间的同步协作执行
- 相比于join方法,countDownLatch适用于更多场景;join是让某线程等待其他线程执行完成以后再继续执行,但是线程池中的线程永远在等待接受任务,永远在执行不会结束,若用join方法则无法实现线程池中多任务的同步协作功能,线程池与countDownLatch组合更合理
- 在线程无需获取其他线程返回值的情况下,适用于countDownLatch,若需要等待其他线程返回值,可以用Future中的get方法等待其他线程返回值
- 用法
- 将倒计时数值作为构造函数的参数传入(并且cas设置为state),需要等待的线程用countDownLatch调用awaite方法(内部调用tryAcquire)等待倒计时数值state减为0,在同步协作线程中用countDownLatch调用countDown方法(内部调用tryRelease)对倒计时数值state-1
- 原理
- 同步器继承AQS,cas设置state
- tryAcquire尝试获得共享锁时,若state=0则获得锁成功,若state>0则park阻塞等待其他线程将倒计时数值减为0
- tryRelease释放共享锁时,state-1,若-1结束后state=0则返回真unpark唤醒阻塞线程,state>0则返回假不唤醒阻塞线程
- 功能
- Future
多线程同步协作,实现互相等待返回值- 应用情况
- 多线程同步协作情况下,线程需要等待其他线程的返回结果
- 示例
- 线程池中submit提交任务有Future类型返回值,在主线程中用Future调用get方法等待任务线程返回值,实现多线程同步协作执行
- 应用情况
- cyclicbarrier循环栅栏
可重用换代的实现多线程同步协作,”线程齐了再出发“- 用途
- countDownLatch(共享锁)的升级(cyclicbarrier独占锁),countDownLatch计数减为0后,只有重新创建一个新的countDownLatch对象再次传入计数,用于实现多线程同步协作
- cyclicbarrier构造函数参数1传入parties参数作为同步线程计数量,每当线程数到达线程同步计数量时,成功完成一次多线程同步协作,然后初始计数值归0换代;再次调用时,初始计数值随线程调用增加,再次到达传入的线程同步计数量时,会再次成功执行一次多线程同步协作(无需重新创建cyclicbarrier对象,可反复使用于实现多线程同步协作)
- cyclicbarrier构造函数参数2传入线程到齐后,所有线程恢复执行完成后,待做的收尾任务
- 实际过程
- 线程需要同步的时刻,cyclicbarrier会调用await方法,线程计数加1,若计数未达到传入的parties参数线程同步计数量,会阻塞等待计数增加到要求时再被唤醒(=等待线程到齐),所有线程一起恢复执行完成后,执行cyclicbarrier构造函数的参数2传入的收尾任务
- 注意
- 往构造函数中传入的同步计数参数1 parties=开始同步协作的必要线程数
- 用途
三、线程安全集合类
- 概述
- 遗留的安全集合
- Hashtable、Vector
- Connection.synchronized修饰的安全集合
- Connection.synchronizedList、Connection.synchronizedSet、Connection.synchronizedMap
- J.U.C安全集合
- Blockling
- 大部分实现基于锁用来阻塞的方法
- CopyOnWrite
- 写操作开销大于读操作,用于读操作大程度多过写操作的情况,弱一致性
- Concurrent
- 优点
- 用 cas(不上锁,共享变量被改变则重试获取最新值) + 多把细密度锁(需要不同锁的线程可以并发执行) 实现优化,提供高吞吐量和高并发的性能
- 缺点
- 导致弱一致性,遍历(迭代器遍历时,线程修改共享变量,可能读到旧值)、求大小(size操作可能得到错误值)、读取(可能读的过程中被其他线程修改读到旧值)
- 遍历弱一致性
- 非安全集合
- fail-fast,遍历时发生修改立即停止遍历,报出异常
- 安全集合
- fail-safe,遍历时发生修改不立即停止遍历,可能遍历得到错误值
- 非安全集合
- 优点
- Blockling
- 遗留的安全集合
- concurrenthashmap并发hashmap
- 安全集合的方法单独使用时线程安全,多个安全集合的方法组合使用时线程不安全
- computerIfAbsent+AtomicAdder
- value compuerIfAbsent(key,value);如果key不存在则计算value,并且返回value
- computerIfAbsent方法可以代替线程不安全的get和put方法组合,安全的实现其目标功能,同时用原子累加器计算value保证value值计算的安全
- JDK7并发使用非安全集合hashMap产生死链
- 多线程并发时,使用了线程不安全的hashmap集合
- JDK7版本下,线程a与线程b同时并发使用hashMap扩容,线程a完成扩容产生了新链表,线程b还在扩容中,由于线程a扩容成功导致其链表内容被改变,线程b扩容时取到新链表中错误顺序的数据导致死链
- 并发时会拿到扩容新表中错误顺序的数据,底层原因是,JDK7版本中hashmap的新元素会插入链表的头部,扩容时从旧链表中获取到最新数据,插入在新表的头处,再加入新数据后该数据会被压到链表尾,导致扩容前后元素顺序不同(扩容前新数据在头,扩容后新数据在尾)
- JDK8调整了扩容算法,元素不在加入链表头,而是保证扩容前后元素顺序相同(不扩容新加在链表头,扩容时新加在链表尾),但是这种情况会出现更频发的扩容丢数据问题,多线程并发用非安全集合hashmap扩容还是不安全
- concurrenthashMap原理
- JDK8
- cas+细密度锁加在链表头(随着扩容锁也变多并发度也提高 & 懒惰第一次使用时才创建)提高并发度;初始化大小会自动传化为2^n次方而非自己设置的值;forwardingNode在扩容时标记已经处理过的链表头,在扩容时读到fwn则在新链表中读数据;链表节点数达到阈值8时,查找效率降低,先扩容,扩容至链表头数大于64时,将链表升级为黑红树
- JDK7
- cas+细密度锁加在segment(分段锁继承自ReentrantLock本身就是一把锁)提高并发度;与JDK8相比,segment数组大小固定不能随着扩容提高并发度、segment数组和segment[0]非懒惰创建(使用时才初始化),一开始就固定大小的创建;shift移位+Mark掩码计算key对应的segment值
- JDK8
- linkedblockingqueue阻塞链表队列
- 实现细节
- 两把锁 + 哨兵节点 + 条件变量,懒惰(第一次用时)初始化,入队创建Node,支持有界,实现多线程高并发的阻塞、同步入队出队操作
- 生产者入队时获得tail锁,await阻塞等待notFull条件变量,每次只signal一个线程防止提高竞争,可以自己唤醒自己
- 消费者出队时获得head锁,await阻塞等待notEmpty条件变量,每次只signal一个线程防止提高竞争,可以自己唤醒自己
- 两把锁使得生产者与消费者可以并发执行,提高效率
- 哨兵Dummy节点可以在只有一个元素节点时保证head锁(Dummy)与tail锁(元素Node)锁住两个不同节点,防止生产者消费者拿到同一个锁;没有元素Node只有Dummy时,消费者会阻塞等待notEmpty条件变量
- 与Arrayblockingqueue阻塞数组队列区别
- 一把锁,Array强制有界、非懒惰(提前初始化)、提前创建好Node
- 实现细节
- concurrentlinkedqueue同步链表队列
- 两把[锁]+ 哨兵节点 + 条件变量,实现生产者消费者同时执行,生产者[锁]Dummy,消费者[锁]元素Node或者阻塞等待notEmpty条件变量
- [锁]用cas与volatile组合实现,不真正获得锁,允许线程修改,有线程修改时,重试获取最新值
- copyonwritearraylist写入式拷贝数组列表
- 增删改查操作时,作用于复制的新底层数组,原始数组不动,实现读读并发、读写分离并发(写写互斥)
- 读不上锁,写复制上锁开销大,弱一致性(会读到旧数据)
- 并发与一致性互斥,提高并发则会出现弱一致性问题,强一致性会上锁降低并发度