多线程和并发编程(3)—AQS和ReentrantLock实现的互斥锁

简介: 多线程和并发编程(3)—AQS和ReentrantLock实现的互斥锁

一、管程模型—MESA模型

管程是什么?

管程就是指管理共享变量,以及对共享变量的相关操作。

在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在正在广泛使用的是MESA模型。

MESA模型的核心是需要一个共享变量来表示共享资源的数量,同步等待队列中的线程请求到一个共享资源,相应共享变量要减一,一直到共享变量为0,则请求的线程阻塞在同步等待队列中,如果需要满足某些条件才能竞争共享资源,这些线程会阻塞在条件等待队列中,但条件满足后要么转移到同步等待队列中,要么直接占有共享资源。

MESA 管程模型

同步等待队列:竞争共享资源暂没竞争到的线程会阻塞在同步等待队列中。

条件等待队列:在竞争资源的过程中还未达到某个条件,会阻塞在条件等待队列中,其中需要达到的条件即用条件变量表示,当满足这个条件后就会转移到同步等待队列中。

二、AQS原理

在Java中针对管程有两种实现:(1)一种是基于Object的Monitor机制,用于synchronized内置锁的实现;(2)一种是抽象队列同步器AQS,用于JUC包下Lock锁机制的实现;以下重点介绍方案(2)中的AQS。

1.实现思路

对于被请求的共享变量如果是空闲的,则将请求共享资源的线程设置为工作线程并且将共享变量减一。对于被请求资源是被占用的情况,则将该线程阻塞起来放到双向同步等待队列中,等共享资源被释放再进行申请。

2.AQS的组成

  • 共享变量

AQS内部维护属性volatile int state ,其中state表示资源的可用状态,State三种访问方式:

  1. getState()
  2. setState()
  3. compareAndSetState()

AQS实现时主要实现以下几种方法:

  1. isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  2. tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  3. tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  4. tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  5. tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
  • 同步队列

AQS的数据结构包括同步等待队列和条件等待队列,同步等待队列是一个双端队列CLH,每个队列元素为一个Node,Node中保存前驱后置节点、当前线程状态、当前线程以及下一个等待线程。每个Node通过获取和重置state参数来进行加锁操作。共享资源是否被占用是通过对state进行修改来实现的,当该共享资源被加锁后,就会修改state为1。条件等待队列是保存不满足条件的线程,每一个条件对于一个条件同步队列。

img

同步阻塞队列和条件阻塞队列在一定条件会相互转换,当条件阻塞队列满足条件的情况,就能转移到同步阻塞队列中。即同步阻塞队列中的线程只差竞争到锁,而条件阻塞队列中的线程还需要满足条件才能转移到同步阻塞队列。在Java中通过signal()或signalAll()将条件同步队列中的线程转移到同步等待队列。但不满足条件时,调用await()方法会将同步等待队列中线程转移到条件等待队列中。

  • 编程模型

常见的使用AQS的编程模式如下:

public class BlockedQueue<T>{
   
   
  final Lock lock = new ReentrantLock();
  final Condition condition1 = lock.newCondition();
  final Condition condition2 = lock.newCondition();
  //Boolean flag = true;

  // 入队
  void enq(T x) {
   
   
    lock.lock();
    try {
   
   
      while (!条件1){
   
   
        // 条件1不满足,进入1条件等待队列
        condition1.await();
      }  
      // ...
      //解锁前,唤醒条件2等待队列中线程
      condition2.signal();
    }finally {
   
   
      lock.unlock();
    }
  }
  // 出队
  void deq(){
   
   
    lock.lock();
    try {
   
   
      while (!条件2){
   
   
        // 条件2不满足,进入条件2等待队列
        condition2.await();
      }
      // ...
       //解锁前,唤醒条件1等待队列中线程
      condition1.signal();
    }finally {
   
   
      lock.unlock();
    }  
  }
}
  1. 申请一把Lock锁,即申请一个同步等待队列,每个线程进入业务处理前需要竞争锁资源,没有竞争上锁资源的线程放到同步等待队列中;
  2. 在获取锁之后,会进行条件判断,如果满足条件则会在持有该锁的条件下进行业务逻辑的处理,其他线程无法并发处理,如果没有满足条件则会进入到条件同步队列中,并且暂时释放锁,等待满足条件后再参与锁竞争;

三、ReentrantLock原理

ReentrantLock是实现AQS的悲观锁。初始化状态是state为0,当调用lock()方法时候会调用tryAcquire方法将其state设为1并且锁定,之后其他线程调用tryAcquire方法将会失败并加入到同步队列阻塞,直到该线程调用unlock()方法会将此锁释放,调用tryRelease方法释放锁,将state改为0,同时注意,该线程在释放该锁之前,可以重复获得此锁,所以ReentrantLock是可重入的。

ReentrantLock内部有三个内部类,包括抽象类Sync,和其实现类NonfairSync、FairSync,可以分别实现公平锁和非公平锁,对ReentrantLock的操作基本是对Sync的操作,Sync分为公平实现FairSync和非公平实现NonfairSync,他们都是继承AQS接口。

image-20230914205718458

ReentrantLock和synchronized的区别?

  • 相同点:

(1)synchronied和ReentrantLock都是可重入锁;

(2)synchronied是Java的关键字,是通过JVM对对象进行加Monitor锁操作实现的;而ReentrantLock通过JDK中的AQS接口来实现,提供多种加锁、解锁方法;

  • 不同点:

(1)特性:synchronied是非公平锁,ReentrantLock可以实现公平锁和非公平锁;ReentrantLock可以实现可中断、可重试、超时中断、多条件加锁机制;ReentrantLock可以实现等待多条件释放锁,总之ReentrantLock更加灵活、功能更强大;

(2)实现:Synchronized是JVM自动隐式加解锁,执行完成他会自动释放锁;ReentrantLock通过Lock()和unLock()实现手动加锁和取消加锁。

参考资料

  1. 管程(Moniter): 并发编程的基本心法:https://developer.aliyun.com/article/904581
  2. Java中的管程模型:https://segmentfault.com/a/1190000021557492
目录
相关文章
|
26天前
|
安全 Java 编译器
线程安全问题和锁
本文详细介绍了线程的状态及其转换,包括新建、就绪、等待、超时等待、阻塞和终止状态,并通过示例说明了各状态的特点。接着,文章深入探讨了线程安全问题,分析了多线程环境下变量修改引发的数据异常,并通过使用 `synchronized` 关键字和 `volatile` 解决内存可见性问题。最后,文章讲解了锁的概念,包括同步代码块、同步方法以及 `Lock` 接口,并讨论了死锁现象及其产生的原因与解决方案。
56 10
线程安全问题和锁
|
6天前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
21 2
|
21天前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
10天前
|
负载均衡 Java 调度
探索Python的并发编程:线程与进程的比较与应用
本文旨在深入探讨Python中的并发编程,重点比较线程与进程的异同、适用场景及实现方法。通过分析GIL对线程并发的影响,以及进程间通信的成本,我们将揭示何时选择线程或进程更为合理。同时,文章将提供实用的代码示例,帮助读者更好地理解并运用这些概念,以提升多任务处理的效率和性能。
|
25天前
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。
|
10天前
|
存储 算法 Java
关于python3的一些理解(装饰器、垃圾回收、进程线程协程、全局解释器锁等)
该文章深入探讨了Python3中的多个重要概念,包括装饰器的工作原理、垃圾回收机制、进程与线程的区别及全局解释器锁(GIL)的影响等,并提供了详细的解释与示例代码。
15 0
|
13天前
|
并行计算 API 调度
探索Python中的并发编程:线程与进程的对比分析
【9月更文挑战第21天】本文深入探讨了Python中并发编程的核心概念,通过直观的代码示例和清晰的逻辑推理,引导读者理解线程与进程在解决并发问题时的不同应用场景。我们将从基础理论出发,逐步过渡到实际案例分析,旨在揭示Python并发模型的内在机制,并比较它们在执行效率、资源占用和适用场景方面的差异。文章不仅适合初学者构建并发编程的基础认识,同时也为有经验的开发者提供深度思考的视角。
|
1月前
|
安全 Java API
Java线程池原理与锁机制分析
综上所述,Java线程池和锁机制是并发编程中极其重要的两个部分。线程池主要用于管理线程的生命周期和执行并发任务,而锁机制则用于保障线程安全和防止数据的并发错误。它们深入地结合在一起,成为Java高效并发编程实践中的关键要素。
18 0
|
2月前
|
Java 数据库连接 微服务
揭秘微服务架构下的数据魔方:Hibernate如何玩转分布式持久化,实现秒级响应的秘密武器?
【8月更文挑战第31天】微服务架构通过将系统拆分成独立服务,提升了可维护性和扩展性,但也带来了数据一致性和事务管理等挑战。Hibernate 作为强大的 ORM 工具,在微服务中发挥关键作用,通过二级缓存和分布式事务支持,简化了对象关系映射,并提供了有效的持久化策略。其二级缓存机制减少数据库访问,提升性能;支持 JTA 保证跨服务事务一致性;乐观锁机制解决并发数据冲突。合理配置 Hibernate 可助力构建高效稳定的分布式系统。
48 0
|
2月前
|
程序员 调度 C++
解锁Ruby并发编程新境界!Fiber与线程:轻量级VS重量级,你选哪一派引领未来?
【8月更文挑战第31天】Ruby提供了多种并发编程方案,其中Fiber与线程是关键机制。Fiber是自1.9版起引入的轻量级并发模型,无需独立堆栈和上下文切换,由程序员控制调度。线程则为操作系统级别,具备独立堆栈和上下文,能利用多核处理器并行执行。通过示例代码展示了Fiber和线程的应用场景,如任务调度和多URL数据下载,帮助开发者根据需求选择合适的并发模型,提升程序性能与响应速度。
30 0