浅谈JAVA并发之ReentrantLock
作者:ukyu 发布时间:2022-10-22 10:03:33
1. 介绍
结合上面的ReentrantLock类图,ReentrantLock实现了Lock接口,它的内部类Sync继承自AQS,绝大部分使用AQS的子类需要自定义的方法存在Sync中。而ReentrantLock有公平与非公平的区别,即'是否先阻塞就先获取资源',它的主要实现就是FairSync与NonfairSync,后面会从源码角度看看它们的区别。
2. 源码剖析
Sync是ReentrantLock控制同步的基础。它的子类分为了公平与非公平。使用AQS的state代表获取锁的数量
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
...
}
我们可以看出内部类Sync是一个抽象类,继承它的子类(FairSync与NonfairSync)需要实现抽象方法lock。
下面我们先从非公平锁的角度来看看获取资源与释放资源的原理
故事就从就两个变量开始:
// 获取一个非公平的独占锁
/**
* public ReentrantLock() {
* sync = new ReentrantLock.NonfairSync();
* }
*/
private Lock lock = new ReentrantLock();
// 获取条件变量
private Condition condition = lock.newCondition();
2.1 上锁(获取资源)
lock.lock()
public void lock() {
sync.lock();
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// 获取资源
final void lock() {
// 若此时没有线程获取到资源,直接设置当前线程独占访问资源。
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// AQS的方法
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
// 实现在父类Sync中
return nonfairTryAcquire(acquires);
}
}
AQS的acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// Sync实现的非公平的tryAcquire
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;
}
尝试获取资源的过程是非常简单的,这里再贴一下acquire的流程
2.2 释放资源
lock.unlock();
public void unlock() {
// AQS的方法
sync.release(1);
}
AQS的release
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release的流程已经剖析过了,接下来看看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;
}
tryRelease的实现也很简单,这里再贴一下release的流程图
2.3 公平锁与非公平锁的区别
公平锁与非公平锁,即'是否先阻塞就先获取资源', ReentrantLock中公平与否的控制就在tryAcquire中。下面我们看看,公平锁的tryAcquire
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// (2.3.1)
// sync queue中是否存在前驱结点
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
区别在代码(2.3.1)
hasQueuedPredecessors
判断当前线程的前面有无其他线程排队;若当前线程在队列头部或者队列为空返回false
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());
}
结合下面的入队代码(enq), 我们分析hasQueuedPredecessors为true的情况:
1.h != t ,表示此时queue不为空; (s = h.next) == null, 表示另一个结点已经运行了下面的步骤(2),还没来得及运行步骤(3)。简言之,就是B线程想要获取锁的同时,A线程获取锁失败刚好在入队(B入队的同时,之前占有的资源的线程,刚好释放资源)
2.h != t 且 (s = h.next) != null,表示此时至少有一个结点在sync queue中;s.thread != Thread.currentThread(),这个情况比较复杂,设想一下有这三个结点 A -> B C, A此时获取到资源,而B此时因为获取资源失败正在sync queue阻塞,C还没有获取资源(还没有执行tryAcquire)。
时刻一:A释放资源成功后(执行tryRelease成功),B此时还没有成功获取资源(C执行s = h.next时,B还在sync queue中且是老二)
时刻二: C此时执行hasQueuedPredecessors,s.thread != Thread.currentThread()成立,此时s.thread表示的是B
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node())) // (1) 第一次初始化
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { // (2) 设置queue的tail
t.next = node; // (3)
return t;
}
}
}
}
Note that 1. because cancellations due to interrupts and timeouts may occur at any time, a true return does not guarantee that some other thread will acquire before the current thread(虚假true). 2. Likewise, it is possible for another thread to win a race to enqueue after this method has returned false, due to the queue being empty(虚假false).
这位大佬对hasQueuedPredecessors进行详细的分析,他文中解释了虚假true以及虚假false。我这里简单解释一下:
1.虚假true, 当两个线程都执行tryAcquire,都执行到hasQueuedPredecessors,都返回true,但是只有一个线程执行compareAndSetState(0, acquires)成功
2.虚假false,当一个线程A执行doAcquireInterruptibly,发生了中断,还没有清除掉该结点时;此时,线程B执行hasQueuedPredecessors时,返回true
来源:https://www.cnblogs.com/ukyu/p/14802757.html
猜你喜欢
- 在网上拷贝的所有阿里云镜像比如:<mirror> <id>nexus-aliyun</id>
- Spring Framework 提供了一套可以方便地对 Controller 层中接收的参数进行校验的框架,其中就包括了 @Validat
- 本教程基于 JetBrains IntelliJ IDEA 2018.3.6 编写,高版本未经测试,或有不兼容,请见谅!JetBrains
- 一、Filter(过滤器)Filter接口定义在javax.servlet包中,是Servlet规范定义的,作用于Request/Respo
- c#创建圆形类,其中包括set,get方法using System;using System.Collections.Generic;usi
- 概述Kryo 是一个快速序列化/反序列化工具,依赖于字节码生成机制(底层使用了 ASM 库),因此在序列化速度上有一定的优势,但正因如此,其
- 本文使用SpringBoot结合Redis进行简单的token鉴权。1.简介刚刚换了公司,所以最近有些忙碌,所以一直没有什么产出,最近朋友问
- package com.huateng.readcsv;import java.io.BufferedReader;import java.
- C#控制台程序使用Log4net日志组件,供大家参考,具体内容如下1、Log4net一般都不陌生,但是在配置上不同类型的项目又不相同的地方比
- 无聊逛论坛,发现了这张图真是厉害啊,这排序, 既有多线程,又有排序,还有lambda表达式,但是这是C#版本,作为一个入坑的Java爱好者,
- 高并发下restTemplate的错误分析1. 问题现象和分析org.apache.http.conn.ConnectionPoolTime
- 今天学习了Map集合的几种方法,尤其是遍历Map集合感觉尤为重要,所以发出来供大家学习和自己复习以用。众所周知Map集合里存储元素是以键值对
- 在WPF中,TreeView默认情况是不支持右键选定的,也就是说,当右键点击某节点时,是无法选中该节点的。当我们想在TreeViewItem
- 本文实例讲述了C#线程同步的三类情景,分享给大家供大家参考。具体分析如下:C# 已经提供了我们几种非常好用的类库如 BackgroundWo
- 1.多数据源配置类整体项目结构1).pom.xml 项目依赖<?xml version="1.0" encodin
- 利用TCP传输数据,编写客户端和服务器的程序,实现两个程序间的实时通信。在每个程序中实现了实时的发送与接收数据的功能。客户端的io界面服务器
- 浅谈java内存模型 不同的平台,内存模型是不一样的,但是jvm的
- settings.xml有什么用?如果在Eclipse中使用过Maven插件,想必会有这个经验:配置settings.xml文件的路径。se
- Web服务器是运行及发布Web应用的容器,只有将开发的Web项目放置到该容器中,才能使网络中的所有用户通过浏览器进行访问。开发Java We
- 目录int和Integer的区别及自动装箱和自动拆箱Integer和int的对比,如下所示:自动装箱和自动拆箱:Integer的自动拆装箱的