java架构之路(多线程)AQS之ReetrantLock显示锁的使用和底层源码解读

简介:

java架构之路(多线程)AQS之ReetrantLock显示锁的使用和底层源码解读

说完了我们的synchronized,这次我们来说说我们的显示锁ReetrantLock。

上期回顾:

上次博客我们主要说了锁的分类,synchronized的使用,和synchronized隐式锁的膨胀升级过程,从无锁是如何一步步升级到我们的重量级锁的,还有我们的逃逸分析。

锁的粗化和锁的消除

  这个本来应该是在synchronized里面去说的,忘记了,不是很重要,但是需要知道有这么一个东西啦。

我们先来演示一下锁的粗化:

StringBuffer sb = new StringBuffer();

public void lockCoarseningMethod(){

//jvm的优化,锁的粗化
sb.append("1");

sb.append("2");

sb.append("3");

sb.append("4");

}

我们都知道我们的StringBuffer是线程安全的,也就是说我们的StringBuffer是用synchronized修饰过的。那么我们可以得出我们的4次append都应该是套在一个synchronized里面的。

StringBuffer sb = new StringBuffer();

public void lockCoarseningMethod() {

synchronized (Test.class) {
    sb.append("1");
}

synchronized (Test.class) {
    sb.append("2");
}
synchronized (Test.class) {
    sb.append("3");
}
synchronized (Test.class) {
    sb.append("4");
}

}

按照理论来说应该是这样的,其实JVM对synchronized做了优化处理,底层会优化成一次的synchronized修饰,感兴趣的可以用javap -c 自己看一下,这里就不带大家去看了,我以前的博客有javap看汇编指令码的过程。

StringBuffer sb = new StringBuffer();

public void lockCoarseningMethod() {

synchronized (Test.class) {
    sb.append("1");
    sb.append("2");
    sb.append("3");
    sb.append("4");
}

}

再来看一下锁的消除,其实这个锁的消除,真的对于synchronized理解了,锁的消除一眼就知道是什么了。

public static void main(String[] args) {

synchronized (new Object()){
    System.out.println("开始处理逻辑");
}

}
对于synchronized而言,我们每次去锁的都是对象,而你每次都创建的一个新对象,那还锁毛线了,每个线程都可以拿到对象,都可以拿到对象锁啊,所以没不会产生锁的效果了。

概述AQS:

AQS是AbstractQueuedSynchronizer的简称,字面意思,抽象队列同步器。Java并发编程核心在于java.concurrent.util包而juc当中的大多数同步器 实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获 取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定 义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。就是我们上次博客说的什么公平锁,独占锁等等。

AQS具备特性
阻塞等待队列
共享/独占
公平/非公平
可重入
允许中断
AQS的简单原理解读:

ReetrantLock的内部功能还是很强大的,有很多的功能,我们来一点点缕缕。如Lock,Latch,Barrier等,都是基于AQS框架实现,一般通过定义内部类Sync继承AQS将同步器所有调用都映射到Sync对应的方法AQS内部维护属性volatile int state (32位),state表示资源的可用状态

State三种访问方式
getState()
setState()
compareAndSetState()
AQS定义两种资源共享方式
Exclusive-独占,只有一个线程能执行,如ReentrantLock
Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
AQS定义两种队列
同步等待队列
条件等待队列
AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
刚才提到那么多属性,可能会有一些懵,我们来看一下ReentrantLock内部是怎么来实现哪些锁的吧。

打开我们的ReetrantLock源代码可以看到一个关键的属性

private final Sync sync;
后面有一个抽象方法并且继承了AbstractQueuedSynchronizer类,内部有一个用volatile修饰过的整型变量state,他就是用来记录上锁次数的,这样就实现了我们刚才的说的重入锁和非可重入锁。我们来画一个图。

AbstractQueuedSynchronizer这个类里面定义了详细的ReetrantLock的属性,后面我会一点点去说,带着解读一下源码(上面都是摘自源码的)。state和线程exclusiveOwnerThread比较好理解,最后那个队列可能不太好弄,我这里写的也是比较泛化的,后面我会弄一个专题一个个去说。 相面说的CLH队列其实不是很准确,我们可以理解为就是一个泛型为Node的双向链表结构就可以了。

等待队列中Node节点内还有三个很重要的属性就是prev前驱指针指向我们的前一个Node节点,和一个next后继指针来指向我们的下一个Node节点,这样就形成了一个双向链表的结构,于此同时还有一个Thread来记录我们的当前线程。

在条件队列中,prev和next指针都是null的,不管是什么队列,他都有一个waitStatus的属性来记录我们的节点状态的,就是我们刚才说的CANCELLED结束、SIGNAL可唤醒那四个常量值。

AQS中ReetrantLock的使用:

公平锁和非公平锁:这个还是比较好记忆的,举一个栗子,我们去车站排队上车,总有**插队,用蛇形走位可以上车的是吧,这就是一个非公平的锁,如果说,我们在排队的时候加上护栏,每次只能排一个人,他人无法插队的,这时就是一个公平锁。总之就是不加塞的就是公平的,我们都讨厌不公平。

重入锁与非可重入锁:这个也很好理解,重入锁就是当我们的线程A拿到锁以后,可以继续去拿多把锁,然后再陆陆续续的做完任务再去解锁,非可重入呢,就是只能获得一把锁,如果想获取多把锁,不好意思,去后面排下队伍。下面我化了一个重入锁的栗子,快过年了,大家提着行李回老家,我们进去了会一并带着行李进去(不带行李的基本是行李丢了),这就是一个重入锁的栗子,我们人进去了获得通道通过(锁),然后我们也拖着行李获得了通道通过(锁),然后我们才空出通道供后面的人使用。如果是非可重入锁就是人进去就进去吧,行李再次排队,说不准什么时候能进来。

上一段代码来验证一下我们上面说的那些知识点。

import java.util.concurrent.locks.ReentrantLock;

public class Test {

private ReentrantLock lock = new ReentrantLock(true);//true公平锁,false非公平锁
public void lockMethod(String threadName) {
    lock.lock();
    System.out.println(threadName + "得到了一把锁1");

    lock.lock();
    System.out.println(threadName + "得到了一把锁2");

    lock.lock();
    System.out.println(threadName + "得到了一把锁3");

    lock.unlock();
    System.out.println(threadName + "释放了一把锁1");

    lock.unlock();
    System.out.println(threadName + "释放了一把锁2");

    lock.unlock();
    System.out.println(threadName + "释放了一把锁3");
}
public static void main(String[] args) {
    Test test = new Test();

    new Thread(() -> {
        String threadName = Thread.currentThread().getName();
        test.lockMethod(threadName);

    }, "线程A").start();
}

}

通过代码阅读我们知道我们弄一个重入锁,加三次锁,解三次锁,我们来看一下内部sync的变化,调试一下。

我们看到了我们的state变量是用来存储我们的入锁次数的。刚才去看过源码的小伙伴知道了我们的state是通过volatile修饰过的,虽然可以保证我们的有序性和可见性,但是一个int++的操作,他是无法保证原子性的,我们继续来深挖一下代码看看内部是怎么实现高并发场景下保证数据准确的。点击lock方法进去,我们看到lock方法是基于sync来操作的,就是我们上面的画的那个ReetrantLock的图。

/**

  • Sync object for fair locks
    */

static final class FairSync extends Sync {

private static final long serialVersionUID = -3000897897090466540L;

final void lock() {//开始加锁
    acquire(1);
}

/**
 * Fair version of tryAcquire.  Don't grant access unless
 * recursive call or no waiters or is first.
 */
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();//得到当前线程
    int c = getState();//得到上锁次数
    if (c == 0) {//判断是否上过锁
        if (!hasQueuedPredecessors() &&//hasQueuedPredecessors判断是否有正在等待的节点,
                compareAndSetState(0, acquires)) {//通过unsafe去更新上锁次数
            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;
}

}

这次我们开启多个线程来同时访问来看一下我们的Node的变化。同时开启ABCD四个线程来执行这个

这次我们看到了head属性和tail属性不再是空的。head是也是一个node节点,前驱指针是空的,后驱指针指向后继节点,Thread为空,tail的node节点正好是和head相对应的节点。这样的设计就是为了更好的去验证队列中还是否存在剩余的线程节点需要处理。然后该线程运行结束以后会唤醒在队列中的节点,然其它线程继续运行。

我们知道我们创建的公平锁,如果说BCD好好的在排队,E线程来了,只能好好的去排队,因为公平,所以排队,如果我们创建的是非公平锁,E线程就有机会拿到锁,拿到就运行,拿不到就去排队。

原文地址https://www.cnblogs.com/cxiaocai/p/12191666.html

相关文章
|
4月前
|
消息中间件 Java Kafka
Java 事件驱动架构设计实战与 Kafka 生态系统组件实操全流程指南
本指南详解Java事件驱动架构与Kafka生态实操,涵盖环境搭建、事件模型定义、生产者与消费者实现、事件测试及高级特性,助你快速构建高可扩展分布式系统。
272 7
|
4月前
|
消息中间件 Java 数据库
Java 基于 DDD 分层架构实战从基础到精通最新实操全流程指南
本文详解基于Java的领域驱动设计(DDD)分层架构实战,结合Spring Boot 3.x、Spring Data JPA 3.x等最新技术栈,通过电商订单系统案例展示如何构建清晰、可维护的微服务架构。内容涵盖项目结构设计、各层实现细节及关键技术点,助力开发者掌握DDD在复杂业务系统中的应用。
817 0
|
5月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
236 0
|
2月前
|
负载均衡 Java API
grpc-java 架构学习指南
本指南系统解析 grpc-java 架构,涵盖分层设计、核心流程与源码结构,结合实战路径与调试技巧,助你从入门到精通,掌握高性能 RPC 开发精髓。
314 7
|
2月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
310 1
|
3月前
|
存储 小程序 Java
热门小程序源码合集:微信抖音小程序源码支持PHP/Java/uni-app完整项目实践指南
小程序已成为企业获客与开发者创业的重要载体。本文详解PHP、Java、uni-app三大技术栈在电商、工具、服务类小程序中的源码应用,提供从开发到部署的全流程指南,并分享选型避坑与商业化落地策略,助力开发者高效构建稳定可扩展项目。
|
4月前
|
机器学习/深度学习 人工智能 Java
Java 技术支撑下 AI 与 ML 技术融合的架构设计与落地案例分析
摘要: Java与AI/ML技术的融合为智能化应用提供了强大支持。通过选用Deeplearning4j、DJL等框架解决技术适配问题,并结合Spring生态和JVM优化提升性能。在金融风控、智能制造、医疗影像等领域实现了显著效果,如审批效率提升3倍、设备停机减少41%、医疗诊断延迟降低80%。这种技术融合推动了多行业的智能化升级,展现了广阔的应用前景。
339 0
|
5月前
|
缓存 Java 数据库
Java 项目分层架构实操指南及长尾关键词优化方案
本指南详解基于Spring Boot与Spring Cloud的Java微服务分层架构,以用户管理系统为例,涵盖技术选型、核心代码实现、服务治理及部署实践,助力掌握现代化Java企业级开发方案。
252 2
|
6月前
|
算法 架构师 Java
Java 开发岗及 java 架构师百度校招历年经典面试题汇总
以下是百度校招Java岗位面试题精选摘要(150字): Java开发岗重点关注集合类、并发和系统设计。HashMap线程安全可通过Collections.synchronizedMap()或ConcurrentHashMap实现,后者采用分段锁提升并发性能。负载均衡算法包括轮询、加权轮询和最少连接数,一致性哈希可均匀分布请求。Redis持久化有RDB(快照恢复快)和AOF(日志更安全)两种方式。架构师岗涉及JMM内存模型、happens-before原则和无锁数据结构(基于CAS)。
195 5

热门文章

最新文章