前言
之前这篇关于AQS的源码分析的文章:AbstractQueuedSynchronizer&CountDownLatch源码分析 不够详细,这篇文章再次从ReentrantLock出发来深入分析AQS的源码。可以先看看之前那篇文章大概了解一下AQS,再来看这篇文章。
ReentrantLock加锁过程
ReentrantLock默认使用非公平锁,这里先看非公平锁的实现。(后面再分析公平锁和非公平锁的区别)。
情况1:第一个线程尝试加锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
先看compareAndSetState方法:
//这个很明显了,state是AQS里面标识加锁状态的int字段。这里利用CAS无锁化机制,预期state值为0,并尝试将state值设置为1. 如果返回true,就说明现在没有人获得锁,当前线程成功将state设置为1了
//state字段是volatile修饰的,为什么是用CAS去赋值,而不是直接给state赋值呢?我的理解是,如果多个线程同时给state赋值,那就麻烦了,state倒是给置为1了,但是到底是谁第一个赋值成功的呢?这个1到底算谁的呢?根本说不清啊。
//所以你用CAS去赋值,没有并发冲突,你传给它原始值expect,要修改的值update,cpu底层会给你判断,现在内存里面是不是expect,是的话就给你修改成update,并且这个操作期间是没有人打断的。所以返回true的话你就肯定知道这个1是被我这个线程给设置的.
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
关于Unsafe放到最后面再详细解析。这里先不展开了。 然后就执行setExclusiveOwnerThread(Thread.currentThread());这句话:
//这句话的意思就是将AQS的exclusiveOwnerThread属性设置为当前线程。 这个属性含义很明显,就是当前获得排他锁的线程呗.
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
这种情况下,加锁过程就很简单了,无非就是把state设置为0,exclusiveOwnerThread设置为当前线程就可以了。
情况2:第一个线程再次尝试获取锁(可重入锁的实现)
final void lock() {
//这种情况那么这个if条件显然就不成立了
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//程序会进入这里
acquire(1);
}
进入acquire()方法看看:
public final void acquire(int arg) {
//预告一下,这种情况tryAcquire方法会返回true,意思就是加锁成功了,所以if不成立,不再继续往下执行
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
再进入tryAcquire()方法:
protected final boolean tryAcquire(int acquires) {
//注意这里调用了调用了非公平锁的实现
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//state字段是加了volatile的,所以拿的肯定是主内存中的最新值
int c = getState();
//这种情况c是等于1的,if不成立
//还有为啥要再获取一次c,判断c是不是为0呢?因为整个过程是没有加锁的,那么可能刚才state!=0,你执行到这里,刚好这期间别人已经释放了锁,state又=0了,所以再判断一次.
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//进入了这个if条件
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//这里面就把state值+1了
setState(nextc);
return true;
}
return false;
}
所以可重入锁的实现也很简单,就是把state字段值+1了。现在state值就等于2了。
情况3:第二个线程尝试加锁
public final void acquire(int arg) {
//这种情况tryAcquire肯定返回false了,会去看后面的一个条件
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
先进入addWaiter方法:
private Node addWaiter(Node mode) {
//这里传入的Node.EXCLUSIVE实际上是null.
//这里就是在构造一个Node节点,节点的nextWaiter属性就是这个mode,也就是null.
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//一开始tail肯定是null,if不成立
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//走到这儿了
enq(node);
return node;
}
进入enq()方法:
//这里方法很明显就是在初始化这个等待队列了
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
//第一次循环,先尝试用CAS把head头结点设置为空Node.为啥要用CAS?还是一样的道理,现在有多个线程在一起执行这个代码,你用CAS才能知道这个值是被你设置成功的
if (compareAndSetHead(new Node()))
//把tail尾节点也指向头结点
tail = head;
} else {
//第二次循环走到这里了。把当前节点的prev指向t
node.prev = t;
//现在尝试把tail指向当前node.注意要考虑并发的情况,所以要用cas
if (compareAndSetTail(t, node)) {
//head的next指向当前node
t.next = node;
return t;
}
}
}
}
这个方法执行完之后,队列就是这样的:
现在进入acquireQueued方法:
final boolean a www.thd178.com cquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取当前节点的prev节点,这里就是head节点了
final Node p = node.predecessor();
//如果当前节点的前一个节点是头结点,再次尝试获取锁,这里目前是获取不到锁的
if (p == head && tryAcquire(www.dasheng178.com arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//走到这里来了.第一次循环shouldParkAfterFailedAcquire返回了fale,第二次循环走到这里,这个方法返回了true.会进入parkAndCheckInterrupt方法
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(www.leyouzaixian2.com node);
}
}
进入shouldParkAfterFailedAcquire(p, node)方法:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//没有给pread设置waitStatus,所以一开始默认就是0
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > www.boyunylpt1.com 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//把pread设置成了Node.SIGNAL状态
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//最后返回了false
return false;
}
现在队列的状态是这样的:头结点的waitStatus被更新为SIGNAL.
进入parkAndCheckInterrupt方法:
private final boolean parkAndCheckInterrupt() {
//这里调用LockSupport.park方法,意思就是把当前线程给阻塞住.所以这个线程就一直卡在这里了.什么时候会被唤醒呢? 直到获得锁的线程来唤醒他,这个后面在分析
LockSupport.park(this);
return Thread.interrupted();
}
所以说到这里第二个线程就被阻塞住了.不会再往下执行了
情况4:第三个线程尝试获取锁
public final void acquire(int arg) {
//跟第二个线程一样,tryAcquire返回false,进入下一个条件
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
进入addWaiter方法:
private Node addWaiter(www.fengshen157.com/ Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//tail节点已经有了,条件成立
if (pred != null) {
//当前节点的prev指向tail节点
node.prev = pred;
//把当前节点设置为tail节点
if (compareAndSetTail(pred, node)) {
//之前的tail节点的next指向当前节点
pred.next = node;
return node;
}
}
enq(node);
return node;
}
现在的队列状态如下:
进入acquireQueued方法:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取当前节点的prev节点
final Node p = node.predecessor();
//当前节点的上一个节点,不是head,条件不成立
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//走到这里来了.第一次循环shouldParkAfterFailedAcquire返回了fale,第二次循环走到这里,这个方法返回了true.会进入parkAndCheckInterrupt方法
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
后面跟第二个线程是一样的了。第三个线程也被阻塞住了。 现在的队列状态是这样的:
也就是说,shouldParkAfterFailedAcquire这个方法的目的就是,每次有新的线程进入队列了,就把他的前一个节点的waiteStatus状态置为SINGAL(这有啥用?后面释放锁的时候就知道了).
公平锁与非公平锁
非公平锁: 根据前面非公平锁的代码分析,假设线程1持有锁,线程2和线程3阻塞等待。 线程1释放锁,正常应该是先把state状态改为0,exclusiveOwnerThread改为null,再去唤醒线程2. 但是假如再唤醒线程2之前,有个线程4突然冒出来尝试获取锁,发现state是0,就把锁给获取到了。 这就是非公平锁。 就是说每个线程获取锁并不完全是按照先来后到的顺序进行的,有可能后来的线程先获得了锁。
公平锁: 公平锁就是每个线程就是先来后到,按照排队的顺序,一定是先排队的线程先获得锁。
看一下公平锁是怎么实现的:
final void lock() {
//公平锁一上来直接进入了acquire方法。 不会像非公平锁那样一上来就先尝试一把看看能不能获得锁
acquire(1);
}
进入acquire方法,这里是跟非公平锁一样的:
public final void acquire(int arg) {
//tryAcquire方法里面有点不一样,其他逻辑都是一样的
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
进入tryAcquire方法:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//公平锁的关键就是在这里,要先判断一下现在队列中是否已经有线程在排队,没有的话才会去尝试获取锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
进入hasQueuedPredecessors方法:
//就是在判断等待队列中是否已经有线程了
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
//这里我一开始很懵逼,head!=tail和head.next!=Thread.currentThread()我能理解能够说明当前线程中有人在等待。但是head.next=null是什么意思?为什么有这个判断??
//后来我终于想明白了。去看一下enq方法里面初始化队列的逻辑:1.先初始化head,2.然后当前节点prev指向head,3.tail指向当前节点,4.最后是把head的next指向当前节点。 所以说当有一个线程走到第三步的时候,还没来得及把head.next执行自己,那么此时head.next是等于null的。 这个时候上一个线程其实已经入队了,所以这种情况就应该判断为队列中已经有线程了.
//不得不佩服这里逻辑之缜密.
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
tryLock给线程设置等待时间
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
//这里把时间统一转为纳秒
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
进入tryAcquireNanos方法:
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//tryAcquire不用看了,直接看后面获取不到锁的情况
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
进入doAcquireNanos方法:
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
//将自己加入等待队列
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
//还没获得锁,赶紧返回false
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
//没什么花头,无非就只是阻塞操作设置一个超时时间而已,到时间就自动醒来了。LockSupport底层是调用Unsafe里面的本地方法,具体CPU怎么操作的,我也不知道
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
锁的释放过程
public void unlock() {
sync.release(1);
}
进入release方法:
public final boolean release(int arg) {
//如果这个线程上的锁重入了,假如加了两次锁。第一次tryRelease返回fasle,直接退出了。 只剩一把锁的时候,这里返回true,会进入if条件
if (tryRelease(arg)) {
Node h = head;
//如果头结点不为null,并且头结点的状态不为0,才会去唤醒下一个线程。因为为0的话代表这个节点后面没有节点了.
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
进入tryRelease方法:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//如果c为0了,把exclusiveOwnerThread也改为null
free = true;
setExclusiveOwnerThread(null);
}
//如果是可重入锁,就只是把state值-1了而已,什么都没干,锁实际还没释放
//我去,为啥这里设置state值的时候就不用CAS了??为啥??因为这个方法里面不会有并发冲突问题呀。只有获得锁的这个线程才可能执行tryRelease方法是不?而此时别的线程都在阻塞等待,不可能去改state的值。所以这里就没有并发冲突问题,所以可以直接设置值。
setState(c);
return free;
}
进入unparkSuccessor方法:
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
//此时这个几点的waitStatue是SIGNAL,也就是-1
if (ws < 0)
//把这个节点的等待状态设置为0
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
//不会进入if里面,因为s不为空,并且waitStatus为-1
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//这里就是在唤醒下一个线程了
LockSupport.unpark(s.thread);
}
总结
关于CAS无锁化机制的理解
在AQS的源码里面大量使用了CAS机制,所以说加锁性能比较好(实际上jdk1.6之后底层对sychronized底层做了优化,现在貌似sychronized和ReentrantLock性能是差不多的)。
另外因为用了无锁化机制,那么代码里面就要经常注意,对共享变量的操作,共享变量使用valotile关键字修饰或者使用Atotic类,这样读的时候一定读的是最新值。 写的时候,你就要考虑是不是有并发冲突问题,有的话,你就用CAS的机制去修改。
我们知道valotile修饰的变量是对线程立即可见的。 那么如果我们使用CAS去修改值的话,对别的线程也是立即可见的吗?应该不是的。因为AQS源码和Atomic类里面使用CAS机制修改的变量都是使用了valotile修饰的。也就是说使用CAS避免并发冲突,然后使用valotile保证线程立即可见。
Unsafe类的理解
UNsafe类我们的应用程序可以直接用么?不能!
//私有构造函数,没法new
private Unsafe() {}
//这个方法你也没法调用,调用的话编译器就报错了
//这个注解是啥意思?
@CallerSensitive
public static Unsafe getUnsafe()
{
Class localClass = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(localClass.getClassLoader())) {
throw new SecurityException("Unsafe");
}
return theUnsafe;
}
我们看AtomicLong的incrementAndGet方法:
private static final long valueOffset;
//valueOffset只的是这个类中value这是属性的内存地址偏移量。这个偏移量是在类初始化的时候就获取的
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicLong.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile long value;
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
再进入unsafe.getAndAddLong方法:
public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2)
{
long l;
do
{
//先根据对象和属性的内存地址偏移量得到当前属性值
l = getLongVolatile(paramObject, paramLong1);
//然后再调用本地方法,判断内存中的值是不是l,如果是,就修改为l + paramLong2,否则的话,就死循环不断去获取,再尝试修改
} while (!compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));
return l;
}
底层应该就是cpu根据对象的地址和偏移量直接定位到这个属性的内存位置去读写的。这个内存偏移量我是这么理解的:就是说你在类初始化的时候就计算出了这个偏移量。 因为类加载的时候你就是知道这个类有哪些属性,基本类型占去几个字节,引用类型占去8个字节,那么你就能算出来当你new出这个实例的时候,这个属性的内存地址相当于实例内存地址的偏移量了。
另外我认为用这个偏移量去读取/修改属性值,可能改的工作内存中的值,而不一定是共享内存的值。因为源码里面CAS操作的共享变量都是加了volatile关键字的。 所以CAS和volatile一般就是结合起来操作变量的。不然你这个变量如果没有加volatile,那就很尴尬,如果你CAS比较的是工作内存,发现值没变,但共享内存的值其实已经变了,那就很尴尬了。
LockSupport
这个工具类就是提供了一系列park/unpark操作。用于阻塞/唤醒线程的。 那用这个park/unpark和wait/notify有什么区别?
wait/notify是和synchronized结合使用的。阻塞期间还会释放锁。
park/unpark这个不是和锁绑定的。你就是可以单独使用,park阻塞当前线程,然后别的线程可以调用unpark去唤醒那个线程。 park还
ReentrantLock源码分析
最新推荐文章于 2022-02-22 04:48:40 发布