• java锁之ReentrantLock及Condition


    前言

    最近在看java的LinkedBlockingQueue数据结构时,发现里面使用了ReentrantLock,为了更好的理解LinkedBlockingQueue的线程安全原理,就不得不搞清楚ReentrantLock的背后原理,本篇文章详细介绍ReentrantLock的加锁、解锁、公平锁、非公平锁的幕后故事。

    1、ReentrantLock

    1.1、ReentrantLock的数据结构

            ReentrantLock中有一个Sync对象sync,Sync继承自AbstractQueuedSynchronizer(AQS)。Sync的具体实现有两个NonfairSync(非公平锁)和FairSync(公平锁),继承关系如下:

    因为都继承自AQS,所以无论是NofairSync还是FairSync两种锁的数据结构是一样的,主要的类成员如下:

    -- head,tail:保存等待获取锁的一个链表队列,head指向链表头,tail指向链表尾

    -- state:状态,如果大于0,说明锁已被使用。

    -- exclusiveOwnerThread:保存占据锁的线程

    1.2 ReentrantLock的初始化

     ReentrantLock有两个构造函数,默认构造函数使用的是非公平锁;如果设置参数为true,则为公平锁,代码如下:

    1. public ReentrantLock() {
    2. sync = new NonfairSync(); //作者注:默认非公平锁
    3. }
    4. public ReentrantLock(boolean fair) {
    5. sync = fair ? new FairSync() : new NonfairSync(); //作者注:通过fair决定使用公平锁还是非公平锁
    6. }

    2、公平锁

            在调用ReentrantLock的lock方法时,实际上最终调用到Sync的lock方法,当锁是公平锁时,调用到FairSync的lock方法,整个获取锁的代码调用过程如下:

    1. //作者注:FairSync的lock方法
    2. final void lock() {
    3. acquire(1);
    4. }
    5. //作者注:AQS的acquire方法
    6. public final void acquire(int arg) {
    7. if (!tryAcquire(arg) &&
    8. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    9. selfInterrupt();
    10. }
    11. //作者注:FairSync的tryAcquire
    12. protected final boolean tryAcquire(int acquires) {
    13. final Thread current = Thread.currentThread();
    14. int c = getState();
    15. if (c == 0) {
    16. if (!hasQueuedPredecessors() &&
    17. compareAndSetState(0, acquires)) {
    18. setExclusiveOwnerThread(current);
    19. return true;
    20. }
    21. }
    22. else if (current == getExclusiveOwnerThread()) {
    23. int nextc = c + acquires;
    24. if (nextc < 0)
    25. throw new Error("Maximum lock count exceeded");
    26. setState(nextc);
    27. return true;
    28. }
    29. return false;
    30. }

    1、调用tryAcquire方法尝试去获取锁:

            -- 如果当前锁没有线程使用(state=0)并且队列里面没有等待的线程(!hasQueuedPredecessors()),则获取锁,将state置为1且exclusiveOwnerThread置为当前线程,获取成功返回true;

            -- 如果锁已被当前线程获取,则将state=state+1,获取成功返回true。(这种叫做重入锁,也就是同一个线程内可以多次获取锁,当然一个线程获取了几次锁,最终也要释放几次),

            -- 获取锁失败,返回false。

    2、如果获取锁失败,则调用addWaiter(Node.EXCLUSIVE), arg)方法,将当前线程放到等待队列里面,等待别的线程释放锁。假如thread-0获取了锁,未释放状态下,后续到来要获取锁的线程都放到链表里面去。具体数据结构如下:

    (这个图是一个大概的情况,里面涉及的一些细节没有展示,比如head指向的其实是一个空的节点,空的节点后面才跟实际的线程节点)

    3、非公平锁

    非公平锁和公平锁的加锁过程的唯一区别如下:

    1. //作者注:NonFairSync的lock方法
    2. final void lock() {
    3. if (compareAndSetState(0, 1)) //作者注:直接尝试获取锁,有可能插队成功。
    4. setExclusiveOwnerThread(Thread.currentThread());
    5. else
    6. acquire(1); //作者注:如果获取失败,也要乖乖的去队列排队去。
    7. }

    由2中知,公平锁在获取锁之前,先去判断有没有其他线程占用锁(state是否等于0)以及队列里面是否有等待的线程,如果有,则将当前线程加入到等待队列。

    但是非公平锁不判断等待队列里是否有等待的线程,而是直接尝试去重新设置锁的状态(compareAndSetState),如果设置成功,说明锁已被释放,该线程直接占有锁。所以我的理解就是:

    公平锁:所有线程都要先判断队列里是否有等待的线程,如果有,要乖乖的到队列里去排队。

    非公平锁:不判断队列里是否有等待的线程,直接尝试获取锁(也就是插队),可能比队列里的线程优先获得锁。

    当然,如果当前线程没有获取到锁,最终还是要乖乖的去队列里排队去(acquire(1)后续的执行过程和公平锁是一模一样的)。

    4、锁的释放

    锁的释放入口函数如下:

    1. //作者注:AbstractQueuedSynchronizer里的方法
    2. public final boolean release(int arg) {
    3. if (tryRelease(arg)) {
    4. Node h = head;
    5. if (h != null && h.waitStatus != 0)
    6. unparkSuccessor(h);
    7. return true;
    8. }
    9. return false;
    10. }
    11. //作者注:Sync里的方法
    12. protected final boolean tryRelease(int releases) {
    13. int c = getState() - releases;
    14. if (Thread.currentThread() != getExclusiveOwnerThread())
    15. throw new IllegalMonitorStateException();
    16. boolean free = false;
    17. if (c == 0) {
    18. free = true;
    19. setExclusiveOwnerThread(null);
    20. }
    21. setState(c);
    22. return free;
    23. }

    1、tryRealease(arg)中将state置为0,且将exclusiveOwnerThread置为null,

    2、具体队列里的线程重新获取锁是在什么地方调用的,我没找到。(不是unparkSuccessor这个方法)

    5、Condition

    Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。

    在实际使用中,Condition配合Sync进行使用,为了了解Condition和Sync的关系,我通过以下方式建立了两个Contition,运行之后进行debug,看看这两个Condition里面都有什么。

    1. ReentrantLock lock = new ReentrantLock();
    2. Condition cond1 = lock.newCondition();
    3. Condition cond2 = lock.newCondition();

    debug看一下对象里面的信息:

    可以看到cond1和cond2共用一个Sync。所以通过上述建立Condition的方式,知道生成的数据结构如下:

    等待线程和被阻塞线程的区别是:

    等待线程:有权利获取锁。

    被阻塞线程:没有权利获取锁,需要被唤醒后加入到AQS等待队列才有权利获取锁。

    5.1、await和signal:手动阻塞线程和唤起线程

    调用condition的await方法,旨在说明要对当前线程进行堵塞,并释放当前线程持有的锁,将当前线程添加到condition的阻塞队列中,调用代码如下:

    1. public final void await() throws InterruptedException {
    2. if (Thread.interrupted())
    3. throw new InterruptedException();
    4. //作者注:将当前线程添加到Condition的阻塞线程队里的末尾
    5. Node node = addConditionWaiter();
    6. //作者注:释放当前线程持有的锁
    7. int savedState = fullyRelease(node);
    8. int interruptMode = 0;
    9. while (!isOnSyncQueue(node)) {
    10. //作者注:将当前线程挂起
    11. LockSupport.park(this);
    12. if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
    13. break;
    14. }
    15. //作者注:如果当前线程被唤起,尝试去获取锁
    16. if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
    17. interruptMode = REINTERRUPT;
    18. if (node.nextWaiter != null) // clean up if cancelled
    19. unlinkCancelledWaiters();
    20. if (interruptMode != 0)
    21. reportInterruptAfterWait(interruptMode);
    22. }

    为了更直观的看到执行过程,我们使用简单的代码进行debug测试,写一个如下简单的代码:

    1. public class ReentrantLockTest {
    2. public static final ReentrantLock lock = new ReentrantLock();
    3. public static final Condition condition = lock.newCondition();
    4. public static class Td1 extends Thread {
    5. ReentrantLock lock ;
    6. Condition condition ;
    7. public Td1(ReentrantLock lock,Condition condition){
    8. this.lock = lock;
    9. this.condition = condition;
    10. }
    11. @Override
    12. public void run() {
    13. try {
    14. lock.lock(); // 代码1
    15. condition.await(); //代码2
    16. System.out.println("thread-0");
    17. lock.unlock(); //代码3
    18. } catch (InterruptedException e) {
    19. e.printStackTrace();
    20. }
    21. }
    22. }
    23. public static class Td2 extends Thread {
    24. ReentrantLock lock ;
    25. Condition condition ;
    26. public Td2(ReentrantLock lock,Condition condition){
    27. this.lock = lock;
    28. this.condition = condition;
    29. }
    30. @Override
    31. public void run() {
    32. try {
    33. Thread.sleep(5000); //为了保证先运行线程1,后运行线程2,在这里暂停5秒
    34. lock.lock(); //代码4
    35. condition.signal(); //代码5
    36. System.out.println("thread-1");
    37. lock.unlock(); //代码6
    38. } catch (InterruptedException e) {
    39. e.printStackTrace();
    40. }
    41. }
    42. }
    43. public static void main(String[] args) {
    44. Td1 td1 = new Td1(lock,condition);
    45. Td2 td2 = new Td2(lock,condition);
    46. td1.start();
    47. td2.start();
    48. }
    49. }

    1、运行到代码1:可以看到lock的debug数据如下,锁被Thread-0占据(exclusiveOwnerThread=thread-0,state=1):

     2、运行到代码2:因为此时线程被挂起,为了看到具体数据,我们debug到await代码内部的int interruptMode = 0这一行,可以看到debug数据如下:thread-0被添加到condition的队列中,而锁被释放(exclusiveOwnerThread=null,state=0)

     3、运行到代码4:此时thread-0还在condition队列,而锁被thread-1占据

    4、运行到代码5:唤起condition,将thread-0从condition队列清除,添加到lock的等待队列;但因为此时thread-1还没有释放锁,锁还是被thread-1持有。

     5、运行到代码6:此时thread-1释放锁,因为运行速度较快,没有看到锁空闲时段,就已经被thread-0捕获到:

     6、此时thread-0已经被唤起,代码运行到代码3处,thread-0释放锁整个代码运行结束。

    以上就是ReentrantLock和Condition的配合使用整个流程,从debug过程中,可以清晰的看到整个内部数据结构和锁持有的变化情况。

    6、再次说明

    本篇文章从整体的锁的持有变化及ReentrantLock和Condition内部数据结构进行了框架性的说明,其实内部的锁实现机制有很多细节,在这里并没有展示,因为作者的目的是让大家看清楚锁是如何被获取和释放的。如果想了解具体的细节机制,可以参考下面这个作者写的,非常详细,但是需要仔细的品味,想过一遍就看懂是不太可能的。

    ReentrantLock锁内部详细实现机制

  • 相关阅读:
    冲刺十五届蓝桥杯P0001阶乘求和
    法律战爆发:“币安退出俄罗斯引发冲击波“
    Spring Cloud Alibaba
    python 学习笔记
    Win10系统磁盘问题----- 分区、c盘空间清理、扩展卷、恢复分区解决办法合集
    Twitter 审核研究联盟 - 深入了解 Twitter 上对话的安全性和完整性。
    openGauss3.1.0 版本的gs_stack功能解密
    请教下Elasticsearch7.14向量检索
    雅思词汇真经单词共3663个
    【计算机毕业设计】35.流浪动物救助及领养管理系统源码
  • 原文地址:https://blog.csdn.net/chenzhiang1/article/details/126739613