目录
一、ReentrantLock入门
二、AQS原理
1、AQS介绍
2、自定义锁
三、ReentrantLock实现原理
1、非公平锁的实现
加锁流程
释放锁流程
2、可重入原理
3、可打断原理
4、公平锁原理
5、条件变量原理
await流程
signal流程
一、ReentrantLock入门
相对于synchronized它具备如下特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
与synchronized一样,都支持可重入
基本语法
//获取锁
reetrantLock.lock();
try{
//临界区
}finally{
//释放锁
reentrantLock.unlock();
}
可重入
可重入指一个线程如果首次获取这把锁,他就是这把锁的拥有者,有权再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
可打断
如果用的lock.lockInterruptibly()这个方法上锁的话,别的线程是可以通过interrupt方法打断的。比如:我的a线程正在尝试获取锁,但这个lock已经被b线程拿了,b可以如果执行Interrupt就可以把a线程正在尝试的打断直接获取失败不等待。就是一种防止无限制等待的机制,避免死等,减少死锁的产生
锁超时
有个lock.tryLock()方法,返回boolean,获取到就返回true,获取不到锁就返回false,这个方法的可以传入两个参数超时时间,第一个参数数字代表时间,第二个是单位。代表他去tryLock()尝试获取锁的时候最多等待的实践,如果是1和秒就是最多尝试等待一秒,还没拿到就返回false。也是一直超时机制,防止死锁。
公平锁
syn就是非公平的,重量级的monitor的堵塞队列就是非公平的。
ReentrantLock默认是不公平,但是我们可以通过构造方法改成公平的,传的是boolean值
条件变量
syn不满足条件的线程都在一个休息室
而ReentranLock支持多休息室,唤醒的时候也是按照休息室来唤醒
使用流程:
- await进行等待(前需要获得锁)
- await执行后,会释放锁,进入conditionObject等待
- await的线程被唤醒(或打断或超时)去重新竞争lock锁
- 竞争lock锁成功后,从await后继续执行
在new完ReentranLock之后可以用newCondition()方法创建休息室,然后就可以用new出来的condition调用await方法进入休息室等待,唤醒的话是signal()方法
二、AQS原理
1、AQS介绍
Abstract Queued Synchronizer,抽象队列同步器,是阻塞式锁和相关的同步器工具框架,主要是继承他,然后重用他的功能
特点:
- state属性用来表示资源的状态(分独占模式和共享模型)子类需要定义如何维护这个状态,控制如何获取锁和释放锁
- getState 获取状态
- setState 设置状态
- compareAndSetState -cas机制设置state状态(cas防止多个线程修改state时线程安全)
- 独占模式是只有一个线程能够访问资源,共享模式允许多个线程访问资源
- 提供了基于FIFO的等待队列,类似于Monitor的EntryList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似与monitor的waitSet
获取锁:tryAcquire(arg)返回boolean,aqs里面暂停和恢复用park和unpark
释放锁:tryRelease(arg)返回boolean
2、自定义锁
自定义的锁的方法基本上都是通过AQS来进行实现的
- tryAcquire给state进行修改状态,这次使用的是独享锁,给AQS对象锁设置owner
- 然后就是tryRelease,主要就是设置state为0,解锁,释放owner
- isHeldExcusively:判断是不是有对象在占用锁
- newCondition实际上还是AQS里面的ConditionObject对象,也就是条件变量的创建
最后就是实现锁的方法,基本上都是间接调用同步器的方法来执行
class MyLock implements Lock {
//先实现同步器,实现AQS抽象类,他已经帮我们写了大多数方法,我们只需要自定义4个方法
class MySync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryAcquire(int arg) {
//尝试加锁
if(compareAndSetState(0,1)){
//这个用的是CAS的无锁机制加锁防止多线程安全问题
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
//尝试释放锁
setExclusiveOwnerThread(null);
//这个state是volatile修饰,防止指令重排
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
//锁是不是被线程持有
return getState()==1;
}
public Condition newCondition(){
return new ConditionObject();
}
}
private MySync sync=new MySync();
@Override//加锁
public void lock() {
sync.acquire(1);
}
@Override//加锁,可以被中断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override//尝试加锁,不成功就放弃
public boolean tryLock() {
return sync.tryAcquire(1);
}
//尝试加锁,超时就进入队列
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
long waitTime = unit.toNanos(time);
return sync.tryAcquireNanos(1,waitTime);
}
@Override
public void unlock() {
//他这个释放锁,调用的是release方法,这个方法还会唤醒堵塞队列中的一个线程
sync.release(1);
}
@Override
public Condition newCondition() {
// sync.newCondition();
return sync.newCondition();
}
}
三、ReentrantLock实现原理
他底层也是有个抽象Sync类继承了AQS,他有两个实现NonfairSync非公平锁和FairSync公平锁
1、非公平锁的实现
加锁流程
他先用cas的compareAndSetState尝试加锁,当没有竞争时,如果上锁成功就setExclusiveOwnerThread改成当前线程
第一个竞争出现时,他会自己tryAcquire重试一次,如果成功就加锁成功。
如果还是加锁失败就会构建node队列,head指向的是第一个节点称为dummy哑元或哨兵,是占位的,并不关联线程
然后回进入一个死循环汇总不断尝试获取锁,失败后进入park阻塞
如果自己是紧邻head的第二位,那么可以再次tryAcquire尝试获取锁,当仍然state为1,失败
将前驱的node,即head的waitStatus改为-1(表示要堵塞了,head是status-1就可以后面有责任唤醒它),这次返回false
这个时候还会再循环一遍,进入方法因为前驱节点为-1了就回返回true,然后就会调用park方法堵塞当前线程
当有多个节点都park堵塞的时候就会一个个进入,然后每个state上面的小三角都是-1表示前驱节点有责唤醒它
释放锁流程
会tryRelease,如果成功直接设置exclusiveOwnerThread为null,然后state为0
当前队列不为null时,并且head的waitStatus为-1,他就会找到队列中里head最近的node,unpark恢复他的运行,然后就会执行上面加锁的方法。
但是这个是非公平锁,假如这个时候突然来了个不在队列里面的线程4,就会跟刚刚唤醒的线程竞争,如果4先拿到锁设置为exclusiveOwnerThread,那么1将会重新park进入阻塞
2、可重入原理
以非公平锁为例,获取锁会调用nonfairTryAcquire
他会先判断state状态,如果为0说明没有加锁,直接加锁
如果不为0说明上锁了就会判断当前线程是不是和exclusiveOwnerThread里面的一样,如果一样的话就说明是重入了,直接s
释放的时候就会调用tryRelease方法,调用完就会state--,如果当减到0的时候,就会让锁释放掉,设置exclusiveOwnerThread为null,让state为0
3、可打断原理
不可打断模式
默认情况是不可打断的,线程在没变法立刻获得锁的时候,就会不断循环尝试获取锁,仍然不成功会进入park,进入park是可以被其他线程被唤醒。有线程来打断他,打断标记默认值是false,一旦被打断就会色设置为true,但是他就是改个这个标记,然后又重新进入循环park了。就是唤醒了但是没有拿到锁还是继续阻塞,只是有一个true标记
不可打断模式下,即使被打断,仍会驻留在AQS队列里面,等获得锁后才能继续执行,在获得锁以后才知道其他线程打断我了(打断标记被设置为true)
可打断模式
当调用doAcquireInterruptibly,也是去获取锁,然后park进入队列,这个时候别的线程唤醒它继续执行,直接是抛出InterruptedException异常就直接不用循环等待了,实现了可打断
4、公平锁原理
非公平锁是nonfairTryAcquire去获取锁,获取锁的时候如果为state为0说明没有人拿锁,他会直接去抢锁,没有任何判断
公平锁会多个判断条件,如果堵塞队列还有线程就不会去拿锁
5、条件变量原理
await流程
- 首先就是把线程放入到对应的await的condition队列(相当于就是休息室)
- 就是清空锁,防止可重入和获取了别的锁。然后唤醒Sync的队列的线程
- 然后就是进入condition阻塞
总结:进入condition队列,清空锁并且唤醒线程,最后就是使用park进行阻塞。
signal流程
- 首先检查线程是不是获取了锁,然后就是获取队列的头结点
- 接着就是调用doSignal唤醒first加入到Sync的队列(加入到Sync才能够进入到owner竞争锁执行),类似wait之后进入休息室,唤醒后还是要进入队列竞争
- 接着就是获取下一个节点(也就是真正的条件队列节点),获取如果是null,那么最后的节点设置为空
- 如果不为空,节点被取出来并且next节点设置为空(已经保存到firstWaiter中)
- if ( (firstWaiter = first.nextWaiter) == null)
- lastWaiter = null;
- first.nextWaiter = null;
- 首先需要知道这是一个单向链表,而且有指向的队首和指向队尾的节点,可以看下面的addConditionWaiter方法,很明显每次都是直接通过队尾下一个节点指向新节点,然后队尾=新节点。这样子的移动。那么这里的first.nextWaiter就是断开first而已,并没有把firstWaiter设置为null。只是指针没有指向下一个节点。每次相当于就是把firstWaiter队首往后面移动,然后把first节点弄到Sync队列上面去等待。
- 接着就是到把first节点转移通过方法transferForSignal,并且把节点的状态设置为0
- Node p = enq(node);最后就是把节点拼接上Sync队列,并且返回前驱节点
- 修改前驱节点状态-1。结束了signal唤醒
总结:signal完成了条件队列清除(单项链表清除),然后就是把对应的节点全部送去Sync队列。如果失败可能就是队列满了或者是超时了。最后就是取出前驱节点修改状态。