jdk11源码-ReentrantLock源码

简介: jdk11 ReentrantLock 源码分析

更多java源码分析请见:jdk11源码分析系列文章专栏:Java11源码分析

@[toc]

概述

ReentrantLock是java中常用的加锁工具,下面是一个典型的写法:

ReentrantLock lock = null;
try {
    System.out.println(System.currentTimeMillis());
    lock = new ReentrantLock();
    lock.lock();
    lock.lock();

    TimeUnit.SECONDS.sleep(1);
    System.out.println(System.currentTimeMillis());
} catch (Exception e) {
    e.printStackTrace();
} finally {
    if (null != lock) {
        lock.unlock();
        lock.unlock();
    }
}

ReentrantLock使用非常灵活,支持公平锁和非公平锁,并且可以与condition配合使用来实现复杂的应用场景。接下来就详细分析一下ReentrantLock的源码实现。

类图
所有源码的解读,都要现有一个概览,从全局解读出发,然后逐步细化理解各个模块的含义。我们先看一下整体的类图:
在这里插入图片描述
其中Sync是ReentrantLock内部的一个抽象类。NonfairSync和FairSync是ReentrantLock公平锁和非公平锁的实现。AbstractQueuedSynchronizer维护了AQS队列,AQS队列是ReentrantLock实现的核心。

new ReentrantLock

ReentrantLock构造函数有两个重载,当参数fair=true时,创建的是公平锁(公平与非公平锁后面会讲)。默认创建的是非公平锁。

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();//需注意这里三元运算符的优先级高哦
}

AQS队列

这里简单介绍一下AQS队列的结构,后面会详细介绍队列的变更请求。
AQS的核心实现在AbstractQueuedSynchronizer,该类继承了AbstractOwnableSynchronizer。AbstractOwnableSynchronizer内部实现比较简单,核心代码:

/**
 * 独占模式下,当前锁的拥有者。
 * 指某个线程拥有锁
 */
private transient Thread exclusiveOwnerThread;

protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

上面提到了独占模式,AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,ReentrantLock使用该模式)和Share(共享,多个线程可同时执行,Semaphore/CountDownLatch使用该模式)。
在AbstractQueuedSynchronizer的内部类Node中定义了这两种模式:

static final class Node {
    /** 共享模式 */
    static final Node SHARED = new Node();
    /** 独占模式 */
    static final Node EXCLUSIVE = null;
}

好了,进入正题,AQS队列。AQS队列是一个等待队列,他是CLH队列的变体。关键字段如下:

private transient volatile Node head;//队列头 
private transient volatile Node tail; //队列尾
private volatile int state;  //状态

static final class Node {
        static final Node SHARED = new Node();//共享模式
        static final Node EXCLUSIVE = null;  //独占模式

//下面这四个值是变量waitStatus的值
        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;//当前节点代表的线程
        /**
        排他锁模式下表示正在等待条件的下一个节点,因为只有排他锁模式有conditions;所以在共享锁模式下,我们使用’SHARED’这个特殊值来表示该字段。
        */
        Node nextWaiter;
}

state是关键之一,他是volatile的,保证了线程间可见性。ReentrantLock加锁的所标记就记录在state中,state初始值是0,每当线程请求一个锁,state加1,在本文开始的代码中,连续获取两个锁,获取锁成功后,state值是2。ReentrantLock是可重入的,之所以可重入,就是依赖这个volatile 的state保证的。当然这里可重入指的是同一个获取锁的线程可以多次获取锁,然后获取N次锁,必须unlock释放N次,保证state=0才表明当前线程释放了锁。
在多线程并发请求锁时,采用CAS修改state的值,修改成功则获取锁成功,修改失败则加入到AQS等待队列尾部。

waitStatus

waitStatus是状态属性,AQS队列的核心之一。他有四个值:

  • static final int SIGNAL = -1
    这个节点的后继节点是阻塞的(LockSupport.park(this)),所以当前节点被释放或者取消时需要唤醒它的后继节点(后继线程)。
  • static final int CANCELLED = 1;
    这个节点由于超时或中断被取消了。CANCELLED 状态的节点永远不会再次阻塞。
  • static final int CONDITION = -2;
    这个节点当前在一个condition队列中。
  • static final int PROPAGATE = -3;
    一个releaseShared操作必须被广播给其他节点。(只有头节点的)该值会在doReleaseShared方法中被设置去确保持续的广播,即便其他操作的介入。
  • 0:不是上面的值的情况。
    这个值使用数值排列以简化使用。非负的值表示该节点不需要信号(通知)。因此,大部分代码不需要去检查这个特殊的值,只是为了标识。

对于常规的节点该字段会被初始化为0,竞争节点该值为CONDITION。

AQS队列典型结构图:
在这里插入图片描述

AQS队列的头结点并不关联任何线程,他是一个默认的Node节点。后面enq方法会具体讲

非公平锁加锁

lock.lock();

我们先看非公平锁的实现。由于是非公平锁,所以lock.lock();调用的是NonfairSync类的lock方法:

static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0, 1))      //1
                setExclusiveOwnerThread(Thread.currentThread());  //2
            else
                acquire(1);  //3
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);   //4
        }
    }

public final void acquire(int arg) {//5
        if (!tryAcquire(arg) &&  //6
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))   //7
            selfInterrupt();   //8
    }

总体流程如下:
第一步:CAS原子性的修改AbstractQueuedSynchronizer#state的值,由0改为1,成功则说明当前线程加锁成功.
然后第二步,设置AbstractOwnableSynchronizer#exclusiveOwnerThread的值为当前线程,表示当前锁的拥有者是当前线程。
如果第一步中修改失败,则进入第三步:acquire(1)。申请1个state(共享锁可以申请多个哦),这里可以理解为申请一个信号量,因为ReentrantLock的实现基本都是在java层面使用代码实现的。
acquire方法中首先尝试获取锁tryAcquire(第6步),如果获取失败,则将当前线程以独占模式Node.EXCLUSIVE加入等待队列尾部(addWaiter方法)。
acquireQueued():以独占无中断模式获取锁,这个方法会一直无限循环,直到获取到资源或者被中断才返回。如果等待过程中被中断则返回true。这里有自旋锁的意思,加入队列中的线程,不断的重试检测是否可以执行任务。

接下来一个一个方法逐个阅读源码:

tryAcquire

tryAcquire的具体实现是在NonfairSync类中,然后调用其父类Sync 中的nonfairTryAcquire()方法。

static final class NonfairSync extends Sync {
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
    
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();//:1、获取volatile int state的值
    if (c == 0) {//2:state=0表示当前可以加锁
        if (compareAndSetState(0, acquires)) {//CAS将state设置为acquires的值
            setExclusiveOwnerThread(current);//设置当前拥有锁的线程
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {//当前锁的拥有者线程是currentThread
        int nextc = c + acquires;//将state累加上acquires
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);//设置state的值。由于这里只有获取锁的线程才能执行,所以不会出现并发,不需要额外的加锁处理
        return true;
    }
    return false;//当前锁的拥有者线程不是currentThread,直接返回false,也就是获取锁失败
}

注释已经添加好,nonfairTryAcquire的实现还是比较简单的:如果当前没有锁,那么加锁。如果已经有了锁,那么看看当前锁的拥有者线程是不是currentThread,是则累加state的值,不是则返回失败。
**特别说明:这也是ReentrantLock为什么是可重入锁的原因,同一个线程加多次锁(lock.lock)也就是给state的值累加而已。**

addWaiter和enq方法

按照指定模式(独占还是共享)将节点添加到等待队列。

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    //1、首先尝试以快速方式添加到队列末尾
    Node pred = tail;//pred指向现有tail末尾节点
    if (pred != null) {
        node.prev = pred;//新加入节点的前一个节点是现有AQS队列的tail节点
        if (compareAndSetTail(pred, node)) {//CAS原子性的修改tail节点
            pred.next = node;//修改成功,新节点成功加入AQS队列,pred节点的next节点指向新的节点
            return node;
        }
    }
    //2、pred为空,或者修改tail节点失败,则走enq方法将节点插入队列
    enq(node);
    return node;
}
private Node enq(final Node node) {
    for(;;) {//CAS
        Node t = tail;
        if (t == null) { // 必须初始化。这里是AQS队列为空的情况。通过CAS的方式创建head节点,并且tail和head都指向同一个节点。
            if (compareAndSetHead(new Node()))//注意这里初始化head节点,并不关联任何线程!!
                tail = head;
        } else {//这里变更node节点的prev指针,并且移动tail指针指向node,前一个节点的next指向新插入的node
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
  • 上面addWaiter方法的第一行代码new Node(Thread.currentThread(), mode);,会创建一个node对象,该对象的重要属性值初始化为:
nextWaiter = Node.EXCLUSIVE; // Node.EXCLUSIVE值为null
thread = Thread.currentThread();
waitStatus = 0;// 默认是0
  • addWaiter首先会以快速方式将node添加到队尾,如果失败则走enq方法。失败有两种可能,一个是tail为空,也就是AQS为空的情况下。另一是compareAndSetTail失败,也就是多线程并发添加到队尾,此时会出现CAS失败。
  • 注意enq方法,在t==null时,首先创建空的头节点,不关联任何的线程,nextWaiter和thread变量都是null.

AQS队列操作过程中的并发问题

先看一下没有并发的情况,AQS队列的变化过程:

在这里插入图片描述

思考个问题,这里enq方法会存在并发的问题,那么如何保证线程安全的呢?
首先使用CAS来操作线程间的共享变量,比如head,tail。由compareAndSetTail和compareAndSetHead方法保证。

//通过objectFieldOffset方法来获取属性的偏移量,用于后面的CAS操作
headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
//CAS:比较交换
private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

其次,prev,next,t这几个变量如何保证线程安全的呢?
enq方法中的tail = head;这行代码,肯定只有一个线程执行,因为上面有一个CAS,这里只有一个线程会修改成功,进入if方法体。而且这里只存在于AQS队列没有初始化时只执行一次,比较简单。
另外,t是方法内部局部变量,是线程私有的,不会有线程安全问题。当多个线程都想添加到AQS队列尾部,肯定只有一个执行compareAndSetTail成功并且移动tail指针指向最新的末尾node,其他线程则会重新执行for循环。

acquireQueued

上面tryAcquire失败没有获取到锁,addWaiter加入了AQS等待队列,进入acquireQueued方法中,acquireQueued方法以独占无中断模式获取锁,这个方法会一直无限循环,直到获取到资源或者被中断才返回。有点类似于自旋锁的意思。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//是否获取到资源
    try {
        boolean interrupted = false;//是否中断
        for (;;) {
            final Node p = node.predecessor();//获取前一个节点
            if (p == head && tryAcquire(arg)) {//如果当前node节点是第二个节点,紧跟在head后面,那么tryAcquire尝试获取资源
                setHead(node);//获取锁成功,当前节点成为head节点
                p.next = null; // 目的:辅助GC
                failed = false;
                return interrupted;//返回是否中断过
            }
            
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())//当shouldParkAfterFailedAcquire返回成功,也就是前驱节点是Node.SIGNAL状态时,进行真正的park将当前线程挂起,并且检查中断标记,如果是已经中断,则设置interrupted =true。如果shouldParkAfterFailedAcquire返回false,则重复上述过程,直到获取到资源后者被park。
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);//添加AQS失败,取消任务
    }
}

//前面讲过,head节点不与任何线程关联,他的thread是null,当然head节点的prev肯定也是null
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

//在Acquire失败后,是否要park中断
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws= pred.waitStatus;//获取到上一个节点的waitStatus
    if (ws == Node.SIGNAL)//前面讲到当一个节点状态时SIGNAL时,他有责任唤醒后面的节点。所以这里判断前驱节点是SIGNAL状态,则可以安心的park中断了。
        return true;
    if (ws > 0) {
        /*
         * 过滤掉中间cancel状态的节点
         * 前驱节点被取消的情况(线程允许被取消哦)。向前遍历,知道找到一个waitStatus大于0的(不是取消状态或初始状态)的节点,该节点设置为当前node的前驱节点。
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * 修改前驱节点的WaitStatus为Node.SIGNAL。
         * 明确前驱节点必须为Node.SIGNAL,当前节点才可以park 
         * 注意,这个CAS也可能会失败,因为前驱节点的WaitStatus状态可能会发生变化
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

//阻塞当前线程
//park并且检查是否被中断过
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

获取非公平锁过程总结

在这里插入图片描述

cancelAcquire

上面acquireQueued方法在出现异常时,会执行cancelAcquire方法取消当前node的acquire操作。接下来看看cancelAcquire()都做了哪些事情。

private void cancelAcquire(Node node) {
    if (node == null)
        return;

    node.thread = null;

    // 跳过中间CANCELLED状态的节点
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    Node predNext = pred.next;

    // 将node设置为CANCELLED状态
    node.waitStatus = Node.CANCELLED;

    // 如果当前节点是tail节点,则直接移除
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {//如果pred不是head节点并且是SIGNAL 状态,或者可以设置为SIGNAL 状态,那么将pred的next设置为node.next,也就是移除当前节点
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            unparkSuccessor(node);//唤醒node的后继节点
        }

        node.next = node; // help GC
    }
}
private void unparkSuccessor(Node node) {
    //如果waitStatus为负数,则将其设置为0(允许失败)
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    //唤醒当前节点后面的节点。通常是紧随的next节点,但是当next被取消或者为空,则从tail到node之间的所有节点,往后往前查找知道找到一个waitStatus <=0的节点,将其唤醒unpark
    Node s = node.next;
    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);
}

总结一下:
1、设置thread变量为空,并且设置状态为canceled
2、跳过中间的已经被取消的节点
3、如果当前节点是tail节点,则直接移除。否则:
4、如果其前驱节点不是head节点并且(前驱节点是SIGNAL状态,或者可以被设置为SIGNAL状态),那么将当前节点移除。否则通过LockSupport.unpark()唤醒node的后继节点

公平锁加锁

公平锁与非公平锁的区别就在于这里,在tryAcquire方法中,首先会检查是否有任何线程等待获取的时间长于当前线程
在这里插入图片描述

public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

很简单,就是看看AQS队列是否为空,如果不为空,那么head的下一个节点是否为当前请求的线程,如果不是,说明前面有其他线程排队,当前线程应该加入等待队列中。

release

public final boolean release(int arg) {
    if (tryRelease(arg)) {//尝试释放资源  state
        Node h = head;
        if (h != null && h.waitStatus != 0)//如果AQS不为空,并且头节点的waitStatus不是0【重点】
            unparkSuccessor(h);//unpark后继节点
        return true;
    }
    return false;
}

//这里不需要加锁,因为只有获取锁的线程才会来释放锁,所以这里直接将state减去releases即可
protected final boolean tryRelease(int releases) {
    intc = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    //
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)//注意这里是从AQS队列的尾节点开始查找的,找到最后一个 waitStatus<=0 的那个节点,将其唤醒。
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

这里的重点是,彻底释放完资源(state=0)后,会去唤醒AQS队列中的一个等待节点,该节点查找顺序为从AQS队列的尾节点开始查找的,找到最后一个 waitStatus<=0 的那个节点,通过LockSupport.unpark将其唤醒。
这里可能会有疑问,h=head ; h.waitStatus !=0 这个判断合适成立,head初始化时waitStatus=0,waitStatus 是什么时候修改为非0的?其实上面已经列出来了,在shouldParkAfterFailedAcquire中,会有这么一行代码compareAndSetWaitStatus(pred, ws, Node.SIGNAL);,不仅head节点,所有加入AQS队列的节点的前驱节点都会被设置为SIGNAL,因为他们被park后,需要unpark才可以继续执行。
综上,AQS队列是一个FIFO队列。

AQS内部实际状态

这里模拟一种实际加锁的情况,看看AQS队列中实际状态是什么样的。
线程伪代码如下:

new Thread(() -> {
    try {
        lock.lock();
        //do something
    }finally {
        lock.unlock();
    }        
}).start();

假设有4个线程,并发竞争互斥锁。那么队列中有几个节点?state的值是多少?
测试代码如下:

ReentrantLock lock = new ReentrantLock();
newThread(() -> {
    lock.lock();
    try {TimeUnit.SECONDS.sleep(100);} catch (InterruptedException e) {}
    lock.unlock();
}, "aaa").start();
new Thread(() -> {
    lock.lock();
    try {TimeUnit.SECONDS.sleep(100);} catch (InterruptedException e) {}
    lock.unlock();
}, "bbb").start();
new Thread(() -> {
    lock.lock();
    try {TimeUnit.SECONDS.sleep(100);} catch (InterruptedException e) {}
    lock.unlock();
}, "ccc").start();
new Thread(() -> {
    lock.lock();
    try {TimeUnit.SECONDS.sleep(100);} catch (InterruptedException e) {}
    lock.unlock();
}, "ddd").start();

new Thread(() -> {
    for(int i = 0; i < 1000 ; i ++){
        try {
            System.out.println("---> " + lock.getQueueLength() + "  " +  lock.getHoldCount());
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
},"eee").start();

TimeUnit.SECONDS.sleep(50000);//主线程等待

运行代码,打印输出;

---> 3  0
---> 3  0
---> 3  0
---> 3  0

此时看一下线程快照,可以发现,bcd三个线程全部是WAITING状态,而且是由于Unsafe.park导致的WAITING。aaa线程已经进入了TIMED_WAITING状态,由Thread.sleep触发的。
在这里插入图片描述
在这里插入图片描述
那么分析一下---> 3 0这个输出结果。
getQueueLength只是简单统计一下AQS队列中的节点数量。源码如下:

public final int getQueueLength() {
    intn = 0;
    for (Node p = tail; p != null; p = p.prev) {//从tail尾结点向前累加
        if (p.thread != null)
            ++n;
    }
    return n;
}

再来看看getHoldCount方法。由于调用getHoldCount方法的线程是eee, 该线程不是加锁的线程,所以会返回0。不过没关系,我们可以加断点,来看一下state的值是多少。

final Thread getOwner() {
    return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
    return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
    return getState() != 0;
}

在这里插入图片描述
由于我们这里测试的是ReentrantLock,是互斥锁,所以state为1。

加断点看一下所有节点的真实状态,验证上面的逻辑分析。bbb,ccc,ddd三个线程追加在head节点的后面,head节点和ccc,ddd线程的waitstatus=-1,这是因为在添加到AQS队列中时会将其前驱节点设置为NODE.SIGNAL状态。
当然多线程运行时,bbb,ccc,ddd的顺序可能会不同。
在这里插入图片描述
更直观一点:
在这里插入图片描述

几个疑问

AQS是FIFO的,那么他是怎么实现非公平锁的?

AQS队列先进先出不假。但是仔细阅读源码,可以发现,在lock时,会首先尝试获取锁。这里就达到了一定的非公平性。思考这种情况:已经存在了AQS队列,且不为空。当获取锁的线程将state全部释放,还没有来得及唤醒后继线程时,此时新加入的线程,则会通过这里的CAS获取到锁,相当于插队了。这也是为什么公平锁的代码中没有这段代码的原因。
在这里插入图片描述
另外,在tryAcquire()方法中,公平和非公平方法获取锁的区别也是在于:非公平锁不去校验AQS中是否有不是当前线程的前驱节点(hasQueuedPredecessors())。

非公平锁:饥饿

上面也提到了,即使AQS队列中有等待的线程,新来的线程也可能抢先获取到锁。在竞争激烈的情况下,AQS队列中的线程,可能会长时间获取不到锁,影响了正常任务执行,这就是饥饿。

ReentrantLock性能是如何保证的?

首先代码中大量使用了CAS来操作,CAS是无锁的,所以性能比较高。
但是在竞争激烈的情况下,,CAS频繁的进行循环比较,也是非常浪费资源的,比如CPU时钟。因此在jdk代码中,有两处进行了优化:

  1. 非公平锁的lock方法中,提前进行一次CAS操作,如果没有这一个其实也是可以的,但是首先进行一次CAS操作,就可以使得一部分线程可以在加入AQS队列前就获取锁,提高了性能。
    在这里插入图片描述
  2. 在入队时,首先尝试直接加入到队列末尾,因为大部分情况下直接加入队尾是OK的。这样避免了enq方法中的其他的复杂逻辑。
    在这里插入图片描述

ReentrantLock是通过java代码层实现的,那么其线程安全是如何保证 的?

整个实现过程中,对于共享变量,通过CAS来实现。其他的变量都在线程内部,是私有变量,不会发生线程安全问题。

羊群效应

简单描述一下羊群效应:假设AQS队列中已经存在了很多等待的线程,那么当获取锁的线程释放锁时,有一种做法是取唤醒所有等待的线程,让他们都去竞争锁,这也符合非公平锁的特征,是比较容易想到的方案。
但是这中方案会造成巨大的资源浪费,首先是需要通知所有的线程去释放锁,然后所有的线程都去竞争锁,虽然ReentrantLock加锁主要是在java层面,但是也是需要耗费大量的CAS操作及CPU时钟,最终只有一个线程抢到锁,其他的还是乖乖回去等待,固不可取。
ReentrantLock给出的解决方案是:维护一个 FIFO 队列,也就是AQS队列,队列中每个节点只关心其前一个节点的状态,线程唤醒也只唤醒队头等待线程。如此一来,即保证了非公平锁的特性,又大大降低了多个线程之间的竞争,提升了性能。

这种方式在《从PAXOS到ZOOKEEPER分布式一致性原理与实践》中的分布式锁一章中也有讲解,请自行查阅。

更多java源码分析请见:jdk11源码分析系列文章专栏:Java11源码分析

相关文章
|
20天前
|
XML Java 编译器
Java注解的底层源码剖析与技术认识
Java注解(Annotation)是Java 5引入的一种新特性,它提供了一种在代码中添加元数据(Metadata)的方式。注解本身并不是代码的一部分,它们不会直接影响代码的执行,但可以在编译、类加载和运行时被读取和处理。注解为开发者提供了一种以非侵入性的方式为代码提供额外信息的手段,这些信息可以用于生成文档、编译时检查、运行时处理等。
59 7
|
1月前
|
数据采集 人工智能 Java
Java产科专科电子病历系统源码
产科专科电子病历系统,全结构化设计,实现产科专科电子病历与院内HIS、LIS、PACS信息系统、区域妇幼信息平台的三级互联互通,系统由门诊系统、住院系统、数据统计模块三部分组成,它管理了孕妇从怀孕开始到生产结束42天一系列医院保健服务信息。
32 4
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
77 2
|
2月前
|
Java Apache Maven
Java百项管理之新闻管理系统 熟悉java语法——大学生作业 有源码!!!可运行!!!
文章提供了使用Apache POI库在Java中创建和读取Excel文件的详细代码示例,包括写入数据到Excel和从Excel读取数据的方法。
65 6
Java百项管理之新闻管理系统 熟悉java语法——大学生作业 有源码!!!可运行!!!
|
3月前
|
数据采集 运维 前端开发
【Java】全套云HIS源码包含EMR、LIS (医院信息化建设)
系统技术特点:采用前后端分离架构,前端由Angular、JavaScript开发;后端使用Java语言开发。
126 5
|
13天前
|
存储 JavaScript 前端开发
基于 SpringBoot 和 Vue 开发校园点餐订餐外卖跑腿Java源码
一个非常实用的校园外卖系统,基于 SpringBoot 和 Vue 的开发。这一系统源于黑马的外卖案例项目 经过站长的进一步改进和优化,提供了更丰富的功能和更高的可用性。 这个项目的架构设计非常有趣。虽然它采用了SpringBoot和Vue的组合,但并不是一个完全分离的项目。 前端视图通过JS的方式引入了Vue和Element UI,既能利用Vue的快速开发优势,
76 13
|
4月前
|
Kubernetes jenkins 持续交付
从代码到k8s部署应有尽有系列-java源码之String详解
本文详细介绍了一个基于 `gitlab + jenkins + harbor + k8s` 的自动化部署环境搭建流程。其中,`gitlab` 用于代码托管和 CI,`jenkins` 负责 CD 发布,`harbor` 作为镜像仓库,而 `k8s` 则用于运行服务。文章具体介绍了每项工具的部署步骤,并提供了详细的配置信息和示例代码。此外,还特别指出中间件(如 MySQL、Redis 等)应部署在 K8s 之外,以确保服务稳定性和独立性。通过本文,读者可以学习如何在本地环境中搭建一套完整的自动化部署系统。
75 0
|
26天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
54 12
|
21天前
|
JavaScript 安全 Java
java版药品不良反应智能监测系统源码,采用SpringBoot、Vue、MySQL技术开发
基于B/S架构,采用Java、SpringBoot、Vue、MySQL等技术自主研发的ADR智能监测系统,适用于三甲医院,支持二次开发。该系统能自动监测全院患者药物不良反应,通过移动端和PC端实时反馈,提升用药安全。系统涵盖规则管理、监测报告、系统管理三大模块,确保精准、高效地处理ADR事件。
|
1月前
|
人工智能 监控 数据可视化
Java智慧工地信息管理平台源码 智慧工地信息化解决方案SaaS源码 支持二次开发
智慧工地系统是依托物联网、互联网、AI、可视化建立的大数据管理平台,是一种全新的管理模式,能够实现劳务管理、安全施工、绿色施工的智能化和互联网化。围绕施工现场管理的人、机、料、法、环五大维度,以及施工过程管理的进度、质量、安全三大体系为基础应用,实现全面高效的工程管理需求,满足工地多角色、多视角的有效监管,实现工程建设管理的降本增效,为监管平台提供数据支撑。
43 3