1、ReentrantLock介绍
ReentrantLock就是一个互斥锁。类比sync。套路都类似,只不过sync是基于对象头和类实现的,ReentrantLock基于AQS实现的。
代码语言:javascript复制static ReentrantLock lock = new ReentrantLock();
publicstaticvoidmain(String[] args){
lock.lock(); // sync(){// --- 业务要求原子性
lock.unlock(); // }
}
2、AQS分析
AQS是JUC包下的一个基类,就是JUC包下提供的一个抽象类AbstractQueuedSynchronizer
AQS中有两个核心内容:
- state属性:state基于volatile修饰,并且采用CAS的方式去修改,这样state就保证了原子性,可见性,有序性。 同一时间点,只有会有一个线程成功的修改state。修改过程就是将state从旧值修改为新值
- 双向链表:线程在竞争资源时,可能会出现没有拿到资源,线程不能一直CAS,因为频繁的CAS会造成浪费CPU资源,线程需要挂起。挂起就需要考虑线程存在哪。 线程会存储在Node对象中,并且没有获取到资源的线程可能或有多个,多个Node就会组成一个双向链表。
ReentrantLock加锁流程(一)(重要)
3、ReentrantLock源码剖析-lock方法
代码语言:javascript复制// 非公平锁finalvoidlock(){
// 直接CAS尝试将state从0改为1,如果成功,返回trueif (compareAndSetState(0, 1))
// 获取锁资源成功,直接将exclusiveOwnerThread设置为当前线程,代表当前线程持有着锁资源
setExclusiveOwnerThread(Thread.currentThread());
else// 修改state失败,执行acquire方法
acquire(1);
}
// 公平锁finalvoidlock(){
// 执行acquire方法
acquire(1);
}
4、ReentrantLock源码剖析-acquire方法
代码语言:javascript复制publicfinalvoidacquire(int arg){
// tryAcquire(公平&非公平),再次尝试拿锁资源。 如果拿到返回true,没拿到返回falseif (!tryAcquire(arg) &&
// 没拿到锁资源,执行addWaiter以及acquireQueued// addWaiter:拿锁失败,将当前线程封装为Node对象,并且添加到AQS双向链表的末尾// acquireQueued:acquireQueued中会根据情况尝试再次执行tryAcquire,如果多次尝试依然拿不到,挂起线程(WAITING)
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
}
6、ReentrantLock源码剖析-tryAcquire方法
代码语言:javascript复制// 非公平锁实现finalbooleannonfairTryAcquire(int acquires){
// 获取当前线程final Thread current = Thread.currentThread();
// 拿到stateint c = getState();
// 判断为0,为0就CASif (c == 0) {
// 非公平锁直接CAS尝试将state从0改为1if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
// 代表锁资源拿到~~returntrue;
}
}
// 有线程持有当前锁,查看是否是锁重入操作elseif (current == getExclusiveOwnerThread()) {
// 如果是当前线程持有锁资源,进到else if,执行锁重入流程// 对state 1int nextc = c acquires;
// 判断锁重入是否达到上限if (nextc < 0)
// 凉凉~thrownew Error("Maximum lock count exceeded");
// 如果没有达到重入上线,修改state即可
setState(nextc);
returntrue;
}
// 没拿到锁,也不是锁重入,凉凉~returnfalse;
}
// 公平锁实现protectedfinalbooleantryAcquire(int acquires){
final Thread current = Thread.currentThread();
int c = getState();
// state为0时,公平锁的操作if (c == 0) {
// 查看AQS双向链表中是否有排队的Node// 如果没有排队的Node,直接抢锁// 如果有排队的Node,查看当前线程是不是优先级最高的Node的线程if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
returntrue;
}
}
// 锁重入操作。。elseif (current == getExclusiveOwnerThread()) {
int nextc = c acquires;
if (nextc < 0)
thrownew Error("Maximum lock count exceeded");
setState(nextc);
returntrue;
}
returnfalse;
}
publicfinalbooleanhasQueuedPredecessors(){
// 拿到头和尾
Node t = tail;
Node h = head;
Node s;
// 如果头尾不相等,证明node个数 > 1return h != t &&
// 有Node在排队// 拿到head的next,赋值给s,s就是优先级最高的节点// 排在第一位的Node中的线程是不是当前线程?
((s = h.next) == null || s.thread != Thread.currentThread());
}
7、ReentrantLock源码剖析-addWaiter方法
代码语言:javascript复制// addWaiter将当前thread封装为Node,并扔到AQS的双向链表中// mode:代表互斥。private Node addWaiter(Node mode){
// 将当前thread封装为Node
Node node = new Node(Thread.currentThread(), mode);
// 拿到tail
Node pred = tail;
// 如果tail不为null,说明双向链表中有Node节点if (pred != null) {
// 将当前Node扔到AQS双向链表中,
node.prev = pred;
// 修改tail指向时,是CAS操作if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果出现并发或者tail还没有初始化,执行enq方法
enq(node);
return node;
}
private Node enq(final Node node){
// 死循环for (;;) {
Node t = tail;
// 如果tail为null,先初始化一个伪的head节点if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 你看上面代码
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
8、ReentrantLock源码剖析-acquireQueued方法
代码语言:javascript复制// 准备挂起线程// node: 当前线程Node// arg: 1finalbooleanacquireQueued(final Node node, int arg){
// 拿锁失败标识:失败了么?是boolean failed = true;
for (;;) {
// 拿到当前节点的上一个节点final Node p = node.predecessor();
// true:当前Node的位置是head.nextif (p == head &&
// 如果是head的next,再次执行tryAcquire
tryAcquire(arg)) {
// 拿锁成功!当前Node变为head,并且设置thread和prev为null// 拿到锁的变伪节点了
setHead(node);
p.next = null; // help GC
failed = false;
returnfalse;
}
// 没有拿到锁资源,或者无法执行tryAcquire// 查看能否挂起线程,如果可以,返回true// 如果prev节点是1,代表取消,此时需要往前找到有效节点// 如果prev节点状态不是-1,需要先改为-1// 如果prev节点状态是-1,当前节点可以挂起// 为什么这么做?希望可以多执行几次tryAcquire,避免线程的挂起,因为线程挂起和唤醒需要用户态和内核态的切换,消耗性能if (shouldParkAfterFailedAcquire(p, node) &&
// 挂起线程,执行Unsafe类中的park方法,挂起线程
parkAndCheckInterrupt())
}
}
// 查看当前线程能否挂起// 每一个Node都会有一个状态,封装在waitStatus属性中// 状态分为5个,只关注前三个/*
1:代表节点取消,不排了
0:代表刚来排队,初始化状态
-1:当前节点的next节点挂起了(park了,线程WAITING)
-2:Condition,await或者signal了
-3:为了解决JDK1.5的BUG,BUG是在Semaphore产生的。
*/privatestaticbooleanshouldParkAfterFailedAcquire(Node pred, Node node){
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
returntrue;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
returnfalse;
}
9、ReentrantLock源码剖析-release方法
代码语言:javascript复制publicfinalbooleanrelease(int arg){
// 对state - 1,如果state减到0,代表锁资源完全释放干净if (tryRelease(arg)) {
Node h = head;
// 准备唤醒线程~~if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
returntrue;
}
returnfalse;
}
AQS为什么不用单向链表不可以吗?
因为排队的Node会出现取消操作,取消操作需要将当前节点的上一个节点的next执行当前节点的下一个节点,为了找到当前节点的上一个节点,需要从头遍历,成本太高了。
释放后,是拿链表中第一个?
释放的是head.next。就是第二个。
node对象 数据结构是咋样的?
Node对象里面就存储了几个属性,Node本身组成了一个双向链表,有prev,next。
为什么ReentrantLock中,head指向的Node是伪结点?
因为第一个节点是伪节点,伪节点不绑定任何线程,只有head.next后面的才绑定线程
head.next后面的才绑定线程?
因为在执行release方法时,需要判断是否需要唤醒线程。通过head节点的状态来判断后续节点是否需要被唤醒,如果head节点的状态是-1,我才需要执行唤醒后面挂起的线程。
ConcurrentHashMap在查询数据时,针对并发情况(有线程在写数据),是如何查询的?
查询数组上的数据,并发没问题
查询链表上的数据,并发也没问题
但是查询红黑树的上的数据就有问题了,因为如果存在写数据,红黑树会为了保证平衡,出现左旋和右旋操作,导致红黑树结构变化,查询就不能直接查询红黑树,他会保留一个双向链表,这是会查询双向链表
ConcurrentHashMap的size方法没有加锁,如何保证数据不出问题?(ConcurrentHashMap的addCount如何实现的)
ConcurrentHashMap中采用了类似LongAdder方式记录元素个数,内部除了基本的baseCount之后,还会有一个CounterCell[]存储着元素个数,执行size方法值需要将baseCount拿到,并且遍历CounterCell数据拿到全部数据即可。
线程池中执行addWorker时,为什么会添加一个任务为null的非核心线程?
因为执行这个方法前,会判断阻塞队列有任务,但是没有工作线程,这就会导致阻塞队列中的任务没有工作线程可以处理,一直卡在这个位置,导致任务阻塞了,所以会添加一个空任务的非核心线程处理阻塞队里的任务