ReentrantLock 基本原理

导读:本篇文章讲解 ReentrantLock 基本原理,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一. 概述

ReentrantLock底层基于AQS,其构造方法返回的就是NonfaireSync和faireSync;
两种同步器都继承自Sync,Sync又继承自AQS!在这里插入图片描述

在这里插入图片描述
new ReentrantLock时,返回的就是同步器,默认是非公平的;

二. 原理分析

非公平锁、可重入锁、可中断锁、条件变量锁 都是基于底层 state属性(volatile修饰)和阻塞队列来配合实现的;

1. 非公平锁的加锁

1. 1 加锁成功

调用lock() 方法,使用CAS机制尝试将AQS底层 volatile修饰的 state 属性改为1,并将当前线程设为exclusiveOwnerThread即占有锁;
若CAS修改失败即出现竞争,则进入acquire() 方法;
在这里插入图片描述

1. 2 加锁失败

  1. 当锁已经被占用,共享内存中的 state属性是1,compareAndSet(0,1) 会失败,CAS更改state属性失败,会进入 acquire() 方法;
  2. 进入else中 AQS的 acquire() 方法;
  3. 调用 tryAcquire() 再次尝试,如果还失败,再执行acquireQueued() ,如果是第一次会创造一个哨兵节点,然后再创造一个Node节点对象关联线程,添加到FIFO等待队列(双向链表)中;
  4. 节点进入队列后,会循环尝试加锁,失败后会被park阻塞,节点前驱节点的 waitStatus 会被置为 -1,-1表示可以去唤醒后继节点;

AQS中的acquire()方法:
在这里插入图片描述
假设多个线程多竞争失败,进入了FIFO等待队列:
在这里插入图片描述

1. 非公平锁的释放

1.1 unlock() 释放锁+唤醒线程

  1. 调用 unlock() 方法,底层调用了release() 方法,使用tryRelease()state置为0,并将exclusiveOwnerThread置为null,即释放锁;
  2. tryRelease() 返回true后,判断head哨兵节点是否为null ,如果否 且哨兵节点的waitStatus 也不会0(为-1),则哨兵节点的后继节点被unParkSuccessor 唤醒 !线程就恢复允许了,就有机会去竞争锁;原来的节点就从队列中断开;

在这里插入图片描述

在这里插入图片描述

1.1 竞争失败

如果此时又来了一个新的线程,那么就会和队列中要被释放的线程一起竞争锁,
如果新的线程获取到了锁成为了exclusiveOwnerThread,则队列中的线程解锁失败,
在for中循环tryAcquire返回false,又被阻塞住;
在这里插入图片描述

2. 可重入锁

2.1 加锁(state自增)

加锁最终调用nonFairTryAcquire( 1 ), 先获取state值,
①如果state=0即无锁,并使用CAS将state改为1,并将当前线程设为exclusiveOwnerThread,即占有锁;
②如果state=1,则判断当前线程是否等于exclusiveOwnerThread,如果是即锁重入,就让state值++;
在这里插入图片描述

2.2 解锁(state自减)

调用tryRelease(1)方法,使state减去参数中的1,
如果减1之后state为0,则将exclusiveOwnerThread设为null,即释放锁,并返回true;
如果state减1之后不为0,则返回fasle;
在这里插入图片描述

3. 可中断机制(跳出阻塞)

【不中断的模式】下,即使它被打断,会仍然驻留在AQS的等待队列中被阻塞;
【可打断模式】下,如果没有获得锁,进入 doAcquireInterruptibly() 方法而不是acquireQueued(),在线程被park阻塞的状态时,如果被interrupt则会抛出异常,线程就不会在AQS中的等待队列继续等待;
在这里插入图片描述
在这里插入图片描述

4. 公平锁(加锁时 看排队位置)

非公平锁:
加锁时,最终调用nonFairAcquire(),如果state=0,则直接用CAS机制将state改为1,设置Owner线程,不去检查AQS的等待队列;

公平锁:
1.最终调用 tryAcqure() 方法,会用hasQueuePredecessors()方法检查当前线程在AQS等待队列中的位置;
2.hasQueuePredecessors即 检查当前这个线程在AQS队列中的位置是否处于head.next(最优先的位置,head是哨兵节点不关联线程),
如果线程是 就使用CAS机制将state设为1,并设置当前线程为exclusiveOwnerThread,
不是就不会往下执行,不会用当前线程占有锁;

在这里插入图片描述

在这里插入图片描述

5. 条件变量

ConditionObject条件变量的等待队列 类似Moniter的waitset队列 !
每个条件变量其实就对应着一个等待队列(双向链表),实现类是AQS中的ConditionObject

5.1 await() 加入条件变量队列+fullRelease

前提:起初state=1,线程-0是Owner线程占用着锁,调用await()

  1. 进入addConditionWaiter()方法,会创建节点关联当前线程-0,并将节点加入到 ConditionObject条件变量的队列 中去,然后将节点的waitStatus状态设为 -2 ,并且执行 park() 阻塞该线程;
    (-2在条件变量里是等待的状态);
    (如果队列为空,就将节点作为firstNode,如果不为空就加到队列的尾部);
  2. 执行fullyRelease():然后将当前这个线程占用的锁都释放掉,因为可能有锁重入;
  3. 释放锁都会在同步器的阻塞队列中head的后继节点唤醒,即unparkSuccesser(),后面的线程就能去竞争锁;

在这里插入图片描述
在这里插入图片描述

5.2 singal()

只有Owner线程才有资格唤醒条件变量中的线程

假设Thread-1 唤醒Thread-0:

  1. 判断当前线程是不是锁的持有者,如果不是则报错;
  2. 获取ConditionObject队列的头元素firstWaiter,如果不为空,就调用doSignale()将其移除等待队列,
    然后再将这个节点转移到同步器的阻塞队列中并将waitStatues改为0(因为阻塞队列最后一个节点的waitStatues是0), 这样线程就有机会抢占锁了;
    如果遇到中断等,就取消转移,将等待队列的下一个节点来唤醒;
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

synchronized和ReentrantLock的区别 ?

ReentrantLock底层基于AQS实现,即使用volatile修饰的state属性和阻塞队列来实现线程的串行执行,从而达到线程安全性的目的;

ReentrantLock是可重入的互斥锁,虽然具有与synchronized相同功能,但是会比synchronized更加灵活(有更多的方法),能解决synchronized关键字在一些并发场景下不适用的问题;

  1. synchronized 依赖于 JVM, 而 ReentrantLock 依赖于 juc的API;

  2. ReentrantLock可以指定是公平锁还是非公平锁。⽽synchronized只能是非公平锁;

  3. synchronized只能等待同步代码块执行结束,不可以中断,而reentrantlock可以调用线程的interrupt方法来中断等待;

  4. 可以支持多个变量:类似于调用wait方法时,不满足条件的线程进入waitset队列等待CPU随机调度,支持多个变量表示支持多个类似自定义waitset,这样就可以指定对象来唤醒了。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/89218.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!