欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java锁Lock源码分析(一)

程序员文章站 2022-05-04 20:13:12
...

Java中的锁Lock源码分析(一)

Java中的锁有很多,同时也是了整个并发包的基础,可以说明白了锁整个并发包你也就能明白一半了,如果之前你有所了解的话java中的锁你或许对这些名词有些概念:

  • 独占锁、共享锁
  • 公平锁、非公平锁、重入锁
  • 条件锁
  • 读写锁

本节要点:

0)锁是如何表示的(怎么样就代表获取到了锁)
1)volatile在作用
2)lock的源码分析
3)重入锁是如何做到的
4)公平锁与非公平锁的区别

我们使用ReentrantLock的方式很简单在方法体内lock.lock();finally中执行lock.unlock()方法,非常简单。
进入源码可以看到我们常说的AQS(AbstractQueuedSynchronizer)

lock方法:

    public void lock() {
        sync.lock();
    }

Java锁Lock源码分析(一)

ASQ的成员 state int类型 volatile修饰

    private volatile int state;

Java锁Lock源码分析(一)

CAS(就是在操作AQS的state字段)最终也是调用sun.misc.Unsafe相关的方法,这个方法的四个参数第一个表示操作的是那个对象,第二个表示操作对象字段的偏移量,第三个是期望值,第四个是更新值
Java锁Lock源码分析(一)
lock方法返回就是获取了锁,即上图CAS设置整个state+1返回true,就说明获取到了锁。

要点0的答案:AQS的volatile int state +1表示获取到了锁。

通过cas设置AQS的成员status,大家注意到status是用volatile来修饰的,它在此处表示让所线程能够获取到最新更改的值。

要点1的答案

a: 保证变量在线程之间的可见性 就是上面说的
b:禁止指令重排序 在编译阶段插入内存屏障,来特定禁止指令重排序

我来先画张图表示下主存中变量和方法栈中的变量关系:
我们知道对象的成员是跟对象在堆(主存)中,方法运行时在栈中的
Java锁Lock源码分析(一)

线程在栈中运行方法修改变量的时候会从主存中拷贝一个副本到自己的栈中,当方法修改变量执行返回会把最新值写会到主存。

1)没有使用volatile修饰时,thread1和thread2同时执行同一个方法来修改state为1(cas的第三个值expcet=0,update=1),那么当thread1返回之后写会到主存,thread2没有感知到还认为是expect=0,update=1 ,thread2中就是老的数据此次cas应该是也会成功,修改了相当于是成功获取到了锁 ,对于独占锁来说肯定是不对的。

2)使用volatile修饰时,当thread1写会成功之后会让其它线程中该变量的副本失效,并重新从主存load,这样一来thread2 expect=0,update=1就会失败,因为此时的expect=0是不成立的,此时的state已经是1了如下图。
Java锁Lock源码分析(一)

volatile在此处的就是保证变量被修改的最新值,能够被其它线程感知。

要点2源码分析

ReentrantLock改造方法有个参数能决定是否是公平所

 public ReentrantLock(boolean fair) {
     sync = fair ? new FairSync() : new NonfairSync();
 }

公平锁lock方法如下:
Java锁Lock源码分析(一)

非公平锁lock方法:
Java锁Lock源码分析(一)

刚开始看的时候我一直感觉不到公平锁和非公平锁到底区别在哪里???

要点4的答案的第一部分

公平锁与非公平锁基本一样其实在方法的最外层就可以看到:
非公平锁先进性一次CAS抢占

要点4的答案的第二部分
公平锁先判断队列(双向链表)为空(head==tail)在进行cas抢占
最终两者为获取所的线程都会进入到队列中,稍后你会看到。

AQS的模板方法acquire(args)
它是ReentrantLock成员Sync的整个锁的逻辑,所有的类型的都是基于这个模板方法实现的:
Java锁Lock源码分析(一)

我们以三个线程thread1,thread2,thread3同时执行lock.lock()方法展开源码的分析,以为例非公平锁:

lock方法如下:
Java锁Lock源码分析(一)

thread1,thread2,thread3三个线程同时进入方法,都先进行一次CAS获取锁一下,设置state,expect=0,update=1,
假设thread1设置成功,那么独占线程设置为当前thread1,获取到了锁lock方法退出,thread1就可以执行业务逻辑了。

thread2和thread3都会进入else即acquire(1)方法,
成员Sync提供了整个的锁的逻辑,acquire() 为AQS实现锁逻辑的模板方法
Java锁Lock源码分析(一)

tryAcquire方法在非公平锁实现中调用了nonfairTryAcquire
Java锁Lock源码分析(一)

三个线程我们还是一个一个来分析:
thread1进入acquire(1):
如果再次调用了lock.lock()那么还是同样会进入到acquire进入到nonfairAcquire(1) 此时getState() ==1
当前线程==独占线程 再次将state+1了,此时state thread1此时就表明了重入了,nonfairTryAcquire返回true,tryAcquire返回true,acquire方法退出,lock方法退出,thread1成功的再次获取到锁,state=2了。

要点3的答案:获取锁的线程,还能在获取锁就表示重入

thread2进入acquire(1):
我们假设thread1是持有锁的线程,那么本次的tryAcqurie返回了false。进入到addWaiter(Node.EXCLUSIVE)方法。
我们先看下Node的结构:

static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
        Node() {    // Used to establish initial head or SHARED marker
        }
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

预先透漏下这就是一个FIFO的双向链表,这里waiterStatus是进入到链表之后决定是否可以尝试获取锁,每个节点的等待状态的。
前面的state是尝试获取锁的
这里的waitStatus是等待链表(队列)中的状态,不要记混了。
waitStatus有五个值:1表示链表中节点取消,0表示链表中节点初始状态 ,-1表示链表中节点等待唤醒状态,-2表示链表中节点是条件锁(下一篇博文会说),-3表示传播

addWaiter源码

Java锁Lock源码分析(一)

Java锁Lock源码分析(一)

构造独占节点进入enq方法,进入死循环中,我们来看下每循环一次都有那些变化。

下图为addWaiter的一种场景(并不是一定会这样,主要看线程执行到那个方法什么时候入队)就是一个简单的入队操作,入队成功之后就自动返回了当前Node。

Java锁Lock源码分析(一)

这里画图主要是看waitStatus跟下面的部分结合。

入队之后然后就是自旋获取锁的部分,代码:
Java锁Lock源码分析(一)

哎,又是一个死循环,来看看逻辑是如何的。
上图的节点入队是跟这里交叉的,也就是说thread2入队的时候,thread3也刚入队,两个线程也可能是同时同时进入acquireQueued的也有可能是thread2进入完了acquireQueued,thread3才刚执行addWaiter,是不确定的,但不论如何最终执行逻辑都是一样的(不管是并行的还是先后的)。

假设这里是thread2执行,获取node到prev如果为head则再次执行tryAcquire(1)如果获取到的话,thread2的node设置为队列的头结点,thread2获取返回,acquireQueued返回,acquire返回,lock返回,thread2执行业务逻辑。
如果tryAcquire(1)失败或者不是node.prev!=head(比如thread3的node),进入方法shouldParkAfterFailedAcquire(prevNode,node)
Java锁Lock源码分析(一)
ws>0的情况其实是取消了节点,图中ws的循环时将取消的节点移除掉。

这里我们以thread3执行为例(那个都一样),还记得Node.waitStatus的那5个值吗?

waitStatus有五个值:
1表示链表中节点取消,
0表示链表中节点初始状态 ,
-1表示链表中节点等待唤醒状态,
-2表示链表中节点是条件锁(下一篇博文会说),
-3表示传播

shouldParkAfterFailAcquire是在外层的死循环中if语句中被多次调用的,还是我们在来看看那每轮的结果。

Java锁Lock源码分析(一)

此时thread2和thread3都LockSupport.park(this)挂起了,阻塞住了,等待thread1唤醒,看看thread1是如何唤醒挂起线程的
lock.unlock方法的执行逻辑
Java锁Lock源码分析(一)

就两个部分tryRelease(1)和unparkSuccessor(head),也就是对应了两个操作,释放锁,唤醒下一个等待节点

释放锁:
Java锁Lock源码分析(一)
加锁的时候是对state进行加操作,release进行减操作。
重入几次即state>0,就要释放几次,很简单明了。

唤醒头结点下一个未取消的节点 upparkSuccessor(head);
Java锁Lock源码分析(一)

将head.waitStatus设置为0,找到第一个head.next.waitStatus<=0,将其唤醒。

唤醒了之后还需要从等待队列tryAccquire来抢占,再回到acquireQueued
Java锁Lock源码分析(一)

此时的等待节点的状态如下:

Java锁Lock源码分析(一)

此时thread2的node.prev==head && tryAcquire(1)便能够成功,将thread2设置为设置为head从等待队列中摘除掉。

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

thread2的acquireQueued方法返回,acquire方法返回,lock.lock()方法返回,thread2执行业务逻辑。
此时队列等待状态如下:
Java锁Lock源码分析(一)

整个流程的闭环就结束了。

补充:
上面讲的lock()方法如果当前线程获取不到锁,那么该线程会一直阻塞。实际情况下还有如下需求:

1)尝试获取锁一次,失败了返回失败,成功就返回成功,不阻塞,只是试一下。
2)超时尝试,如果尝试获取锁失败了,一直重试,或者等一会再尝试获取锁。

其实分别对应两个方法:
tryLock()
tryLock(timeout,unit)

tryLock()方法
尝试一下获取锁

public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

太简单了(非公平方式)
state=0(没有线程获取锁)的时候cas一下
state>0(有线程获取锁)的时候判断是不是自己获取到的锁,是就重入state++

tryLock(timeout,unit)方法

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}
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();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);//②
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

逻辑也是很简单:先放入阻塞队列(FIFO双向链表)
怎么样跟你像的不一样吧不是一直在尝试获取锁而是根据timeout时长来判定
1)等待时间小于1ms=1000纳秒=spinForTimeoutThreshold=1000L,就无限尝试直至超时
2)等待时间>1ms 就调用LockSupport.park(timeout)
最长失败的情况下都会讲改Node从阻塞队列(FIFO双向链表)中移除掉。

嗯,看来jdk对某个线程阻塞的时间主要靠的LockSupport来支持,最终调用unsafe来调用操作系统的阻塞时间。

总结:

ReentrantLock的底层是AQS,通过控制state完成一些锁特有的特性:重入、公平与非公平、读写锁(后面的文章会说明)
获取锁就是当前线程成功修改了AQS的volatile成员state
获取锁失败就进入到了AQS的等待队列(FIFO的双向无环链表),进入到等待队列之后开始自旋,当前节点的waitStatus=-1之后lockSupport.park()挂起自己,等待唤醒
获取锁的线程释放锁(state执行减操作),唤醒head节点之后第一个未取消的等待节点
head节点之后第一个未取消的等待节点被唤醒,判断prev是否为head 是head则尝试获取所,将自己设置为head节点,将原先老的head移除等待队列。