CS-LogN思维导图:记录CS基础 面试题
开源地址:https://github.com/FISHers6/CS-LogN
JUC
分类
线程管理
-
线程池相关类
- Executor、Executors、ExecutorService
- 常用的线程池:FixedThreadPool、CachedThreadPool、ScheduledThreadPool、SingleThreadExecutor
-
能获取子线程的运行结果
- Callable、Future、FutureTask
并发流程管理
- CountDwonLatch、CyclicBarrier、Semaphore、Condition
实现线程安全
-
互斥同步(锁)
- Synchronzied、及工具类Vector、Collections
- Lock接口的相关类:ReentrantLock、读写锁
-
非互斥同(原子类)
- 原子基本类型、引用类型、原子升级、累加器
-
并发容器
- ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue
-
无同步与不可变方案
- final关键字、ThreadLocal栈封闭
线程池
使用线程池的作用好处
-
降低资源消耗
- 重复利用已创建的线程降低线程创建和销毁造成的消耗
-
提高响应速度
- 任务到达,可以不需要等到线程创建就能立即执行
-
提高线程的可管理性
- 使用线程池可以进行统一的分配,调优和监控
线程池的参数
-
corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、handler
-
图示
常用线程池的创建与规则
-
线程添加规则
-
1.如果线程数量小于corePoolSize,即使工作线程处于空闲状态,也会创建一个新线程来运行新任务,创建方法是使用threadFactory
-
2.如果线程数量大于corePoolSize但小于maximumPoolSize,则将任务放入队列
-
3.如果workQueue队列已满,并且线程数量小于maxPoolSize,则开辟一个非核心新线程来运行任务
-
4.如果队列已满,并且线程数大于或等于maxPoolSize,则拒绝该任务,执行handler
-
图示(分别与3个参数比较)
-
-
常用线程池
-
newFixedThreadPool
- 创建固定大小的线程池,使用无界队列会发生OOM
-
newSingleThreadExecutor
- 创建一个单线程的线程池,线程数为1
-
newCachedThreadPool
- 创建一个可缓存的线程池,60s会回收部分空闲的线程。采用直接交付的队列 SynchronousQueue ,队列容量为0,来一个创建一个线程
-
newScheduledThreadPool
- 创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求
-
-
如何设置初始化线程池的大小?
-
可根据线程池中的线程
处理任务的不同进行分别估计-
CPU 密集型任务
- 大量的运算,无阻塞
通常 CPU 利用率很高
应配置尽可能少的线程数量
设置为 CPU 核数 + 1
- 大量的运算,无阻塞
-
IO 密集型任务
- 这类任务有大量 IO 操作
伴随着大量线程被阻塞
有利于并行提高CPU利用率
配置更多数量: CPU 核心数 * 2
- 这类任务有大量 IO 操作
-
-
-
使用线程池的注意事项
- 1.避免任务堆积(无界队列会OOM)、2.避免线程数过多(cachePool直接交付队列)、3.排查线程泄露
线程池的状态和常用方法
-
线程池的状态
- RUNNING(接受并处理任务中)、
SHUTDOWN(不接受新任务但处理排队任务)、
STOP(不接受新任务 也不处理排队任务 并中断正在进行的任务)、
TIDYING、TEMINATED(运行完成)
- RUNNING(接受并处理任务中)、
-
线程池停止
-
shutdown
- 通知有序停止,先前提交的任务务会执行
-
shutdownNow
- 尝试立即停止,忽略队列里等待的任务
-
线程池的源码解析
-
线程池的组成
-
1.线程池管理器
2.工作线程
3.任务队列:无界、有界、直接交付队列
4.任务接口Task -
图示
-
-
Executor家族
-
Executor顶层接口,只有一个execute方法
-
ExecutorService继承了Executor,增加了一些新的方法,比如shutdown拥有了初步管理线程池的功能方法
-
Executors工具类,来创建,类似Collections
-
图示
-
-
线程池实现任务复用的原理
-
线程池对线程作了包装,不需要启动线程,不需要重复start线程,只是调用已有线程固定数量的线程来跑传进来的任务run方法
-
添加工作线程
- 4步:1. 获取线程池状态、4.判断是否进入任务队列 3.根据状态检测是否增加工作线程4.执行拒绝handler
-
重复利用线程执行不同的任务
-
面试题
- 为什么要使用线程池?
- 如何使用线程池?
- 线程池有哪些核心参数?
- 初始化线程池的大小的如何算?
- shutdown 和 shutdownNow 有什么区别?
ThreadLocal
ThreadLocal的作用好处
- 为每个线程提供存储自身独立的局部变量,实现线程间隔离
- 即:达到线程安全,不需要加锁节省开销,减少参数传递
ThreadLocal的使用场景
- 1.每个线程需要一个独享的对象,如 线程不安全的工具类,(线程隔离)
- 2.每个线程内需要保存全局变量,如 拦截器中的用户信息参数,让不同方法直接使用,避免参数传递过多,(局部变量安全,参数传递)
ThreadLocal的实现原理
-
每个 Thread 维护着一个 ThreadLocalMap 的引用;ThreadLocalMap 是 ThreadLocal 的内部类,用 Entry 来进行存储;key就对应一个个ThreadLocal
-
get方法:取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把ThreadLocal作为key参数传入,取出对应的value
-
set方法:往 ThreadLocalMap 设置ThreadLocal对应值
initalValue方法:延迟加载,get的时候设置初始化 -
图示
缺陷注意
-
value内存泄漏
-
原因:ThreadLocal 被 ThreadLocalMap 中的 entry 的 key 弱引用。如果 ThreadLocal 没有被强引用, 那么 GC 时 Entry 的 key 就会被回收,但是对应的 value 却不会回收,就会造成内存泄漏
-
解决方案:每次使用完 ThreadLocal,都调用它的 remove () 方法,清除value数据。
-
源码图示
-
面试题
- ThreadLocal 的作用是什么?
- 讲一讲ThreadLocal的实现原理(组成结构)
- ThreadLocal有什么风险?
Callable与Future
Callable
-
引入目的
-
解决Runnable的缺陷
- 1.没有返回值,因为返回类型为void
- 2.不能抛出异常,因为没有继承Execption接口
-
-
是什么如何使用
- Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务。
- 实现Call方法,可以有返回值
Future
-
引入目的
- Future的核心思想是:一个方法的计算过程可能非常耗时,一直在原地等待方法返回,显然不明智。可以把该计算过程放到子线程去执行,并通过Future去控制方法的计算过程,在计算出结果后直接获取该结果。
-
常用方法
- get方法:获取结果,在没有计算出结果前,会进入阻塞态
-
使用场景
- 用法1:线程池的submit方法返回Future对象
- 用法2:用FutureTask来创建Future
-
注意点
- 当for循环批量获取future的结果时,容易block,get方法调用时应使用timeout限制
- Future和Callable的生命周期不能后退
-
Callable和Future的关系
-
Future相当于一个存储器,它存储未来call()任务方法的返回值结果
-
可以用Future.get方法来获取Callable接口的执行结果,在call()未执行完毕之前没调用get的线程会被阻塞
-
线程池传入Callable,submit返回Future,get获取值
-
-
FutureTask
-
FutureTask是一种包装器,可以把Callable转化成Future和Runnable,它同时实现了二者的接口。所以既可以作为Runnable任务被线程执行,又可以作为Future得到Callable的返回值
-
图示
-
final与不变性
什么是不变性(Immutable)
- 如果对象在被创建后,状态就不能被修改,那么它就是不可变的。
- 具有不变性的对象一定是线程安全的,我们不需要对其采取任何额外的安全措施,也能保证线程安全。
final的作用
- 类防止被继承、方法防止被重写、变量防止被修改
- 天生是线程安全的(因为不能修改),而不需要额外的同步开销
final的3种用法:修饰变量、方法、类
-
final修饰变量
-
被final修饰的变量,意味着值不能被修改。
如果变量是对象,那么对象的引用不能变,但是对象自身的内容依然可以变化。 -
赋值时机
-
属性被声明为final后,该变量则只能被赋值一次。且一旦被赋值,final的变量就不能再被改变,如论如何也不会变。
-
区分为3种
-
final instance variable(类中的final属性)
- 等号右侧、构造函数、初始化代码块
-
final static variable(类中的static final属性)
- 等号右侧、静态初始化代码块
-
final local variable(方法中的final变量)
- 使用前复制即可
-
-
为什么规定时机
- 根据JVM对类和成员变量、静态成员变量的加载规则来看:如果初始化不赋值,后续赋值,就是从null变成新的赋值,这就违反final不变的原则了!
-
-
-
final修饰方法(构造方法除外)
- 不可被重写,也就是不能被override,即便是子类有同样名字的方法,那也不是override,与static类似*
-
final修饰类
- 不可被继承,例如典型的String类就是final的
栈封闭 实现线程安全
- 在方法里新建的局部便咯,实际上是存储在每个线程私有的栈空间,线程栈不能被其它线程访问,所以不会有线程安全问题,如ThreadLocal
面试题
CAS
什么是CAS
- 我认为V的值应该是A,如果是的话那我就把它改成B,如果不是A(说明被别人修改过了),那我就不修改了,避免多人同时修改导致出错。
- CAS有三个操作数:内存值V、预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才将内存值修改为B,否则什么都不做。最后返回现在的V值。
- 最终执行CPU处理机提供的的原子指令
缺点
-
ABA问题
- 我认为 V的值为A,有其它线程在这期间修改了值为B,但它又修改成了A,那么CAS只是对比最终结果和预期值,就检测不出是否修改过
-
CAS+自旋,导致自旋时间过长
-
改进:通过版本号的机制来解决。每次变量更新的时候,版本号加 1,如AtomicStampedReference的compareAndSet ()
应用场景
- 1 乐观锁:数据库、git版本号; 自旋 2 concurrentHashMap:CAS+自旋
3 原子类
CAS底层实现
- 通过Unsafe获取待修改变量的内存递增,
比较预期值与结果,调用汇编cmpxchg指令
以AtomicInteger为例,分析在Java中是如何利用CAS实现原子操作的?
- 1.使用Unsafe类拿到value的内存递增,通过偏移量 直接操作内存数据
- 2.Unsafe的getAndAddInt方法,使用CAS+自旋尝试修改数据
- CAS的参数通过 预期值 与 实际拿到的值进行比较,相同就修改,不相同就自旋
- Unsafe提供硬件级别的原子操作,最终调用原子汇编指令的cmpxchg指令
锁
锁的分类
Lock锁接口
-
简介
- Lock锁是一种工具,用于控制对共享资源的访问
- 如:ReentrantLock
-
Lock和Synchronized的异同点
-
相同点
- 都能达到线程安全的目的
-
不同点
-
Lock 有比 synchronized 更精确的线程语义和更好的性能;高级功能
-
1 实现原理不同
- Synchronized 是关键字,属于 JVM 层面,底层是通过 monitorenter 和 monitorexit 完成,依赖于 monitor 对象来完成;
- Lock 是 java.util.concurrent.locks.lock 包下的,底层是AQS
-
2 灵活性不同
- Synchronized 代码完成之后系统自动让线程释放锁;ReentrantLock 需要用户手动释放锁,加锁解锁灵活
-
3 等待时是否可以中断
- Synchronized 不可中断,除非抛出异常或者正常运行完成;ReentrantLock 可以中断。一种是通过 tryLock,另一种是 lockInterruptibly () 放代码块中,调用 interrupt () 方法进行中断;
-
-
-
可见性
- happens-before规则约定;Lock与Synchronized一致都可以保证可见性
- 即下一个线程加锁时可以看到上一个释放锁的线程发生的所有操作
乐观锁与悲观锁
-
悲观锁(互斥同步锁)
-
思想
- 锁住数据,让别人无法访问,确保数据万无一失
-
实例
- Synchronized、Lock相关类
- 应用实例:select 把库锁住,属于悲观锁,更新期间其它人不能修改
-
缺点
- 在阻塞和唤醒性能开销大(用户态核心态切换、上下文切换、检查是否有线程被唤醒)
- 持有锁的线程被阻塞时无法释放,有可能造成永久阻塞
-
-
乐观锁
-
思想
- 认为自己在操作数据时不会有其它线程干扰,所以不需要锁住被操作对象
- 在更新数据的时候,去对比修改期间有没有被其它人改变过,没改过就正常修改(类似CAS思想)
- 乐观锁一般由CAS实现:CAS在一个原子操作内把数据对比且交换,在此期间不能被打断的
-
实例
- 原子类、并发容器
- 应用实例:数据库版本号控制、git版本号
-
优缺点对比
- 悲观锁一旦切换就不用再考虑切换CPU等操作了,一劳永逸,开销固定
- 乐观锁,会一步步尝试自旋来获取锁,自旋开销
-
-
对比
可重入锁与非可重入锁
-
什么是可重入
- 拿到锁的线程又请求这把锁,允许通过
-
可重入的好处
- 避免死锁(拿到锁的线程内部又请求该锁)
- 提升封装性,避免一次次加锁
-
可重入锁ReentrantLock与非可重入锁ThreadPoolExecutor的Worker类对比
公平锁和非公平锁
-
公平锁
-
介绍
- 公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁
-
优点
- 公平锁的优点是公平执行,等待锁的线程不会饿死
-
缺点
- 缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大
-
-
非公平锁
-
介绍
- 多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景
-
优点
- 减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程
-
缺点
- 处于等待队列中的线程可能会饿死,或者等很久才会获得锁
-
-
优缺点对比
-
源码分析
共享锁和排他锁
-
排他锁
-
介绍
- 排他锁,获取锁后,既能读又能写,但是此时其它线程不能获取这个锁了,只能由当前线程修改数据独享锁,保证了线程安全,synchronized
- 又称为 独占锁,写锁
-
-
共享锁
-
介绍
- 获取共享锁后,其它线程也可以获取共享锁完成读操作,但都不能修改删除数据
- 又成为 读锁
-
-
ReentrantReadWriteLock
-
读写锁的作用
- 共享锁减少了多个读都加锁的开销,线程也安全
- 在读的地方使用读锁,在写的地方写锁;在没有写锁的情况下,读操作无阻塞,提高程序效率
-
读写锁的规则
- 要么可以多读,要么只能一写
- 读写锁只是一把锁,可以通过两个方式锁定:读锁定 或 写锁定
-
一把锁两种方式锁定
- readLock() 读锁
- writeLock() 写锁
-
读线程插队策略(非公平下)
- 写锁可以随时插队,参与竞争
- 读锁仅在等待队列头节点为写的时候不允许插队;当队头为读的时候可以去插队。
-
锁升级
-
引入场景
- 假如一开始持有写锁,但我写需求完了,后面都是读的需求了,如果还占用写锁就浪费资源开销
-
策略
- 只允许降级,不允许升级
-
-
适合场景
- 读多写少,提高并发效率
-
自旋锁和阻塞锁
-
阻塞锁
-
思想
- 没拿到锁之前,会直接把线程阻塞,直到被唤醒
-
开销缺陷
- 阻塞或唤醒一个线程需要操作系统切换CPU状态来完成,恢复现场等需要消耗处理机时间;如果同步代码块的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长,得不偿失
-
-
自旋锁
-
思想
- 让当前抢锁失败的线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销
-
开销缺陷
- 自旋占用时间长,起始开销低,但消耗CPU资源开销会线性增长
-
-
源码分析
-
atomic包下的类基本都是自旋锁的实现
-
AtomicInteger的实现:自旋锁实现原理是CAS,Atomic调用Unsafe进行自增add的源码中的do-while循环就是一个自旋操作,使用CAS如果修改过程中遇到其它线程修改导致没有秀嘎四成功,就在while里死循环,直至修改成功
-
图示
-
-
适用场景
- 多核、临界区短小
可中断锁
-
介绍
- 线程B等待线程A释放锁时,线程B不想等待了,想处理其它事情,我们可以中断它
-
使用场景
- synchronized是不可中断锁,Lock是可中断锁(tryLock(time) 和 lockInterruptibly)响应中断
锁优化
-
JDK1.6 后对synchronized锁的优化
-
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
-
偏向锁
- 无竞争条件下,消除整个同步互斥,连CAS都不操作;即这个锁会偏向于第一个获得它的线程
-
轻量级锁
- 无竞争条件下,通过CAS消除同步互斥,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
-
重量级锁
- 互斥同步锁
-
自旋锁
- 为了减少线程状态改变带来的消耗,不停地执行当前线程
-
自适应自旋锁
- 自旋的时间不固定了,如设置自旋次数
-
锁消除
- 不可能存在共享数据竞争的锁进行消除;
-
锁粗化
- 锁粗化就是增大锁的作用域;如解决加锁操作在循环体内的频开销
-
-
写代码时的优化
- 缩小同步代码块、如不要锁住方法
- 减少锁的请求次数, 如一批一批请求
- 参考LongAdder的思想,每个段有自己的计数器,最后才合并
面试题
- 什么是公平锁?什么是非公平锁?
- 自旋锁解决什么问题?自旋锁的原理是什么?自旋的缺点?
- 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗?
- 说说 synchronized 和 java.util.concurrent.locks.Lock 的异同?
原子类atomic包
原子类的作用
- 原子类的作用和锁类似,都是为了保证并发下线程安全
- 粒度更细,变量级别
- 效率更高,除了高度竞争外
原子类的种类
- Atomic*基本类型原子类:AtomicInteger、AtomicLong、AtomicBoolean
- Atomic*Array数组类型原子类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- Atomic*Reference 引用类型原子类:AtomicReference等
- AtomicIntegerFiledUpdate等升级类型原子类
- Adder累加器、Accumlator累加器
AtomicInteger
-
常用方法
- get、getAndSet、getAndIncrement、compareAndSet(int expect,int update)
-
实现原理
- AtomicInteger 内部使用 CAS 原子语义来处理加减等操作。CAS通过判断内存某个位置的值是否与预期值相等,如果相等则进行值更新
- CAS 是内部是通过 Unsafe 类实现,而 Unsafe 类的方法都是 native 的,在 JNI 里是借助于一个 CPU 指令完成的,属于原子操作。
-
缺点
- 循环开销大。如果 CAS 失败,会一直尝试
- 只能保证单个共享变量的原子操作,对于多个共享变量,CAS 无法保证,引出原子引用类
- 用CAS存在 ABA 问题
Adder累加器
-
引入目的/改进思想
- AtomicLong在每一次加法都要flush和refresh主存,与JMM内存模型有关。工作线程之间不能直接通信,需要通过主内存间接通信
-
设计思想
- Java8引入,高并发下LongAdder比AtomicLong效率高,本质是空间换时间
- 竞争激烈时,LongAdder把不同线程对应到不同的Cell单元上进行修改,降低了冲突的概率,是多段锁的理念,提高了并发性
- 每个线程都有自己的一个计数器,不存在竞争
- sum源码分析:最终把每一个Cell的计数器与base主变量相加
面试题
- AtomicInteger 怎么实现原子操作的?
- AtomicInteger 有哪些缺点?
并发容器
ConcurrentHashMap
-
集合类历史
- Vector的方法被synchronizd修饰,同步锁;不允许多个线程同时执行。并发量大的时候性能不好
- Hashtable是线程安全的HashMap,方法也是被synchronized修饰,同步但并发性能差
- Collections工具类,提高的有synchronizedList和synchronizedMap,代码内使用sync互斥变量加锁
-
为什么需要
-
为什么不用HashMap
- 1.多线程下同时put碰撞导致数据丢失
- 2.多线程下同时put扩容导致数据丢失
- 3.死循环造成的CPU100%
-
为什么不用Collection.synchronizedMap
- 同步锁并发性能低
-
-
数据结构与并发策略
-
JDK1.7
- 数组+链表,拉链法解决冲突
- 采用分段锁,每个数组结点是一个独立的ReentrantLock锁,可以支持同时并发写
-
JDK1.8
- 数组+链表+红黑树,拉链法和树化解决冲突
- 采用CAS+synchronized锁细化
-
1.7到1.8改变后有哪些优点
- 1.数据结构由链表变为红黑树,树查询效率更高
- 2.减少了Hash碰撞,1.7拉链法
- 3.保证了并发安全和性能,分段锁改成CAS+synchronized
- 为什么超过8要转为红黑树,因为红黑树存储空间是结点的两倍,经过泊松分布,8冲突概率低
-
-
注意事项
- 组合操作线程不安全,应使用putIfAbsent提供的原子性操作
CopyOnWriteArrayList
-
引入目的
- Vector和SynchronizedList锁的粒度太大并发效率低,并且迭代时无法编辑exceptMod!=Count
-
适合场景
- 读多写少,如黑名单管理每日更新
-
读写规则
- 是对读写锁的升级:读取完全不用加锁,读时写入也不会阻塞。只有写入和写入之间需要同步
-
实现原理
- 创建数据的新副本,实现读写分离,修改时整个副本进行一次复制,完成后最后再替换回去;由于读写分离,旧容器不变,所以线程安全无需锁
- 在计算机内存中修改不直接修改主内存,而是修改缓存(cache、对拷贝的副本进行修改),再进行同步(指针指向新数据)。
-
缺点
- 1.数据一致性问题,拷贝不能保证数据实时一致,只能保证数据最终一致性
- 2.内存占用问题,写复制机制,写操作时内存会同时驻扎两个对象的内存
并发队列
-
为什么使用队列
- 用队列可以在线程间传递数据,缓存数据
- 考虑锁等线程安全问题的重任转移到了“队列”上
-
并发队列关系图示
-
BlockingQueue阻塞队列
-
阻塞队列是局由自动阻塞功能的队列,线程安全;take方法移除队头,若队列无数据则阻塞直到有数据;put方法插入元素,如果队列已满就无法继续插入则阻塞直到队列里有了空闲空间
-
ArrayBlockQueue
- 有界可指定容量、可公平
- Put源码加锁,可中断的上锁方法。没满才可以入队,否则一直await等待。
-
LinkedBlockingQueue
- 无界容量为MAX_VALUE,内部结构Node
- 使用了两把锁take锁和put锁互补干扰
-
PriorityBlockingQueue
- 支持优先级,无界队列
-
SynchronousQueue
- 直接传递的队列,容量0,效率高线程池的CacheExecutorPool使用其作为工作队列
-
DelayQueue
- 无界队列,根据延迟时间排序
-
-
非阻塞队列
-
ConcurrentLinkedQueue
- 使用链表作为队列存储结构
- 使用Unsafe的CAS非阻塞方法来实现线程安全,无需阻塞,适合对性能要求较高的并发场景
-
-
选择合适的队列
-
边界上看
- ArrayBlockQueue有界;LinkedBlockQueue无界适合容量大容量激增
-
内存上看
- ArrayBlockQueue内部结构是array,从内存存储上看,连续存储更加整齐。而LinkedBlockQueue采用链表结点,可以非连续存储。
-
吞吐量上看
- 从性能上看LinkedBlockQueue的put锁和锁分开,锁粒度更细,所以优于ArrayBlockQueue
-
总结并发容器对比
- 分为3类:Concurrent、CopyOnWrite、Blocking*
- Concurrent*的特定是大部分使用CAS并发;而CopyOnWrite通过复制一份元数据写加锁实现;Blocking通过ReentLock锁底层AQS实现
并发流程控制工具类
控制并发流程工具类的作用
-
控制并发流程的工具类,作用是帮助程序员更容易让线程之间相互配合,来满足业务逻辑
-
并发工具类图示
CountDownLatch倒计时门闩
-
作用(事件)
- 一个线程等多个线程、或多个线程等一个线程完成到达,才能继续执行
-
常用方法
- 构造函数中传入倒数值、await、countDown
Semaphore信号量
-
作用
- 用来限制管理数量有限的资源的使用情况,相当于一定数量的“许可证”
-
常用方法
- 构造函数中传入数量、acquire、release
Condition条件对象
-
作用
- 等待条件满足才放行,否则阻塞;一个锁可以对应多个条件
-
常用方法
- lock.newCondition、await、signal
CyclicBarrier循环栅栏
-
作用(线程)
- 多个线程互相等待,直到达到同一个同步点(屏障),再继续一起执行
-
常用方法
- 构造函数中传入个数、await
AQS
AQS的作用
- AQS是一个用于构建锁、同步器、协作工具类的框架,有了AQS后,更多的协作工具类都可以很方便的写出来
AQS的应用场景
-
Exclusive(独占)
- ReentrantLock 公平和非公平锁
-
Share(共享)
- Semaphore/CountDownLatch/CyclicBarrier
AQS原理解析
-
核心三要素
-
1.sate
- 使用一个 int 成员变量来表示同步状态 state,被volatile修饰,会被并发修改,各方法如getState、setState等使用CAS保证线程安全
- 在ReentrantLock中,表示可重入的次数
- 在Semaphore中,表示剩余许可证信号的数量
- 在CountDownLatch中,表示还需要倒数的个数
-
2.控制线程抢锁和配合的FIFO队列
- 获取资源线程的排队工作
-
3.期望协作工具类去实现的“获取/释放”等唤醒分配的方法策略
-
-
AQS的用法
- 第一步:写一个类,想好协作的逻辑,实现获取/释放方法
- 第二步:内部写一个Sync类继承AbstractQueueSynchronizer
- 第三步:Sync类根据独占还是共享重写tryAcquire/tryRelease或tryAcquireShared和tryReleaseShared等方法,在之前写的获取/释放方法中调用AQS的acquire/release或则Shared方法
AQS应用实例源码解析
-
AQS在CountDownLatch的应用
-
内部类Sync继承AQS
-
1.state表示门闩倒数的count数量,对应getCount方法获取
-
2.释放方法,countDown方法会让state减1,直到减为0时就唤醒所有线程。countDown方法调用releaseShared,它调用sync实现的tryReleaseShared,其使用CAS+自旋锁,来实现安全的计数-1
-
3.阻塞方法,await会调用sync提供的aquireSharedInterruptly方法,当state不等于0时,最终调用LockUpport的park,它利用Unsafe的park,native方法,把线程加入阻塞队列
-
总结
-
-
AQS在Semphore的应用
-
state表示信号量允许的剩余许可数量
-
tryAcquire方法,判断信号量大于0就成功获取,使用CAS+自旋改变state状态。如果信号量小于0了,再请求时tryAcquireShared返回负数,调用aquireSharedInterruptly方法就进入阻塞队列
-
release方法,调用sync实现的releaseShared,会利用AQS去阻塞队列唤醒一个线程
-
总结
-
-
AQS在ReentrantLock的应用
- state表示已重入的次数,独占锁权保存在AQS的Thread类型的exclusiveOwnerThread变量中
- 释放锁: unlock方法调用sync实现的release方法,会调用tryRelease,使用setState而不是CAS来修改重入次数state,当state减到0完全释放锁
- 加锁lock方法:调用sync实现的lock方法。CAS尝试修改锁的所有权为当前线程,如果修改失败就要调用acquire方法再次尝试获取,acquire方法调用了AQS的tryAcquire,这个实现在ReentantLock的里面,获取失败加入到阻塞队列
通过AQS自定义同步器
- 自定义同步器在实现时只需要根据业务逻辑需求,实现共享资源 state 的获取与释放方式策略即可
- 至于具体线程等待队列的维护(如获取资源失败入队 / 唤醒出队等),AQS 已经在顶层实现好了