千家信息网

Java并发中的ReentrantLock和AQS的源码分析

发表于:2026-11-03 作者:千家信息网编辑
千家信息网最后更新 2026年11月03日,这篇文章主要讲解了"Java并发中的ReentrantLock和AQS的源码分析",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"Java并发中的Reen
千家信息网最后更新 2026年11月03日Java并发中的ReentrantLock和AQS的源码分析

这篇文章主要讲解了"Java并发中的ReentrantLock和AQS的源码分析",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"Java并发中的ReentrantLock和AQS的源码分析"吧!

一.前言

首先在聊ReentrantLock之前,我们需要知道整个JUC的并发同步的基石,currrent里面所有的共享变量都是由volatile修饰的,我们知道volatile的语义有2大特点,可见性以及防止重排序(内存屏障,volatie写与volatile读)
1、当第二个操作为volatile写操做时,不管第一个操作是什么(普通读写或者volatile读写),都不能进行重排序。这个规则确保volatile写之前的所有操作都不会被重排序到volatile之后;

2、当第一个操作为volatile读操作时,不管第二个操作是什么,都不能进行重排序。这个规则确保volatile读之后的所有操作都不会被重排序到volatile之前;

3、当第一个操作是volatile写操作时,第二个操作是volatile读操作,不能进行重排序。
而cas操作同时包含了volatile写/读语义,这二者的完美结合就组成了current的基石

二.ReentrantLock的基础用法

1.

public class ReentrantLockText {                        public static void main(String[] args) {                Lock lock = new ReentrantLock();                                                Thread t1 = new Thread(()->{                        try {                                lock.lock();                                System.out.println("t1 start");                                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);                                System.out.println("t1 end");                        } catch (InterruptedException e) {                                System.out.println("interrupted!");                        } finally {                                lock.unlock();                        }                });                t1.start();                                Thread t2 = new Thread(()->{                        try {                                //lock.lock();                                lock.lockInterruptibly(); //可以对interrupt()方法做出响应                                System.out.println("t2 start");                                TimeUnit.SECONDS.sleep(5);                                System.out.println("t2 end");                        } catch (InterruptedException e) {                                System.out.println("interrupted!");                        } finally {                                lock.unlock();                        }                });                t2.start();                                try {                        TimeUnit.SECONDS.sleep(1);                } catch (InterruptedException e) {                        e.printStackTrace();                }                t2.interrupt(); //打断线程2的等待                        }}

运行结果

reentrantlock用于替代synchronized

  • 需要注意的是,必须要必须要必须要手动释放锁(重要的事情说三遍)

  • 使用syn锁定的话如果遇到异常,jvm会自动释放锁,但是lock必须手动释放锁,因此经常在finally中进行锁的释放

  • 使用reentrantlock可以进行"尝试锁定"tryLock,这样无法锁定,或者在指定时间内无法锁定,线程可以决定是否继续等待

  • 使用ReentrantLock还可以调用lockInterruptibly方法,可以对线程interrupt方法做出响应

  • 在一个线程等待锁的过程中,可以被打断

2.ReentrantLock还有一个tryLock(time),可以指定时间,如果指定时间内没有获得锁,则放弃,可以通过其返回值来决定是否继续等待

3.还有就是Condition了(我个人觉得这是最灵活的一个地方)

public class Lock_condition {    public static void main(String[] args) {        char[] aI = "1234567".toCharArray();        char[] aC = "ABCDEFG".toCharArray();        Lock lock = new ReentrantLock();        Condition conditionT1 = lock.newCondition();        Condition conditionT2 = lock.newCondition();        new Thread(()->{            try {                lock.lock();                for(char c : aI) {                    System.out.print(c);                    conditionT2.signal();                    conditionT1.await();                }                conditionT2.signal();            } catch (Exception e) {                e.printStackTrace();            } finally {                lock.unlock();            }        }, "t1").start();        new Thread(()->{            try {                lock.lock();                for(char c : aC) {                    System.out.print(c);                    conditionT1.signal();                    conditionT2.await();                }                conditionT1.signal();            } catch (Exception e) {                e.printStackTrace();            } finally {                lock.unlock();            }        }, "t2").start();    }}

这是condition结合lock(独占锁)的用法
condition目前只实现了独占锁,关于condition的源码理解,后续也会继续更新,暂时我们只需要知道类似object的wait与notifiy

三.原理+源码

我们现在知道了基本用法,那么我们就可以开始探究源码了

1.AQS
我们知道JUC里面的核心类就是AQS,那么AQS究竟是个啥东西呢?
1)先上内部类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 PROPAGATE = -3;        volatile int waitStatus;        volatile Node next;        volatile Node prev;        volatile Thread thread;        Node nextWaiter;

不知道大家在看到这个 Node next;Node prev;的时候是啥感觉,反正我当时是激动坏了,这不就是一个双向链表嘛,
再看volatile Thread thread;这个属性,这是一个管理线程的双向链表,换句话说就是将线程打包成立节点放入AQS的链表中
基础的结构清楚之后。
SHARED 与EXCLUSIVE 代表是独占节点还是共享节点
2)再上AQS属性源码

private transient volatile Node head;    private transient volatile Node tail;    private volatile int state;

Node0 head与tail不用说,这是来管理节点的,
这里我们要核心介绍一个属性是state,这也是AQS这个类的灵魂,
1.再独占锁中这个state是1或者0,(如果大于1则表示锁重入,这个稍后会有源码分析)
2在共享锁中代表还有多少共享锁资源,
3.在读写锁中,高16位代表写锁是否被占用,低16位代表有多少读锁,
4.在CountDownLatch中,通过构造参数代码门闸剩余个数
5.在Semaphore中,同样通过构造参数代表信号灯个数

2.ReentrantLock获取锁源码(独占锁)
首先公平锁与非公平锁,分别继承与Sync
FairSync NoFairSync ,默认是非公平锁,可以在构造方法上指定

ReentrantLock lock = new ReentrantLock(true);//ture则是公平锁

公平锁故名思意,在AQS中管理着一个线程队列,如果这时候 有一个线程过来抢这把锁,如果是公平锁,那么会判断队列是不是存在不同与当前线程的等待队列(FIFO),如果存在则去排队,非公平锁则是直接去排队
(2.1.非公平锁获取锁)

final void lock() {            if (compareAndSetState(0, 1))//cas原子操作尝试去修改值,如果修改成功说明成功获取到了锁                setExclusiveOwnerThread(Thread.currentThread());            else                acquire(1);        }

首先cas原子操作尝试去修改值,如果修改成功说明成功获取到了锁,进入setExclusiveOwnerThread()方法

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

将当前线程记录,实现偏向锁,一行代码便完美实现了偏向锁!!

如果失败则,调用acquire();这个方法调用的实际上是子类的nonfairTryAcquire方法

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,如果为0,则说明锁没有被占有,(可能是刚刚被释放)那么cas操作开始尝试获取锁,
**(注意注意注意)**重要的事情说三遍,这里仅仅尝试获取一次,没有自旋!!这是独占锁与共享锁的区别之一,因为如果state>=0(对于共享锁来说state代表剩余数量),那么共享锁会不断尝试自旋获取锁,之道state<0,因为只要》0那么就可能共享到锁
接下来的else if就是重入锁的操作了,判断当先线程是不是记录的线程,如果是,每次重入state+1,如果不是就返回flase,直接拜拜
(2.2公平锁获取锁)
刚刚介绍了公平锁的意义,所有直接上源码,公平锁比非公平锁多了一个公平判断

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;        return h != t &&            ((s = h.next) == null || s.thread != Thread.currentThread());    }

我们可以看到hasQueuedPredecessors是用来判断队列是否有不同于当前线程的节点等待,这里重点讨论一个情况,
h != t返回true,(s = h.next) == null返回true
首先可以知道队列中有2个节点,但是头节点没有后继结点,在这里列举一种情况,有另一个线程已经执行到初始化队列的操作了,介于compareAndSetHead(new Node())与tail = head之间,也就是之后说的enq自旋方法,请继续往下看

继续如果非公平锁没有获取到锁,那么会调用acquireQueued和addwaiter方法

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

首先来看addwaiter方法

private Node addWaiter(Node mode) {        Node node = new Node(Thread.currentThread(), mode);        // Try the fast path of enq; backup to full enq on failure        Node pred = tail;        if (pred != null) {            node.prev = pred;            if (compareAndSetTail(pred, node)) {                pred.next = node;                return node;            }        }        enq(node);        return node;    }

首先将,当前线程封装成一个,Node,然后通过判断尾结点是不是空的方式,判断队列是不是空的,如果存在尾结点,那么直接进先驱后继的改造,放入双向链表,完成链表结构,那么如果是空的呢?这么调用了一个enq的自旋方法

private Node enq(final Node node) {        for (;;) {            Node t = tail;            if (t == null) { // Must initialize                if (compareAndSetHead(new Node()))                    tail = head;            } else {                node.prev = t;                if (compareAndSetTail(t, node)) {                    t.next = node;                    return t;                }            }        }    }

我们可以看到这个一个自旋方法,第一次循环:t为null,那么cas就new出来一个新结点,头尾都指向这个新结点,
第二次循环,将传进来的这个线程结点的前驱指向刚刚new的这个结点,然后cas操作进行,将这个线程结点替换为尾部结点,然后head后继指向线程结点,返回head
经过二次循环,得到了一个由2个节点组成的队列,head-》node,head是假节点(里面不包括线程为null),node才是真正的线程结点(addwrite封装好的传进来的线程结点)
问题1.为什么一定要用cas操作,因为防止别的线程修改了该队列

好,现在我们继续,看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)) {                    setHead(node);                    p.next = null; // help GC                    failed = false;                    return interrupted;                }                if (shouldParkAfterFailedAcquire(p, node) &&                    parkAndCheckInterrupt())                    interrupted = true;            }        } finally {            if (failed)                cancelAcquire(node);        }    }

这是一个最最核心的方法,从队列中取线程
首先这是一个自旋,判断该节点的前驱节点是不是head,因为(FIFO)先进先出队列。
如果不是直接拜拜进入shouldParkAfterFailedAcquire,继续上源码

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {        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 > 0) {            /*             * Predecessor was cancelled. Skip over predecessors and             * indicate retry.             */            do {                node.prev = pred = pred.prev;            } while (pred.waitStatus > 0);            pred.next = node;        } else {            /*             * waitStatus must be 0 or PROPAGATE.  Indicate that we             * need a signal, but don't park yet.  Caller will need to             * retry to make sure it cannot acquire before parking.             */            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);        }        return false;    }

static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;

  1. CANCELLED:因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会被踢出队列,被GC回收;

  2. SIGNAL:表示这个结点的继任结点被阻塞了,到时需要通知它;

  3. CONDITION:表示这个结点在条件队列中,因为等待某个条件而被阻塞;

  4. PROPAGATE:使用在共享模式头结点有可能牌处于这种状态,表示锁的下一次获取可以无条件传播;

  5. 0:None of the above,新结点会处于这种状态。

首先说明一下waitStatus这个属性,为什么之前不提呢,因为之前没有对waitStatus进行操作,我们在new节点与封装结点的时候没有考虑这个属性,所以我们现在当成一个新属性,值为0来看待,

shouldParkAfterFailedAcquire这个代码的逻辑意义是说明呢?
如果是SIGNAL那么,直接ture,
如果大于0,则是CANCELLED,被取消了,直接剔除队列
如果都不是,那么将其前驱结点设为SIGNAL,也就是可以安心睡觉了,定好闹钟了,可以被等着唤醒了,
我们现在很明显是第三种,因为之前啥都没干,就是0,
所以本来状态

进行shouldParkAfterFailedAcquire之后,
那么现在链表中,对线程1的前驱设闹钟,0变成-1

假设这时候又来了个线程2,那么同理,对线程而的先驱设闹钟0变成-1

在调用了shouldParkAfterFailedAcquire()之后,调用parkAndCheckInterrupt方法用于阻塞,

这里提一下,关于parkAndCheckInterrupt,lock里面用于阻塞都是基于lockSupper.park()与lockSupper.unpark()完成了,而lockSupper调用的又是unsafe这个类,我们知道java是基于jvm实现的,并不能和c++一样直接对os进行操作,所以jvm给我们提供了一个梯子,这个梯子就是unsafe这个类,直接将线程的的交个操作系统阻塞

四,释放锁

终于到了释放锁,独占锁的释放锁的逻辑相对与共享锁来说比较简单,后续我也会继续更新共享锁的源码

public final boolean release(int arg) {        if (tryRelease(arg)) {            Node h = head;            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) {                free = true;                setExclusiveOwnerThread(null);            }            setState(c);            return free;        }

首先获取,int c = getState() - releases;这里可能c是>0的,因为独占锁的重入锁(上面以及说明了独占锁的重入的源码操作),所以有可能是需要进行多次解锁的,继续
判断当前线程是不是独占线程,如果不是则报IllegalMonitorStateException异常
一直解锁到c=0的时候,那么线程已经解锁,则设setExclusiveOwnerThread=null
设置当前独占线程为null,然后设置state为0

继续回到release,如果头节点不是null而且h.waitStatus != 0,说明是-1,说明设置闹钟了,需要唤醒aqs队列中的阻塞结点,调用的是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;        if (ws < 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 == 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);    }

这里首先明确,这里传进来的node是啥?是头节点!!!!,一定要明确这个,博主就是因为刚开始没明确这个,半天没明白,因为唤醒一定是唤醒头节点之后的waitStatus不为1的结点

首先判断头节点,如果是-1则设置为0,这个中间状态,表示有结点被唤醒了,
然后拿到,head的后继节点,进行判断,如何是null或者waitStatus >0,(就是1,CANCELLED,代表被取消了),这个时候从尾部开始遍历,剔除waitStatus >0的节点,找到第一个waitStatus <=0的节点,用LockSupport.unpark(s.thread);将其唤醒,唤醒之后的线程回到acquireQueued方法中,

final boolean acquireQueued(final Node node, int arg) {        boolean failed = true;        try {            boolean interrupted = false;            for (;;) {            //被阻塞的线程,被唤醒后在进行循环,            //然后通过return interrupted;                final Node p = node.predecessor();                if (p == head && tryAcquire(arg)) {                    setHead(node);                    p.next = null; // help GC                    failed = false;                    return interrupted;                }                if (shouldParkAfterFailedAcquire(p, node) &&                    parkAndCheckInterrupt())                    interrupted = true;            }        } finally {            if (failed)                cancelAcquire(node);        }    }

感谢各位的阅读,以上就是"Java并发中的ReentrantLock和AQS的源码分析"的内容了,经过本文的学习后,相信大家对Java并发中的ReentrantLock和AQS的源码分析这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!

线程 结点 节点 方法 源码 队列 就是 状态 代表 这是 阻塞 属性 尝试 分析 成功 前驱 时候 闹钟 后继 循环 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 云南咖啡 北京路客互联网科技有限公司 科技赋能下的互联网金融 互联网 科技创业 互联网未来科技 以房互联网科技 阿里巴巴算科技企业还是互联网 互联网科技实习生岗位职责 港股科技和互联网 湖南一路畅行互联网科技 和信科技工业互联网 中国互联网十大科技股排名 互联网金融科技展 河南住房之家互联网科技 烟台灵鸽互联网科技有限公司 每天都有哪些好的互联网科技 伍佰万互联网科技怎么赚钱 深圳互联网科技的发展 广东工业互联网大赛深圳学泰科技 广州燕处互联网科技有限公司 互联网科技工作委员会毛军 互联网科技有限公司经营范围 奇士达互联网科技 互联网科技美容 海南购全球互联网科技 赶超互联网科技 微信多开 花神(南京)互联网科技有限公司 哈尔滨屹滨互联网科技有限公司 互联网科技和法律行业 数字科技和互联网金融哪个好
0