同步相关
锁的实现
synchronized关键字
两种实现方式:
锁住方法
锁住普通方法
锁住类方法
锁住对象
锁住的无非是两个
当前对象
this
类对象
xx.class
,该对象是唯一的。
底层原理
重量级锁
查看字节码指令发现是在同步段前后获得了对象的monitor
。monitor
就是管程的实现,synchronized关键字和wait
,notiy
,notifyAll
都是管程的组成部分。
monitor
是有ObjectMonitor
实现的,它包含一个同步队列,一个等待队列
_owner
:初始值为NULL
,当有线程占有该monitor时,设置为该线程的ID,当线程释放时,再置为NULL
,JVM通过CAS操作保证其线程安全。_cxq
:竞争队列所有请求锁的线程首先会被放入这个队列中。使用头插法维护。_EntryList
:cxq队列中有资格成为候选资源的线程会被移动到该队列中。_WaitSet
:等待队列,因为调用wait方法而被阻塞的线程会被放入该队列中。
monitor
竞争过程
通过CAS尝试把monitor的owner值设置为当前线程id
若owner已经指向当前线程,则记录重入次数
recursions++
若第一次进去该monitor,设置为
recursions = 1
,owner
指向当前线程,该线程成功获得锁并返回;若操作失败,则等待锁的释放。
monitor
等待
当前线程被封装成
ObjectMonitor
对象node;for循环使用CAS把node节点插入到cxq链表中;
node节点被push到cxq中后,通过自旋尝试获取锁,如果还是没有获得锁,则通过
park
将当前线程挂起等待被唤醒;当线程被唤醒时会从挂起的点继续执行,再尝试获取锁;
notify
可以唤醒同一个monitor
下调用wait
挂起的线程。
锁的状态
在JVM中对象的布局
对象头
Mark Word:锁信息、GC年龄,hashcode等
Klass Pointer:指向类元数据的指针
实例数据
对其填充
偏向锁
通过对比Mark Word中的Thread id来解决加锁问题。只有一个线程执行同步块时提供性能。
流程:当线程访问同步块时获取锁的处理过程
检查Mark Word中的线程id
如果为空,则用CAS替换为当前线程id,替换成功->获取锁成功,替换失败->撤销
如果不为空,则检查是否是当前线程id,若是->获取成功,若不是->撤销
撤销:
撤销动作需要等待全局安全点(当前状态下,堆对象是确定一致的,JVM可以安全地进行操作)
暂停拥有锁的线程,判断锁对象是否处于被锁定状态
恢复到无锁或者升级到轻量级锁。
轻量级锁
通过CAS操作Mark Word和自旋来解决解锁问题,避免线程阻塞和唤醒从而影响性能。
场景:两条线程交替执行,不存在竞争。
加锁: 多个线程竞争偏向锁导致锁升级
JVM在当前线程的栈帧中创建锁记录,将对象MarkWord复制到锁记录中。
线程尝试使用CAS将对象头中的MarkWord和锁记录替换
替换成功,获得锁
替换失败,检查对象的mark word是否指向当前线程的栈帧(栈帧中可以存在多个Lock Record,只有第最开始创建的LockRecord中会复制MarkWord,后面创建的LockRecord中Displaced Mark Word=null用来表示重入计数)
是,获得锁
否,获得锁失败,膨胀为重量级锁。
释放锁
使用CAS将MarkWord还原
若1执行成功,则释放
若1执行失败,则膨胀为重量级锁
重量级锁
除了拥有锁的线程以外的线程都阻塞。
ReentrantLock
可重入锁,用同步状态State来控制整体可重入情况。
有两个内部类实现了公平锁ReentrantLock.FairSync
和非公平锁ReentrantLock.NonfairSync
加锁
lock
,暴露给用户的APIacquire
,AQS核心方法tryAcquire
->nonfairTryAcquire
,自定义同步器实现方法
解锁
unlock
,暴露给用户的APIrelease
,AQS核心方法tryRelease
,自定义同步器实现方法
AQS原理
AQS
是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。
核心思想:如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。
这个机制就是由CLH队列的变体实现(虚拟双向队列)
数据结构分析
Node节点,用于实现双向等待队列
state,表示同步状态,可以实现独占和共享两种模式
同步状态分析
执行
aquire(1)
时,会通过TryAcquire
获取锁,在这种情况下,如果获取失败,则调用addWaiter
加入到等待队列中addWaiter就是一个在双端链表添加尾节点的操作,需要注意的是,双端链表的头结点是一个无参构造函数的头结点。
acquireQueued会把放入队列中的线程不断去获取锁,直到获取成功或者不再需要获取(中断)。
synchronized和ReentrantLock的区别
ReentrantLock
synchronized
锁实现机制
依赖AQS
监视器模式
灵活性
支持响应中断、超时、尝试获取锁
不灵活
锁释放形式
显示调用unlock释放
自动释放
锁类型
公平&非公平锁
非公平锁
条件队列
可以关联多个条件队列
关联一个队列
可重入性
可
可
性能
适用于大量同步的场景下
适用于少量同步的情况下(竞争不激烈)
ReentrantReadWriteLock
使用AQS同步状态中的16位保存写锁持有的次数,用剩下的16位保存读锁持有的次数。
阻塞队列
四种等级的读写
抛出异常
特殊值
阻塞
阻塞超时
Insert
add
offer
put
offer(e, time)
Remove
remove
poll
take
poll(time)
Examine
element
peek
/
/
生产者消费者问题
volatile关键字
Java内存模型
主内存
工作内存,线程私有,保存了该线程使用到的变量的主内存副本拷贝。线程对变量的读写都必须在工作内存中进行,不能直接读写主内存中的变量。
特性
被volatile
关键字修饰的变量具有两种特性:
保证此变量对所有线程的可见性:当一条线程修改了这个值,新值对于其他线程来说是可以立即得知的。
禁止指令重排序优化。
底层原理
可见性
对被volatile
修饰的关键字赋值后,多执行了lock addl $0x0, (%esp)
操作。这个操作相当于一个内存屏障,将这个缓存中的变量写回主存。由于其他处理器遵守缓存一致性协议,CPU发现自己缓存中对应的内存地址被修改后,就会将CPU的缓存设置为无效状态。当处理器对这个数据进行修改操作的时候,会强制重新从主存把数据读到处理器缓存。
有序性
不能将后面的指令重排序到内存屏蔽之前的位置。
JUC中的工具类
AtomicInteger
与i++
的区别,i++
操作不是原子性操作。
CAS
Compare and swap。三个参数:内存地址,期待值,更新值
如果内存地址上的值 == 期待值,则将其更新为更新值
如果不相等,则返回false
应用
原子类
AQS中修改同步状态
ABA
在更新后追加版本号,版本号每次都累加。
BlockingQueue
CopyOnWriteArray
通过写复制实现读写分离。
线程池
本质是对任务和线程的管理,思想是将其解耦,不让两者关联,
好处:
降低资源的消耗
提高响应速度
线程复用,可以控制最大并发数,方便管理线程
7大参数
corePoolSize
maximumPoolSize
keepAliveTime
time unit
Blocking queue
Thread factory
reject policy
3大方法
Executors.newSingleThreadExecutor() c=1,m=1
Executors.newFixedThreadExecutor(n) c=n, m = n
Executors.newCachedThreadExecutor() c = 0, m = Integer.MAX_VALUE
任务管理模块
任务调度
任务调度是线程池的主要入口,该部分是线程池核心运行机制
检查线程池状态,如果不是RUNNING,则拒绝;
workerCount < coreSize ,创建新线程去执行该任务;
coreSize < workerCount && !queue.full,将该任务存入任务队列中;
coreSize < workderCount && queue.full && workerCount < maxSize,创建新线程去执行新提交的任务;
wokerCount == maxSize && queue.full,采用拒绝策略处理该任务;
任务缓冲
用阻塞队列来缓存任务。
任务申请
任务的执行存在两种方式
直接创建线程并执行任务
线程从任务队列中获取任务然后执行任务
getTask
方法实现线程获取任务
获取线程池状态,若已经停止,则返回
null
获取线程数,若数量过多,则返回
null
判断该线程是否是可回收线程
是,限时获取任务
否,阻塞获取任务
任务拒绝
线程池最大任务数(容量)= 最大线程数 + 阻塞队列容量。
当超出容量时,就要采取拒绝策略,保护线程池。
四种拒绝策略:
AbortPolicy 弹出异常
CallerRunsPolicy 让调用者执行该任务
DiscardPolicy 直接丢弃
DiscardOldestPolicy 丢弃最早的未处理的线程
线程管理模块
线程状态
新建
就绪
运行
阻塞
终止
实现原理
线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker
,它继承了AQS
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
final Thread thread;// Worker持有的线程
Runnable firstTask;// 初始化的任务,可以为null
}
线程池使用HashSet
去持有线程的引用,通过添加、移除来控制线程的生命周期。
如何判断线程的状态:继承了AQS
来实现独占锁
若worker获取了独占锁,表示线程正在执行
若worker处于空闲状态,没有独占锁,则说明它没有处理任务
Worker线程增加
addWorker(firstTask, core)
方法
Worker线程回收
线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。
Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。
核心线程默认不会被回收掉,如果设置了allowCoreTimeOut=true
时,当核心线程空闲时,会被回收。
线程池在业务中的实践
快速响应用户请求(计算密集型)
场景:购物车案例。获取最大的响应速度去满足用户,所以不应该设置阻塞队列去缓存并发任务,调高core和maxSize去尽可能创建更多的线程快速执行任务。
快速处理批量任务(IO密集型)
场景:离线的大量计算任务。任务量大,不需要瞬时完成。应该设置阻塞队列去缓冲并发任务,调整合适的核心数量。
Last updated
Was this helpful?