AQS:AbstractQueuedSynchronizer类分析&并发多线程抢锁源码分析(debug跟踪)-同步队列
2021-03-02 23:47:54 0 举报
AQS核心代码图解分析,通过IDEA多线程debug跟踪
作者其他创作
大纲/内容
Hread
这里选中指定线程来执行,其他线程先等着
线程1挂起,无法再继续执行,此时切换到线程0或者1才能继续
线程1再次尝试获取锁
prev
node
waitstate=0prev=nullnext=Thread-1节点nextWaiter=nullthread=null
同步队列
waitstate=-1prev=nullnext=Thread-2节点nextWaiter=nullthread=null
代码执行顺序
next
waitstate=0prev=前面的空节点nextWaiter=nullthread=Thread-1(当前节点)
private final boolean parkAndCheckInterrupt() { LockSupport.park(this);//挂起当前线程 return Thread.interrupted(); }
并发debug跟踪:打断点的方式,可以选中指定线程进行跟踪
waitstate=0prev=Thread-1节点next=nullnextWaiter=nullthread=Thread-2(当前节点)
unlock
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires);}
先选中线程0,让线程0开始执行,其他线程等待
ReentrantLock.NonfairSync#tryAcquire
创建一个新节点,并作为新队列的头部和尾部节点
tryRelease
waitstate=-1prev=前面的空节点next=Thread-2节点nextWaiter=nullthread=Thread-1
将waitStatus为-1的节点状态通过cas置为0,这里是将之前创建的队列的空头部节点状态改为0.然后获取头部节点的下一个节点即Thread-1
如果头部节点不为空,代表队列还有节点等待,再判断waitStatus不为0(为0就是已经唤醒了,就不用UNpark来唤醒了)
唤醒之前挂起后waitStatus设置为-1的待唤醒节点
waitstate=0prev=nullnextWaiter=nullthread=Thread-1(当前节点)
线程1执行,因为线程0目前还没释放锁,因此线程1获取锁cas失败,执行acquire(1)方法。
AbstractQueuedSynchronizer#acquireQueued
tryAcquire
t.next = node;//将头部节点的next指向当前节点
打印sum30000
java.util.concurrent.locks.AbstractQueuedSynchronizer#release
加锁加锁解锁加锁解锁解锁
AbstractQueuedSynchronizer类分析
static final class ConditionNode extends Node implements ForkJoinPool.ManagedBlocker { ConditionNode nextWaiter; //类似于下一个节点 /** *允许在ForkJoinPools中使用条件,而不存在固定池耗尽的风险。 *这只适用于非定时的条件等待,而非定时版本。 */ public final boolean isReleasable() { return status <= 1 || Thread.currentThread().isInterrupted(); } public final boolean block() { while (!isReleasable()) LockSupport.park(); return true; } }
private void setHead(Node node) { head = node; node.thread = null; node.prev = null; }
setHead(node)
p.next = null;
链接到下一个等待条件的节点,或共享的特殊值。因为条件队列只有在独占模式下保持时才被访问,所以我们只需要一个简单的链接队列来保持等待条件的节点。然后将它们转移到队列中重新获取。因为条件只能是独占的,所以我们通过使用特殊值来表示共享模式来保存字段。
Head,tail
Head
按类型标记的具体类: //独占节点 static final class ExclusiveNode extends Node { }//共享节点 static final class SharedNode extends Node { }
特性:1、先进先出(FIFO)的阻塞等待队列ConditionObject:同步等待队列,条件等待队列2、两种资源共享方式:通过volatile修饰的state字段来实现共享和独占。eg:独占:AQS实现类可以在初始化时state默认是0; 当线程进入后通过cas修改为1,后面进来的线程就无法获取锁span style=\"font-size: inherit;\
waitstate=0prev=nullnextWaiter=nullthread=null
parkAndCheckInterrupt
java同步器是基于AbstractQueuedSynchronizer(AQS)实现的,AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。
切换会线程0来释放锁
不定的方式,三个线程并发抢锁释放锁
本次是不会执行这里的,因为队列中只有空节点和Thread-1节点
内部类Node保存前后节点信息和当前节点状态
在前面acquireQueued将线程1和2都挂起了,在挂起线程1的时候就头部节点的waitStatus改为了-1,在挂起线程2的时候讲线程1的节点的waitStatus该微了-1..这里获取头部节点node的waitStatus肯定是-1
图解node.prev = pred = pred.prev;
main线程
这样就可以按指定的方式来实现线程切换,模拟出并发。但是切换必须是这两个线程都在同一个类中时,才能切换
根据ReentrantLock的构建方法可知,默认是非公平锁。即新进的线程先去获取锁,获取失败的再放到队列中
waitstate=1prev=前面的空节点next=Thead-5nextWaiter=nullthread=Thread-1(当前节点)
shouldParkAfterFailedAcquire
新建的thread线程
tail
zombie状态:因为子进程退出后,在进程表中还要占一项,并且子进程的一些资源等待父进程回收。如果父进程没有显示地调用wait来为子进程回收资源的话,在父进程退出之前子进程就变成了僵尸进程。如果父进程退出了,僵尸子进程也消失了。
然后让线程0执行到lock.unlock();此时是要去释放锁了,先不让其释放。来模拟出其他线程抢锁失败的场景。
protected final boolean tryRelease(int releases) { int c = getState() - releases;//释放一次就对state进行减一//此时独占线程肯定是当前线程,如果不是肯定是异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) {//Thread0是锁的拥有者,state为1,因此减1后为0,代表锁已经被释放完 free = true; setExclusiveOwnerThread(null); } setState(c); return free;}
Thread-0执行完后会转为僵尸线程
//等待队列的头部节点,延迟初始化 private transient volatile Node head;//等待队列的尾部节点,初始化后只可通过CAS进行修改 private transient volatile Node tail;
Thread-1节点
main线程醒来
打印
waitstate=0prev=Thead-5节点next=nullnextWaiter=nullthread=Thread-1(当前节点)
这里同样最终挂起
node的前置节点指向状态为1的节点的前置节点,将状态为1的节点剔除队列
第一次循环
volatile
再切换线程1:会发现线程1目前还在获取锁一步阻塞
src/java.base/share/classes/java/util/concurrent/locks/AbstractQueuedSynchronizer.java:底层其实就是队列
释放锁
private static int sum = 0; private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { for (int i = 0; i < 3; i++) { Thread thread = new Thread(()->{ //加锁 System.out.println(\"加锁\"); lock.lock(); try { for (int j = 0; j < 10000; j++) { sum++; // Todo 异常 } } finally { // 释放锁 System.out.println(\"解锁\"); lock.unlock(); } }); System.out.println(\"开启线程start\"); thread.start(); } try { System.out.println(\"线程睡眠\"); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(\"打印sum\"); System.out.println(sum); }
并发多线程抢锁源码分析(debug跟踪)-同步队列以ReentrantLock为例,多线程获取锁模拟
无用队列,等待回收
实现方式:一般是通过一个内部类Sync继承AQS,然后将同步器所有调用都映射到Sync对应的方法
此时Thread-0是锁的独占者,因此state为1.减1的结果就是0
public ReentrantLock() { sync = new NonfairSync(); }public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
waitstate=-1prev=nullnext=Thread-1节点nextWaiter=nullthread=null
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
volatile int waitStatus;
节点状态位,也用作参数和返回值 static final int WAITING = 1; static final int CANCELLED = 0x80000000; static final int COND = 2;
图解队列
//获取getState//设置setState//修改 原子性cas修改compareAndSetState
java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
tail:Thread-2节点
如果没有让main线程睡眠,那么sum的结果可能是0-3000的任意数。因为main线程执行打印的时候,可能sum被0到3个线程执行sum++。
addWaiter(Node.EXCLUSIVE)标记,指示节点正在独占模式下等待
第一次当前是空队列,进入if代码
先让Thread-0获取锁
头部节点有下个节点,且状态也是-1即待唤醒节点的,调用unpark来唤醒这个节点的线程。这里就是将Thread-1的线程唤醒,然后线程1执行获取锁的逻辑
for循环创建三个线程,并分别调用start方法。然后执行睡眠
切换到线程2,接着去抢锁
pred
if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; }
Node nextWaiter;
pred.prev
java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued
开启线程start开启线程start开启线程startmain线程睡眠
//锁状态 private int state;
transient:对于实现Serilizable接口的类,可以通过transient修饰来指定哪些字段不用序列化。这里因为队列一直在变化,因此队列的头尾不要序列化。
释放锁,修改之前挂起节点的waitStatus(-1-->0)。然后唤醒队列中挂起的第一个节点
Idea跟踪并发线程方式
Hread;p
waitstate=-1prev=nullnext=Thread-2节点nextWaiter=nullthread=Thread-1
默认进入非公平锁类的逻辑
unparkSuccessor唤醒队列中挂起的第一个节点(这里就是Thead-1节点)
此时线程2也是走到了挂起线程那一步,所以同样将线程1释放锁执行完,就可以切换到线程2来获取锁释放锁了
java.util.concurrent.locks.ReentrantLock.NonfairSync#lock
waitstate=0prev=nullnext=Thread-2节点nextWaiter=nullthread=Thread-1
再次尝试获取锁ReentrantLock.Sync#nonfairTryAcquire
第二次循环,进入else代码
enq(node);通过自旋将node加入到队列中
原来的同步队列
waitStatus
将当前节点置为头部节点,并将当前节点的前置节点和对应线程都置为null。即将Thread-1节点置为空节点,只保留next指向。
将当前线程放到队列中,并将当前线程的前节点指向新队列的节点
表示节点已被取消。由于超时或中断,此节点被取消。取消节点的线程不会阻塞static final int CANCELLED = 1;表示节点需要被唤醒。挂起状态,等待被唤醒。一般是获取失败后线程被挂起的节点static final int SIGNAL = -1;表示线程正在等待条件;此节点当前在条件队列中,不会用于同步队列。转到同步队列时状态值会改为0static final int CONDITION = -2;仅适用于头部节点static final int PROPAGATE = -3;0 以上情况都不是的状态,同步节点的初始化状态值就是0
新建的三个线程的并发源码跟踪
0 条评论
下一页