java并发编程--lock进入到AbstractQueuedSynchronizer核心抽象类

本文深入探讨了AbstractQueuedSynchronizer (AQS) 的工作原理及其与ReentrantLock的关系。从AQS的基本概念出发,详细分析了ReentrantLock如何利用AQS实现加锁和解锁过程,包括线程等待队列的管理和状态值的原子操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

AbstractQueuedSynchronizer是什么

提供一个框架来实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等).该类被设计的目的是为了实现大多数同步器的实现基础,这些同步器依赖于单个原子值来表示状态.子类必须定义更改此状态的受保护方法,并定义该状态对于正在获取或释放的对象的含义.鉴于此,该类中的其他方法执行所有排队和阻塞机制.

二、从ReentrantLock进入到AbstractQueuedSynchronizer

对于锁lock大家应该都不陌生了吧.

回顾一下lock,举例子:

PrintNumber.java:

package cn.mxl.mythread;

import java.util.concurrent.locks.ReentrantLock;

public class PrintNumber {
	private ReentrantLock lock=new ReentrantLock();
	private int number=0;
	public void printNumber() {
		lock.lock();
		System.out.println(number++);
		lock.unlock();
	}
}

MyRunnable.java:

package cn.mxl.mythread;

public class MyRunnable implements Runnable{
	private PrintNumber pn;
	public MyRunnable(PrintNumber pn) {
		// TODO Auto-generated constructor stub
		this.pn=pn;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		pn.printNumber();
	}

}

MyRun.java

package cn.mxl.mythread;

public class MyRun {
	public static void main(String[] args) {
		PrintNumber pn=new PrintNumber();
		MyRunnable mr=new MyRunnable(pn);
		Thread t1=new Thread(mr);
		Thread t2=new Thread(mr);
		t1.start();
		t2.start();
	}
}

这里想必大家并不陌生,但是你知道lock.lock之后发生了什么吗?那我们继续往下走.

三、进入到ReentrantLock.class,找到lock()方法:

public class ReentrantLock implements Lock, java.io.Serializable {
....................
....................
public void lock() {
        sync.lock();
    }
....................
....................
}

该方法是有sync类提供的,我们往上找:

我们能够看到sync在这里并没有初始化,那我们继续在ReentrantLock中寻找它的初始化,初始化的话一般都在它的构造函数中:

一个是有参构造一个是无参构造,现在我们应该理解了下面这个代码的意思了:

ReentrantLock lock=new ReentrantLock(true);

有参和无参分析方法一样,那我们就挑选一个代表性的,分析有参的吧

先分析fair=false的情况(相当于无参构造函数):

既然是非公平锁,那就new NonfairSync();我们进入到NonfairSync这个类中查看:

在这里你看到了熟悉的lock了,从ReentrantLock.lock()-->NonfairSync.lock();

我们继续分析if判断中的compareAndSetState(0, 1):

注意compareAndSetState()方法是AQS这个抽象类的方法,也就是说我们现在进入到了主题了:

这里出现了CAS这个方法,而CAS底层调用的是native方法,这里不深入探究,我们只要知道他是干嘛的就可以了,如果你想要理解更多的话,请看我另外一篇博客(java并发编程--CAS算法(Compare And Swap)):

compareAndSetState(0, 1):

如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。

参数:

expect- 预期值

update- 新值

简单来说就是,就是如果状态为0说明没有其他线程上锁,就可以获得锁,并将现在的状态改为1,说明加了一把锁上去;

因为现在假设的是第一个线程进入,所以初始化状态为0,进入if语句中;

这里setExclusiveOwnerThread()方法,将当前线程设置为独占线程,

 解释一下:transient关键字的作用是防止被修饰的变量在序列号之后反序列化的时候被持久化或者恢复,也就是相当于反序列化之后,该变量为null;

到这里第一个线程进入了,现在第二个线程调用了ReentrantLock.lock()方法,进入到这里面:

CAS之后发现,里面状态是1,不是预期中的0,所以进入else语句中,执行acquire()方法 ;

然后尝试去获取独占模式,执行tryAcquire()方法 ,进入tryAcquire()方法:


    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

进来发现AbstractQueuedSynchronizer.tryAcquire()没有去实现这个方法,那说明应该是NonfairSync这个类实现了该方法(因为之前我们就是从NonfairSync这个类的acquire()方法进入到这里,说明tryAcquire()调用的是NonfairSync类重新的方法),进入到NonfairSync类中的tryAcquire()方法:

从源码中可以看到重写了tryAcquire()方法;我们进入nonfairTryAcquire(acquires)方法:

如果当前状态为0,说明没有线程持有锁,直接将当前线程设置为独占线程;

如果不满足0,那判断是否当前线程就是独占线程,如果是就在加一层锁上去(重入锁);

如果以上两个条件都不满足,说明不能设置为独占线程,那就返回false:

回到之前那个位置:

我们tryAcquire()没成功,返回false之后,进入 acquireQueued(addWaiter(Node.EXCLUSIVE), arg),该方法中有一个addWaiter(Node.EXCLUSIVE)方法:

Node.EXCLUSIVE

根据注解的意思就是,该node节点以独占模式进入等待状态;

进入到addWaiter(Node.EXCLUSIVE)方法中:

创建当前结点,并设置模式为独占模式;

这里面判断的就是有没有尾节点,如果有的话将当前结点加到尾节点之后 ,这里又有一个CAS,这里就是如果pred是尾节点,那就将尾节点更新为node,并将pred的下一个结点该为node;

否则的话,执行enq()方法:

如果尾节点为空(能进入enq()方法,说明尾节点现在是空的,而且初始化的时候,头结点有,那么尾节点肯定有,如果连尾节点都没有了,说明该等待队列为空),那就new一个新的节点作为头节点和尾节点;然后由于该for语句是一个死循环,所以又进入for循环的else,设置了头节点,那就得将node设置为尾节点,因为这里总共只有两个,一个是刚刚new节点,所以直接将node加载后面,然后返回node的前节点;

就将node结点加到尾节点之后;

但现在为止,我们将没有拿到锁的线程放入到了等待队列中了,算是完成了AQS的一半分析了.

我们又回到acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法,进入到acquireQueued()方法中:

获取node节点的前节点:

然后进入第一个if

 判断p(node的前节点)是不是头结点,

如果是头结点,则tryAcquire(arg),又一次尝试当前线程能否拿到锁,如果拿到锁就设置node为头结点,返回中断状态;

如果是头结点,但是没有获取到锁或者不是头结点,则进入第二个if语句判断:

进入到shouldParkAfterFailedAcquire(p,node)方法:

如果前驱结点的状态为SIGNAL,那么当前结点可以park即让其等待

如果不是继续往下ws>0,说明该前驱结点被取消了,这样的话,需要去掉该节点(node的前节点),让node的前置节点换成它的前置结点的前置结点,

如果以上判断都不是,那就将前驱结点的状态设置为SIGNAL

解释一下状态值:

 当前面的函数为true说明可以将node结点park进来,才进入到这个函数parkAndCheckInterrupt():

首先park当前结点,然后返回中断状态;

然后又回来:

总体来说,这个node结点可以park而且处于中断状态的话,就将中断状态改为true;

然后我们又回到acquire(int arg)方法:

目前进行到这里加锁还剩最后一步,即对线程中断的处理,如果线程被中断,并且没有获得锁,那么会调用方法selfInterrupt来进行中断处理。 

四、加锁到这里就结束了,现在进入释放锁的过程: 

 释放锁部分的代码实际上很简单,由于只有获得锁得线程才能够释放锁,因此,如果调用释放锁的线程和占有锁的线程不是同一个,则需要抛出异常。另外一个需要注意的点就是,由于锁的可重入性,我们使用了state的值来表示锁的重入次数,因此,只有当state的值变为0,我们才能够真正的释放锁,但是,无论是否释放了锁,我们都需要将state的值修改。 

参考博客:

一叶知秋

https://www.cnblogs.com/leesf456/p/5350186.html

https://www.cnblogs.com/qmlingxin/p/9363040.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值