ReentratLock是基于CAS和AQS实现的
加锁的本质
1.等待唤醒机制
基于monitor机制实现 object.wait/notify
基于线程的 LockSupport.park/unpark
2.中断机制
stop 强制线程停止,会发生脏数据,死锁等问题
interrupt 中断标志位,这是一种协作机制,不能通过一个线程中断另一个线程
接收到中断标志以后线程不会自动停止,需要通过Thread.currentThread.isInterrupted方法手动的验证状态再做剩余的操作
Thread.currentThread.isInterrupted() 不会清除中断标志位
Thread.interrupted() 会清除中断标志位
1.Thread.sleep() 可以感知到中断标志位,会立马结束等待,可以被中断的,还会抛出一个sleep interrupted 的异常,同时会清除中断标志位
2.wait 可以感知中断标志位,并且抛出一个中断异常,也会清除中断标志位
3.LockSupport.pack 可以感受到中断标志位,不会清除中断标志位
CAS
比较交换,核心方法unsafe.compareAndSwapInt(this,valueOffset,expect,update)
this :需要改变的对象
valueOffset:value变量的内存偏移地址
expect:期望更新的值
update:要更新的新值
原理:
cas 比较交换,是一只无锁的算法保证了原子性,
atomicInteger类就是通过cas保证的原子性,使用volatile保证可见性和禁止重排序
cas中原理是有有一个当前值,还有偏移量,还有当前值的内存地址,在修改的时候会先通过内存地址从主内存中获取到当前值的数据,接下来吧内存中的数据值与当前值进行比较,相等的活就进行偏移,不相等的话就进行自旋操作,只到主内存中的数据与当前值相等,结果返回true 外层通过!true的方式跳出循环
cas的缺点是只能保证一个变量的原子操作,同时长时间的自旋会给cup带来压力最后还有ABA的问题
会有ABA的问题:
例子:两个线程A,B 还有一个变量X初始值为1
A线程中要对X进行+1的操作,这时候B线程也拿到了变量X,B线程中显示对X变量进行了+10,又对X变量进行了-10,再把X写回主内存,这个时候线程A中通过内存地址从主内存中获取到了X的最新的值和当前值进行比较,比较结果为相等,进行+1的操作,但是实际上当前变量X已经被B线程操作过了,这就好比是某人挪用了公款,再被发现前把钱补回来了,没有本人发现,但是现在的钱已经不是原来的钱了,挪用公款的人可能对钱进行了操作,生出了更多的钱,这就是ABA问题
LongAdder优化了ABA的问题,实际上就是添加了版本号
AQS
使用了模板方法的设计模式,其中重点维护了一个volatile修饰的state,通过CAS去修改他的值,==0代表无锁 >0代表被加锁,还维护了一个双向列表
ReentratLock
创建有两种方式,公平和非公平,公平就是根据线程的顺序依次执行,非公平就是随机争抢资源,ReentratLock中体现在一个线程进入lock的逻辑后是直接尝试获取锁还是先去验证state的状态,直接尝试加锁就是非公平的实现,先验证状态就是公平锁的实现
ReentratLock源码分析:
AQS中维护了一个state状态来 代表有锁和无锁,还有头节点(prev),尾节点(next),当前获取锁的线程(thread)还有一个等待状态(waitStatus)
waitStatus == -1 表示当前线程可以被唤醒
waitStatus > 0 被取消的 超时或者中断
waitStatus == 0
+———+ prev +——-+ +——-+
head | | <——— | | <—— | | tail
| | ———> | | ——> | |
+———-+ +——-+ next +——-+
非公平锁:
加锁
final void lock() {
// 第一次进入会先去cas修改state的值,从0改为1,
if (compareAndSetState(0, 1))
// 如果修改成功就将AQS中的exclusiveOwnerThread修改为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 否则的话就进入入队的逻辑
acquire(1);
}
// 修改state没有成功调用
public final void acquire(int arg) {
// 不死心还要去尝试获取一下锁 tryAcquire方法如果加锁成功就会返回true
if (!tryAcquire(arg) &&
// addWaiter(Node.EXCLUSIVE) 创建队列,维护队列关系 (入队)入队后会返回当前的node节点
// acquireQueued 这里会调用LockSupport.park(this);使当前线程休眠 休眠的实现是一个死循环会先验证当前节点的上一个节点是不是头结点,要是头节点就会去再一次的尝试获 取锁,成功会替换为头结点并返回false
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 返回当前节点的上一个节点
final Node p = node.predecessor();
// 上一节点是头节点 (头节点是空的,标志当正在上锁的线程节点) 证明当前节点为排队的第一个节点,有权利去尝试获取锁
// 当休眠的线程被唤醒的时候还在这个循环里,被唤醒的时候当前线程的上一个节点就是head,那么它这个时候去尝试获取锁就会成功
if (p == head && tryAcquire(arg)) {
// 获取成功将当前节点设置为头节点 ,并且将当前节点的属性修改为空,将原来的第一个空节点出队
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// shouldParkAfterFailedAcquire里面主要使用了上一个节点的waitStatus 初始化的node节点waitStatus为0,第一次修改为-1,第二次返回true
// waitStatus > 0的时候会执行下面的代码
// node.prev = pred = pred.prev; 这行代码的意思就是当前节点(C)的上一个节点(B)的上一个节点(A)赋值给B,其实就是将原来的B进行了剔除
// 总的来说这里就是先去修改当前节点的上一个节点的waitStatus,修改为-1后才会让当前线程阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
// LockSupport.park(this); 调用park方法让当前线程休眠
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
解锁
public final boolean release(int arg) {
// 尝试释放锁
if (tryRelease(arg)) {
// 拿到当前的头结点
Node h = head;
// 重点就是h.waitStatus !=0 的时候才能去唤醒下一个线程
if (h != null && h.waitStatus != 0)
// 唤醒 h.next.thread
// 当前h.waitStatus == -1 首先cas为0
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
// 当前AQS的状态-1 代表释放锁
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果state为0就去修改当前AQS中持有锁的线程为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}