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源码分析

相关文章
|
1月前
|
Java Apache Maven
Java百项管理之新闻管理系统 熟悉java语法——大学生作业 有源码!!!可运行!!!
文章提供了使用Apache POI库在Java中创建和读取Excel文件的详细代码示例,包括写入数据到Excel和从Excel读取数据的方法。
56 6
Java百项管理之新闻管理系统 熟悉java语法——大学生作业 有源码!!!可运行!!!
|
2月前
|
数据采集 运维 前端开发
【Java】全套云HIS源码包含EMR、LIS (医院信息化建设)
系统技术特点:采用前后端分离架构,前端由Angular、JavaScript开发;后端使用Java语言开发。
70 5
|
3月前
|
Kubernetes jenkins 持续交付
从代码到k8s部署应有尽有系列-java源码之String详解
本文详细介绍了一个基于 `gitlab + jenkins + harbor + k8s` 的自动化部署环境搭建流程。其中,`gitlab` 用于代码托管和 CI,`jenkins` 负责 CD 发布,`harbor` 作为镜像仓库,而 `k8s` 则用于运行服务。文章具体介绍了每项工具的部署步骤,并提供了详细的配置信息和示例代码。此外,还特别指出中间件(如 MySQL、Redis 等)应部署在 K8s 之外,以确保服务稳定性和独立性。通过本文,读者可以学习如何在本地环境中搭建一套完整的自动化部署系统。
69 0
|
3月前
|
存储 Oracle 安全
揭秘Java并发核心:深入Hotspot源码腹地,彻底剖析Synchronized关键字的锁机制与实现奥秘!
【8月更文挑战第4天】在Java并发世界里,`Synchronized`如同导航明灯,确保多线程环境下的代码安全执行。它通过修饰方法或代码块实现独占访问。在Hotspot JVM中,`Synchronized`依靠对象监视器(Object Monitor)机制实现,利用对象头的Mark Word管理锁状态。
47 1
|
3天前
|
运维 自然语言处理 供应链
Java云HIS医院管理系统源码 病案管理、医保业务、门诊、住院、电子病历编辑器
通过门诊的申请,或者直接住院登记,通过”护士工作站“分配患者,完成后,进入医生患者列表,医生对应开具”长期医嘱“和”临时医嘱“,并在电子病历中,记录病情。病人出院时,停止长期医嘱,开具出院医嘱。进入出院审核,审核医嘱与住院通过后,病人结清缴费,完成出院。
18 3
|
9天前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
12天前
|
移动开发 前端开发 JavaScript
java家政系统成品源码的关键特点和技术应用
家政系统成品源码是已开发完成的家政服务管理软件,支持用户注册、登录、管理个人资料,家政人员信息管理,服务项目分类,订单与预约管理,支付集成,评价与反馈,地图定位等功能。适用于各种规模的家政服务公司,采用uniapp、SpringBoot、MySQL等技术栈,确保高效管理和优质用户体验。
|
1月前
|
JSON 前端开发 Java
震惊!图文并茂——Java后端如何响应不同格式的数据给前端(带源码)
文章介绍了Java后端如何使用Spring Boot框架响应不同格式的数据给前端,包括返回静态页面、数据、HTML代码片段、JSON对象、设置状态码和响应的Header。
119 1
震惊!图文并茂——Java后端如何响应不同格式的数据给前端(带源码)
|
2月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
365 37
|
1月前
|
存储 前端开发 Java
Java后端如何进行文件上传和下载 —— 本地版(文末配绝对能用的源码,超详细,超好用,一看就懂,博主在线解答) 文件如何预览和下载?(超简单教程)
本文详细介绍了在Java后端进行文件上传和下载的实现方法,包括文件上传保存到本地的完整流程、文件下载的代码实现,以及如何处理文件预览、下载大小限制和运行失败的问题,并提供了完整的代码示例。
273 1