ReentrantLock实现原理
2020-08-19 16:17:53 0 举报
第一次学习AQS下的ReentrantLock源码
作者其他创作
大纲/内容
compareAndSetState()不存在排队线程则修改state状态0->1
线程排队
ReentrantLock
unparkSuccessor(Node node)将waitStatus置为0
NonfairSync
利用前驱节点/头节点唤醒其它节点的好处?1、被唤醒节点的waitStatus永远是0,不需要判断2、节点的状态可能会变
AQS【多线程访问共享资源的同步框架】属性解释:1、java.util.concurrent.locks.AbstractOwnableSynchronizer#exclusiveOwnerThread 独占模式下获取到锁的线程2、state—AQS就是基于状态变量的同步器,默认初始化value=0【当前锁没有被持有】3、java.util.concurrent.locks.AbstractQueuedSynchronizer.Node 同步等待队列/CLH——双向链表【保证遍历的灵活性】4、java.util.concurrent.locks.AbstractQueuedSynchronizer.Node#thread thread实例记录需要被唤醒的线程5、java.util.concurrent.locks.AbstractQueuedSynchronizer.Node#waitStatus【节点的生命状态】 waitStatus=-1(signal)——可被唤醒 waitStatus=1(cancelled)——出现异常,需要被废弃,可能是中断引起的 waitStatus=-2(condition)——条件等待 waitStatus=-3(propagate)——传播 waitStatus=0(init)——当前锁未被持有,任何线程可以抢占
current == getExclusiveOwnerThread():判断是否当前线程重复获取锁
tail=null
lock与unlock之间的代码保持同步,即同一时刻只能由一个线程操作,其余的线程会阻塞在lock中,只有获取到锁之后才能继续执行
执行完毕主动释放锁,唤醒等待的线程
prev前驱指针
锁释放tryRelease(arg)
next后驱指针
由于ReentrantLock支持这种特性,因此开发者可以在外部调用Thread.interrupt()方法,发送中断信号,手动去唤醒线程,这也是ReentrantLock可中断的原因
基于Lock锁的加、解锁分析
问题2、资源消耗问题? 2、Thread.sleep()—由于业务逻辑代码执行的时间不确定,多了不行、少了更不行,故排除!!
问题2、资源消耗问题? 1、Thread.yeild()—让出cpu的使用权;此时如果业务代码执行时间过长,前面99个线程的cpu使用权都让出来了,再次循环的时候无线程的cpu使用权可让了;不符合逻辑,故排除!!
问题3、线程唤醒问题?LockSupport.unpark(reference)方法,但是需要传递需要被唤醒的线程,即线程实例,可以将阻塞的线程放到一个容器中,需要唤醒谁从容器中拿出来执行唤醒操作即可,将unpark方法放到unlock方法中
tryRelease(arg):释放锁;将state减为0,并将exclusiveOwnerThread置为null
中断:停止当前线程手里的工作,java1.5之前,Thread中的stop方法,强制中断;如果一个线程正在执行很重要的工作,强制中断可能会引发一系列问题;Thread.Interrupted()是一种柔性方式中断Thread.Interrupted():获取当前线程中断的状态,获取了之后将中断的标志清除掉,是否存在中断信号
unlock()
lock()
是
LockSupport.unpark(s.thread)唤醒此处阻塞的线程
队列【FIFO】简单介绍:BlockingQueue:在任意时刻,不管并发有多高,在单JVM上,在同一时刻只有一个线程能进行出队、入队操作;是一个线程通信工具 特性:线程安全队列 场景:线程池、超多中间件、非常契合生产者、消费者这种场景 底层:使用ReentrantLock的锁操作 注意: ①队列不满,也可以通知消费 ②只有在CLH队列中等待的节点才能获取锁,生产者每次往等待队列【数据容器】中放一次数据就会给消费者发一个消息,通知消费者,我将数据放到了容器中,你可以从条件队列进入CLH队列排队消费输了;同理,消费者每次take完,会通知生产者,某一个数据我消费完了,你可以进入CLH队列中排队等待被唤醒去继续生产数据了。
存在排队线程
申请锁失败
acquire(1)
尝试去获取锁
此节点是为了保证队列已经被实例化了,不存放任何线程,保证之后的入队不需要考虑null等问题,方便队列的维护
问题2、资源消耗问题? 3、阻塞线程—LockSupport.park()方法,但是不能一直阻塞着,如果线程栈太多,内存会被撑爆,故需要唤醒线程。
compareAndSetHead(new Node())注意:此处没有操作需要排队的节点,而是实例化了一个新节点,主要是为了保证之后的节点入队肯定是在已经实例化好的队列上操作,不用去考虑空指针问题,这种思路可以借鉴到平时的开发中
waitstatus=0
Node包含数据域【真正要操作的节点】
核心原理1、自旋2、LockSupport-阻塞线程3、synchronized或者CAS实现执行的原子性,即线程同步4、队列【FIFO-特别契合这种阻塞场景,即公平与非公平特性】
hasQueuedPredecessors()在state=0的情况下再次判断是否存在排队的线程
exclusiveOwnerThread指向获取到锁的线程
acquireQueued(font color=\"#ff0000\
waitStatus != 0:只有不为0才能被唤醒,这也是为什么在加锁的时候第一次循环要将waitStatus置为-1的原因
enq(node):自旋,必须保证入队成功,否则入队失败的线程将被维护在内核空间,线程栈一直释放不了
锁竞争tryAcquire()hasQueuedPredecessors()判断逻辑体现公平与非公平
state!=0
parkAndCheckInterrupt()#LockSupport.park(this); return Thread.interrupted();将阻塞的唤醒,擦除中断状态interrupted = true-标记线程被中断过,因为上面将状态擦除了,我要保证外部也能知道这个行为
获取锁成功,断开指针域,丢掉前驱节点,自然获取到锁的线程节点被设置为Thread=null的头节点,保证队列的完整性,即自定义的高并发阻塞队列设计思想
FairSync
Thread=t
addWaiter(Node.EXCLUSIVE):线程入队,只有加入到CLH的线程才能被唤醒
不包含数据域Node
注意:此处的waitStatus=-1不是线程节点入队就被更改的而是在阻塞的时候第一轮循环时更改
判断入队成功的线程节点是否是首节点【你的前驱节点就是个头节点】,如果是在抢一次锁,因为线程的唤醒涉及到OS内核状态的切换,开销非常大
问题1、如何阻塞? while(true){ if(加锁成功){ break;//获取到锁跳出循环 } //如果有多个线程,假设100个,一个获取到了锁,另外这99个就会一直循环,这样会一直占着cpu的使用权不放,浪费资源,故你要让出cpu使用权 }
final Node p = node.predecessor();if (p == head && tryAcquire(arg))
release():释放锁
Thread=null
否
waitstatus=-1
手动中断线程失败的情况,自定义处理逻辑: 方式①:Thread.interrupted()—判断是否被中断过,在加入自定义的逻辑,被中断的线程会被从CLH队列中删除掉【见cancelAcquire(node)】 方式②doAcquireInterruptibly()—通过抛异常方式,外部捕获到到异常后进行一系列自定义处理;注意:在抛异常之前会将要中断的节点从CLH节点中移除【见cancelAcquire(node)】
state=0
基于模板方法模式实现—— 子类根据需要做具体业务实现
state++
移动
加锁的时候将waitStatus从0改为-1,现在又改为0,为什么?因为在非公平锁的场景下,这和线程被唤醒后,要尝试去获取锁,此时外部的另一个线程抢了本应该是它的锁,没有办法,人家是VIP,忍着吧,此时它又要去排队等锁,故有需要将waitStatus从0置为-1
线程t
tryAcquire(arg)尝试获取锁
selfInterrupt():将线程被中断过的状态码再次标记到线程上,为开发者提供便利,方便开发者设定线程的中断逻辑
收藏
0 条评论
下一页