独占锁ReentrantLock原理解析

news/2024/11/8 0:05:42/

文章目录

    • 1 基本介绍
      • 1.1 为什么要使用ReentrantLock
      • 1.2 ReentrantLock的常用方法
    • 2 源码梳理
      • 2.1 构造方法
      • 2.2 加锁
      • 2.3 释放锁
    • 参考

1 基本介绍

ReentrantLock是可重入独占锁,同时只能由一个线程锁持有,如果其他线程想要获取锁,就会被阻塞并放入该锁的AQS同步队列中。

1.1 为什么要使用ReentrantLock

在多线程编程中,如果要多资源进行加锁,最常用的方法是使用synchronized关键字,然而synchronized关键字缺有诸多不足之处,而ReentrantLock相对而言可以解决这些不足,如:

  • synchronized无法知道线程有没有成功获取到锁,而ReentrantLock可以。
  • synchronized的 wait方法只能有一个条件变量,而ReentrantLock可以配合Condition的await方法实现多个条件变量,更为灵活。

1.2 ReentrantLock的常用方法

下面就是介绍一下ReentrantLock的常用方法:

方法名作用
lock()获取锁,成功后变返回,如果失败了,就会被阻塞
lockInterruptibly()与lock()方法作用一样,不同的是该方法可以对响应,如果其他线程调用当前线程的interrupt() 方法,则当前线程会抛出InterruptedException异常并返回
tryLock()在不阻塞当前线程的情况下,尝试获取锁,成功则返回true,失败则返回false
tryLock(long timeout, TimeUnit unit)如果在指定的时间内未能成功获取锁,则返回false,成功则返回true
unlock()状态值减去1,如果减1后状态值为0则释放锁
newCondition()获取一个条件变量

2 源码梳理

这里还是一样的用的 Jdk11,和网上一些以 Jdk8的资料有一些差异,但是差异不大。

2.1 构造方法

public ReentrantLock() {sync = new NonfairSync();}
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}

由构造方法可知,ReentrantLock默认为非公平锁,因为这样获取锁时省略了一些检查操作,速度快一些。如果想要使用公平锁,可以在创建ReentrantLock时传入true即可。

2.2 加锁

方法入口如下:

public void lock() {sync.acquire(1);
}

sync是Sync对象,Sync继承了AbstractQueuedSynchronizer,所以sync.acquire(1)实际的调用了AbstractQueuedSynchronizer的acquire方法,下面进入acquire方法。

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

acquire方法中做了2个判断,第一个判断是尝试获取锁,如果失败了则进入第二个判断,将当前线程封装为类型为Node.EXCLUSIVE的Node节点,插入AQS同步队列的尾部,并不停的循环获取锁。
另外,AQS并不实现tryAcquire方法,而是由子类实现,所以我们进入FairSync的tryAcquire方法:

protected final boolean tryAcquire(int acquires) {// 具体实现方法是 nonfairTryAcquire。return nonfairTryAcquire(acquires);
}// 以非公平的方式获取锁
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {// 获取当前线程final Thread current = Thread.currentThread();// 获取 AQS状态值int c = getState();// 如果 AQS状态值为 0,则说明当前锁未被占用,则可以获取锁。if (c == 0) {// 这里主要是执行的 AQS的操作,以 CAS的方式将 AQS状态值设置为 1。// 非公平锁和公平锁的差异主要是体现在这里。if (compareAndSetState(0, acquires)) {// 如果成功将 AQS状态值,则将 AQS中锁的持有者设置为当前线程。然后返回。setExclusiveOwnerThread(current);return true;}}// 如果锁已经被占用了,那么就先判断当前线程是否是该锁的持有者。else if (current == getExclusiveOwnerThread()) {// 如果是,则将 AQS的状态值加 1int nextc = c + acquires;// int类型是有上限的,这里主要是防止可重入次数溢出。if (nextc < 0)throw new Error("Maximum lock count exceeded");// 重新设置 AQS状态值并返回。setState(nextc);return true;}return false;
}

以上是非公平锁的实现方式,另外试想一下,如果当前锁被线程A所持有,AQS同步队列中有一个线程B正在等待锁,正常的情况是A释放锁,然后B通过CAS的方式正常获取锁,但是如果B获取锁的时,恰好有一个线程C也通过CAS的方式获取锁,并且成功获取锁了,那么B将又被方法 AQS同步队列,继续挂起。这样对于B而y言是不公平的,因为它等待的时间很久,反而被C“插队”了。
接下来我们看一下公平锁是如何实现的:

// 以公平的方式获取锁
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {Thread current = Thread.currentThread();int c = this.getState();if (c == 0) {// 差异主要在这里,多了一个 hasQueuedPredecessors 判断。if (!this.hasQueuedPredecessors() && this.compareAndSetState(0, acquires)) {this.setExclusiveOwnerThread(current);return true;}} else if (current == this.getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) {throw new Error("Maximum lock count exceeded");}this.setState(nextc);return true;}return false;
}

由此我们可以看出,公平锁与非公平锁的差异是在于公平锁多了一个 !this.hasQueuedPredecessors()判断,我们可以进入该判断看一下:

public final boolean hasQueuedPredecessors() {Node h, s;// 如过 AQS同步队列的前驱节点(head)不是空if ((h = head) != null) {// 根据前驱节点(head)获取下一个节点,也就是 AQS同步队列中的第一个节点,然后判断其是否为空。// 如果 h.next不是空,说明 AQS同步队列中已经有线程等待获取锁中。那么就接着判断该节点的线程是否调用了条件变量的 await方法将自己挂起。if ((s = h.next) == null || s.waitStatus > 0) {s = null;// 遍历 AQS同步队列,直到找到一个未调用条件变量的 await方法将自己挂起的线程。for (Node p = tail; p != h && p != null; p = p.prev) {if (p.waitStatus <= 0)s = p;}}// 如果 AQS同步队列中存在一个未调用过 await的线程,并且这个线程并非是当前线程,则返回true。if (s != null && s.thread != Thread.currentThread())return true;}return false;
}

由此方法便可以看出,如果 AQS同步队列中存在可以正常获取锁的线程,那么新来的线程就无法“插队”。
接下来我们便进入如果调用tryAcquire方法失败,而进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)的方法。进入addWaiter方法:

private Node addWaiter(Node mode) {//创建一个类型为 Node.EXCLUSIVE的Node节点Node node = new Node(mode);// 将其加入 AQS同步队列的尾部。for (;;) {Node oldTail = tail;if (oldTail != null) {node.setPrevRelaxed(oldTail);if (compareAndSetTail(oldTail, node)) {oldTail.next = node;return node;}} else {// 初始化同步队列,在这个方法中,是创建一个Node节点为哨兵节点,并设置为 AQS前驱节点(head)。initializeSyncQueue();}}
}

接下来我们便进入acquireQueued方法:

final boolean acquireQueued(final Node node, int arg) {boolean interrupted = false;try {// 循环的方式获取锁for (;;) {// 获取当前线程的前置节点final Node p = node.predecessor();// 如果当前线程的前一个(pre)节点就是 AQS的前驱节点(head),那么说明“轮到”这个线程去获取锁了// 然后调用 tryAcquire方法获取锁if (p == head && tryAcquire(arg)) {// 将 AQS的前驱节点(head)设置为当前节点,可以看出这是懒加载的形式。setHead(node);p.next = null;return interrupted;}if (shouldParkAfterFailedAcquire(p, node))interrupted |= parkAndCheckInterrupt();}} catch (Throwable t) {cancelAcquire(node);if (interrupted)selfInterrupt();throw t;}
}

由此,线程便成功获取锁了。

2.3 释放锁

释放锁就相对而言毕竟容易了,下面我们看一下代码:

public void unlock() {sync.release(1);
}public final boolean release(int arg) {// 具体逻辑由 AQS子类实现if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;
}@ReservedStackAccess
protected final boolean tryRelease(int releases) {// 将 AQS状态值减去 1(默认为1)int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())// 如果当期线程不是锁的拥有者,就会报错,所以在未获得锁之前,切勿调用锁的 unlock方法throw new IllegalMonitorStateException();boolean free = false;// 如果状态值减去1后为0,则可以释放锁。if (c == 0) {free = true;// 将当前锁的拥有者设置为空setExclusiveOwnerThread(null);}// 设置 AQS状态值setState(c);return free;
}

这里需要注意的是,因为是可重入锁,同一线程每获取锁一次状态值就会加1,所以解锁也应该解同样的次数,否则仅仅是状态值减1,不会释放锁。

参考

  1. 电子工业出版社,翟陆续,薛宾田著,《Java并发编程之美》。
  2. RedSpider社区,《深入浅出Java多线程》。

http://www.ppmy.cn/news/768913.html

相关文章

Java线程池:基本介绍、源码梳理、注意点

文章目录 1 基本介绍1.1 为什么使用线程池1.2 一个简单的例子1.3 线程池实现类&#xff1a;以ThreadPoolExecutor为例1.3.1 ThreadPoolExecutor的构造方法1.3.2 常用的阻塞队列&#xff1a;1.3.3 常用的拒绝策略&#xff1a;1.3.2 ThreadPoolExecutor的工作模型 1.4 线程池的种…

RabbitMQ学习(二):客户端开发

文章目录 1 直接上代码吧1.1 maven1.2 配置类1.3 生产者测试代码1.4 生产者运行结果1.5 消费者测试代码1.6 消费者运行结果 参考 1 直接上代码吧 本篇文章主要展示一个客户端开发Demo。 1.1 maven maven中需要的配置是直接从官网拷贝的。 <dependency><groupId>…

Redis的基本概念

文章目录 1 为什么使用Redis2 为什么Redis这么快3 Redis命令参考4 Redis的数据结构4.1 string&#xff08;字符串&#xff09;4.2 list&#xff08;列表&#xff09;4.3 hash&#xff08;字典&#xff09;4.4 set&#xff08;集合&#xff09;4.5 zset&#xff08;有序列表&…

RabbitMQ学习(一):基本概念

文章目录 1 为什么使用RabbitMQ2 为什么RabbitMQ这么快3 AMQP介绍3.1 AMQP的核心概念3.2 AMQP分层&#xff1a; 4 RabbitMQ的整体结构4.1 结构图4.2 不同的交换机类型 5 相关命令5.1 服务相关命令5.2 用户相关命令5.3 虚拟主机相关命令5.4 队列相关命令5.5 集群相关命令 参考 1…

Redis如何备份与恢复数据

文章目录 1 数据持久化1.1 快照1.2 AOF1.3 混合持久化1.4 从节点持久化 1 数据持久化 Redis有自己的持久化机制&#xff0c;以防宕机后内存中的数据丢失。当宕机后&#xff0c;便从磁盘恢复内存数据结构。 1.1 快照 Redis是使用COW机制实现快照持久化。 第一步&#xff1a;…

RabbitMQ学习(三):高级特性

文章目录 1 生产端如何可靠地投递消息1.1 消息落库打标1.2 消息延迟投递&#xff0c;通过二次确认回调检查。 2 生产者确认&#xff08;Confirm消息确认机制&#xff09;3 消息的幂等性保障3.1 唯一ID 指纹码 机制3.2 利用Redis的原子性实现 4 Return消息机制5 消息的分发与消…

Spring事务的一些总结

文章目录 1 Transactional 注解特性2 事务的属性3 事务的嵌套4 事务超时设置5 Transaction注解不回滚的可能原因6 事务的基本要素(ACID)7 事务的原理8 数据并发问题9 数据库隔离级别10 Spring隔离级别11 底层所使用的不同的持久化API或框架 1 Transactional 注解特性 可以在整…

计算机科学与技术万金油专业,盘点工学大类里的“万金油”专业

原标题&#xff1a;盘点工学大类里的“万金油”专业 教育部2012年最新修订的本科生专业名录中把专业重新进行了从学科门类到专业类和专业的划分&#xff0c;共计12大学科门类&#xff0c;分别是理、工、农、医 &#xff0c;管、教、文、艺 经、史、法、哲.92大专业类&#xff0…