-
ReentrantLock源码
-
JUC 指java.util.concurrent包下,一系列关于并发的类,JUC就是包名的首字母
-
CAS 比较并交换,可以看另一篇文章
-
AQS 指主要利用CAS来实现的轻量级多线程同步机制,并且不会在CPU上出现上下文切换和调度的情况
自定义锁
如何在自己实现一个锁?
可以定义一个属性来判断当前是否有其线程在运行,如果正在运行那么其他线程需要等待
如何实现? 例如有两个线程T1和T2,都执行同一段代码
自定义两个方法
这里忽略程序出错导致死锁的情况,正常解锁需要放在finally代码块中
当T1进入代码,将锁的改为被持有的状态
上面的伪代码当T1进入lock方法后,因为是第一个进入的,锁的状态还是0,通过cas可以改为1,修改成功返回true,进入循环return到addI方法,执行i++操作,然后进入unLock方法,将状态改为0,方法结束
假设当T1进入方法将状态改为1,那么T2进入会一直循环CAS修改,线程一直在自旋不会走下面的代码,直到锁的状态改为0,才会继续业务代码
那么我们就实现了一个简单的锁,但是这个锁有什么缺点呢? 没有获取到锁的线程会一直自旋,消耗系统资源,这个是我们不想看到的
在java中还有一个类LockSupport
,其中有一个park方法
里面继续调用UNSAFE类,这个类里的方法是使用C/C++实现,park方法的作用是将当前线程立即休眠,让出CPU,直到被唤醒,还有一个唤醒的方法
这个同样也是其他语言实现,传入需要被唤醒的线程,那么我们上面的代码可以改造为
我们修改完后即使没有获取到锁的线程也不会占用CPU的资源,但是如果出现2个以上的线程同时进行操作,那么会出现丢失线程的情况,可以再进行优化,将等待的线程存放到队列中,就不再演示了,而ReentrantLock就是主要使用CAS,park,自旋来实现的,接下来看ReentrantLock的源码
ReentrantLock
当初始化一个ReentrantLock使用默认构造时创建的是一个非公平锁
如果想创建一个公平锁则使用有参构造
这篇文章先来看公平锁的实现
加锁
没有竞争情况
它的公平锁的实现方法,是FairSync类中的,也是一个内部类,在ReentrantLock中,继承了Sync类,实现lock方法
点进tryAcquire方法
继续点进hasQueuedPredecessors方法,该方法定义在AbstractQueuedSynchronizer抽象类中的
因为当前线程执行前锁的状态是未被持有的,所以还没有初始化过队列,那么等待队列的头和尾部都为null,return的第一个判断h!=t为false,后面的&&运算符,所以直接返回
那么回到tryAcquire方法,hasQueuedPredecessors返回false,而前面有一个取反!符号,则继续执行compareAndSetState(0, acquires)
方法,通过cas改变当前锁的状态为1,然后执行setExclusiveOwnerThread
方法,该方法就是简单的赋值
继续返回到acquire
方法,为true,取反false,使用了&&阻断符,则不会执行后面的acquireQueued
方法,直接结束lock()方法,执行自定义的业务代码
tryAcquire方法什么时候走到 else if (current == getExclusiveOwnerThread()) 判断呢
ReentrantLock的特性之一就是体现在这里-重入锁
啥叫重入锁?简单讲就是在加锁后又加锁
当线程和该锁已经持有的线程相同时则会进入这个判断,将锁的状态加1,赋值给state,下面的判断state小于0可能是判断溢出的问题,即数值超出int类型最大容量则为负数,一般这种情况很少见吧
存在竞争情况
那么上面是没有其他线程竞争的情况,如果在T1加锁后,T2,T3..来尝试获取锁改怎么办呢?->进等待队列
这个还是tryAcquire方法的代码,拿下来方便查看
如果在T1进行完加锁后T2来尝试获取锁,因为state状态不为0,而当前线程和锁持有的线程又不同,则直接返回false
那么返回acquire方法中
取反为true,则执行acquireQueued方法,而acquireQueued方法中有执行了addWaiter方法,先来看addWaiter方法
使用链表的形式来存储阻塞排队的线程,来看node的内部结构
主要的三个属性
当进入这个方法后,首先将AbstractQueuedSynchronizer类中的尾部节点赋值给一个临时变量,判断尾部是否为空,假设现在线程为T2,队列还没有被初始化,尾部为空,则进入enq
方法,继续点进
还是将AbstractQueuedSynchronizer类中尾部节点赋值给临时变量t
然后判断t是否为空,因为队列还没有初始化,所以尾巴节点为空,则使用cas来设置 AbstractQueuedSynchronizer类中的头节点,之后将设置的头节点赋值给尾部
当执行完节点的关系如下
这时候有个疑问,怎么没有设置传入的Node节点呢?而是设置新new出来的Node,和参数传入的Node节点没有一点关系?
注意看上面的代码for(;;)
死循环,当下次循环的时候t已经不为空了,因为上次循环给加了一个空节点,然后将传入的Node节点的上一个赋值为t,然后通过CAS获取AbstractQueuedSynchronizer类中的尾部节点,如果尾部节点还是为t,则更改为传入的node对象,如果CAS失败,即在CAS设置前被其他线程对AbstractQueuedSynchronizer类中的尾部节点进行了修改,则进行下一次for循环,直至设置成功,当操作完成后,节点结构如下图
之后代码返回到acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
方法
还是一个for死循环,首先获取上一个节点和AbstractQueuedSynchronizer类中的头节点进行判断,如果相同则调用tryAcquire()方法尝试获取锁,因为在初始化队列过程中可能获取锁执行的线程已经执行完了,并且释放了锁,所以这里尝试一下获取锁,假设没有获取到锁,则不会进入if (p == head && tryAcquire(arg)) {}代码块,继续下面的判断,进入shouldParkAfterFailedAcquire()方法,从名称可以看到[在获取锁失败后应该睡眠]
判断上一个node节点的状态,将上一个节点的Node.SIGNAL状态的值为-1,而上面的代码中并没有对waitStatus的值进行更改,默认初始化为0,则进入最后的else代码块,通过CAS将waitStatus的值改为-1,方法返回false结束,回到acquireQueued方法中,继续进行for循环,假设还是没有获取到锁,则再次进入shouldParkAfterFailedAcquire方法中,因为上次for循环将waitStatus的值改为了-1,则这次进入了if (ws == Node.SIGNAL)
的代码块,返回true,返回到 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())判断中,因为shouldParkAfterFailedAcquire方法返回了true,则继续执行parkAndCheckInterrupt方法
当执行完parkAndCheckInterrupt方法后,T2线程就在这里进行休眠
为什么不开始就把waitStatus设置为-1呢?还要多自旋一次,有一个原因是尽量不使用park,能尝试获取到锁最好
那么假设现在又来一个线程T3
完成操作后节点关系如下
之后继续执行acquireQueued方法
解锁
假设现在T1执行unlock方法,T2,T3在队列中
进入tryRelease方法
首先将状态数值-1,判断如果当前线程和持有锁的线程不是同一个则抛出异常,即解锁的线程和加锁的不是同一个线程
判断如果c==0,也就是没有重入锁的情况,将free改为true,然后进入setExclusiveOwnerThread方法
方法返回,没有重入锁的情况,则free为true,获取AQS类中的头节点,假设不为空,ws=-1,则进入unparkSuccessor(h)
方法
首先获取头结点的状态,小于0进入代码块,将头结点的锁状态改为0,获取下一个节点,那么s就是t2,而t2的ws也是-1,所以直接进入最下面的代码块,if(s!=null),unpark(t2)线程
那么回到t2线程休眠的地方
下面的是判断线程是否被中断过,native方法,无法看到实现了,那么假设没有被中断过则返回false,那么返回上一个方法
因为parkAndCheckInterrupt方法返回false,所以进不去代码块,那么继续执行for,当执行if (p == head && tryAcquire(arg))
时p==head成立,而调用tryAcquire方法尝试获取锁成功,因为t1已经释放了,那么进入下面的代码块
经过上面的操作后节点关系如下
如果这个节点在头说明它正在执行代码,而不是排队,即使初始化时T1没有进队列,但是给它添加了一个空node,来代替它正在执行
例如有T2,T3在排队,T1线程unpark后T2线程执行,上面的代码也能说明T2会先把当前节点的线程,上下节点都设置为null,而T2线程去执行代码去了,已经在运行过程中了
看别的博客有一段解释:比如你去买车票,你如果是第一个这个时候售票员已经在给你服务了,你不算排队,你后面的才算排队
注意一点:队列头始终为空Node
如何保证公平
情况1
T1执行完unpark后,释放完锁,还没来的及唤醒队列中的T2,这时T3线程来尝试获取到锁
这种情况队列中肯定有节点排队,如果没有节点直接获取到锁也是公平的,那么有节点排队h就不等于t,true,&&运算符继续判断,h的next节点也不为null,返回false
s.thread != Thread.currentThread()
如果当前来尝试获取锁的对象不是在排队的第一个(也就是头结点的下一个节点,头结点正在运行,不算在排队的队列中)也就是其他线程插队的情况,则返回true,结果就是(true&&(false||true)) 整体返回true,外层代码取反为false,不会尝试CAS获取锁,则T3去排队
情况2
T2尝试获取锁时发现T1持有锁,于是去初始化队列,在初始化过程中T1执行完释放锁,T2执行初始化队列代码时间片用完,这时T3来尝试获取锁
此时节点关系如下
那么回到hasQueuedPredecessors
方法,看最后的return
h头节点为一个空node,而t为节点为null,不等于true继续判断,h头结点下一个为null,整体返回true,外层代码取反为false,则去排队
遗留问题
1 初始化队列以及后面的入队为什么要设置空的头节点
2 在parkAndCheckInterrupt()方法中最后调用的Thread.interrupted();一系列方法最后不改变任何东西,不明白它这个的作用,也有说是为了复用lockInterruptibly()方法,但是感觉有点牵强
__EOF__