Java 并发编程面试题——Condition 接口

news2024/11/23 15:01:05

目录

  • 1.Condition 接口有什么作用?
  • 2.如何使用 Condition?
  • 3.Condition 中有哪些常用的方法?
  • 4.✨Condition 的底层实现原理是什么?
    • 4.1.等待队列
    • 4.2.等待
    • 4.3.通知

(1)参考书籍:
《Java 并发编程的艺术》
(2)相关知识点:
Java 并发编程面试题——Lock 与 AbstractQueuedSynchronizer (AQS)
Java 并发编程面试题——重入锁 ReentrantLock

1.Condition 接口有什么作用?

(1)Condition 接口是 Java 并发包中的一部分,用于在支持锁的基础上实现更高级的线程同步和协作。它提供了比内置监视器锁更精细的线程通信和条件等待的机制

(2)Condition 是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个 Lock 对象中可以创建多个 Condition 实例(即对象监视器),线程对象可以注册在指定的 Condition 中,从而可以有选择性的进行线程通知,在调度线程上更加灵活

(3)在使用 notify()/notifyAll() 方法进行通知时,被通知的线程是由 JVM 选择的,用 ReentrantLock 类结合 Condition 实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而 synchronized 关键字就相当于整个 Lock 对象中只有一个 Condition 实例,所有的线程都注册在它一个身上。如果执行 notifyAll() 方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题。而 Condition 实例的 signalAll() 方法,只会唤醒注册在该 Condition 实例中的所有等待线程。

(4)ConditionObject 类实现了 Condition 接口,同时它是 AQS 的内部类(具体关系如下图所示),因为 Condition 的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个 Condition 对象都包含着一个队列(以下称为等待队列),该队列是 Condition 对象实现等待/通知功能的关键

在这里插入图片描述

2.如何使用 Condition?

(1)Condition 定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到 Condition 对象关联的锁。Condition 对象是由 Lock 对象(调用 Lock 对象的 newCondition() 方法)创建出来的。Condition 的使用方式比较简单,需要注意在调用方法前获取锁,使用方式如代码如下所示:

Lock lock = new ReentrantLock();
//创建 Condition 对象
Condition condition = lock.newCondition();

public void conditionWait() throws InterruptedException {
    lock.lock();
    try {
        condition.await();
    } finally {
        lock.unlock();
    }
}

public void conditionSignal() throws InterruptedException {
    lock.lock();
    try {
        condition.signal();
    } finally {
        lock.unlock();
    }
}

(2)如示例所示,一般都会将 Condition 对象作为成员变量。当调用 await() 方法后,当前线程会释放锁并在此等待,而其他线程调用 Condition 对象的 signal() 方法,通知当前线程后,当前线程才从 await() 方法返回,并且在返回前已经获取了锁。

(3)下面给出一个更加具体的例子:

@Slf4j(topic = "c.ConditionDemo")
public class ConditionDemo {
    static ReentrantLock lock = new ReentrantLock();
    static Condition waitCigaretteQueue = lock.newCondition();
    static Condition waitbreakfastQueue = lock.newCondition();
    static volatile boolean hasCigrette = false;
    static volatile boolean hasBreakfast = false;
    
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            try {
                lock.lock();
                while (!hasCigrette) {
                    try {
                        waitCigaretteQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("等到了它的烟");
            } finally {
                lock.unlock();
            }
        }).start();
    
        new Thread(() -> {
            try {
                lock.lock();
                while (!hasBreakfast) {
                    try {
                        waitbreakfastQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("等到了它的早餐");
            } finally {
                lock.unlock();
            }
        }).start();
    
        TimeUnit.SECONDS.sleep(1);
        sendBreakfast();
        TimeUnit.SECONDS.sleep(1);
        sendCigarette();
    }
    
    private static void sendCigarette() {
        lock.lock();
        try {
            log.debug("送烟来了");
            hasCigrette = true;
            waitCigaretteQueue.signal();
        } finally {
            lock.unlock();
        }
    }
    
    private static void sendBreakfast() {
        lock.lock();
        try {
            log.debug("送早餐来了");
            hasBreakfast = true;
            waitbreakfastQueue.signal();
        } finally {
            lock.unlock();
        }
    }
}

输出结果如下所示:

21:08:39 [main] c.Test23 - 送早餐来了
21:08:39 [Thread-1] c.Test23 - 等到了它的早餐
21:08:40 [main] c.Test23 - 送烟来了
21:08:40 [Thread-0] c.Test23 - 等到了它的烟

上面的代码是一个简单的示例,模拟了一个等待送烟和早餐并通知的场景。通过使用 ReentrantLock 和 Condition 来实现线程之间的等待和通知。具体来说,代码中创建了一个 ReentrantLock 对象 lock,并通过 lock.newCondition() 方法创建了两个 Condition 对象 waitCigaretteQueuewaitBreakfastQueue

在主线程中启动了两个线程,一个线程等待烟的到来,另一个线程等待早餐的到来。在每个线程中,首先通过 lock.lock() 获取锁,然后使用 while 循环来判断是否满足等待条件(hasCigarette 和 hasBreakfast)。如果不满足条件,则通过调用相应的 wait 方法(waitCigaretteQueue.await() 和 waitBreakfastQueue.await())来挂起线程,并释放锁。一旦满足条件,线程会继续往下执行。

在主线程中,通过调用 sendCigarette() 和 sendBreakfast() 方法来发送烟和早餐。在每个方法中,首先获取锁(lock.lock()),然后修改相应的状态标志位(hasCigarette 和 hasBreakfast),最后通过调用 signal() 方法来通知等待队列中的线程被唤醒。

整个过程中,使用了 ReentrantLock 来提供互斥访问临界区的能力,而使用 Condition 来实现线程之间的等待和通知。Condition 的 wait 方法实际上是将当前线程挂起,并将其放入等待队列,而 signal 方法则是对等待队列中的线程进行唤醒。

通过这种方式,实现了线程间的同步和通信,使得等待烟和早餐的线程能够在满足条件时得到及时通知并继续执行。

3.Condition 中有哪些常用的方法?

在这里插入图片描述

4.✨Condition 的底层实现原理是什么?

4.1.等待队列

(1)等待队列是一个 FIFO 的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在 Condition 对象上等待的线程,如果一个线程调用了 Condition.await() 方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类 AbstractQueuedSynchronizer.Node

(2)一个 Condition 包含一个等待队列,Condition 拥有首节点 (firstWaiter) 和尾节点 (lastWaiter)。当前线程调用 Condition.await() 方法,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列的基本结构如下图所示:

在这里插入图片描述

如图所示,Condition 拥有首尾节点的引用,而新增节点只需要将原有的尾节点 nextWaiter 指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用 CAS 保证,原因在于调用 await() 方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

(3)在 Object 的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的 Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列,其对应关系如下图所示:

在这里插入图片描述

如图所示,Condition 的实现是同步器的内部类,因此每个 Condition 实例都能够访问同步器提供的方法,相当于每个 Condition 都拥有所属同步器的引用。

4.2.等待

(1)调用 Condition 的 await() 方法(或者以 await 开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从 await() 方法返回时,当前线程一定获取了 Condition 相关联的锁。

(2)如果从队列(同步队列和等待队列)的角度看 await() 方法,当调用 await() 方法时,相当于同步队列的首节点(获取了锁的节点)移动到 Condition 的等待队列中。ConditionObjectawait() 方法的代码如下所示:

public abstract class AbstractQueuedSynchronizer 
	extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
	
	//...

	public class ConditionObject implements Condition, java.io.Serializable {

		//...
		
	    public final void await() throws InterruptedException {
	        if (Thread.interrupted())
	            throw new InterruptedException();
			//当前线程加入等待队列
	        Node node = addConditionWaiter();
			//释放同步状态,也就是释放锁
	        int savedState = fullyRelease(node);
	        int interruptMode = 0;
	        while (!isOnSyncQueue(node)) {
	            LockSupport.park(this);
	            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
	                break;
	        }
	        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
	            interruptMode = REINTERRUPT;
	        if (node.nextWaiter != null)
	            unlinkCancelledWaiters();
	        if (interruptMode != 0)
	            reportInterruptAfterWait(interruptMode);
	    }
	}	    
}

(3)调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。

(4)当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用 Condition.signal() 方法唤醒,而是对等待线程进行中断,则会抛出 InterruptedException。如果从队列的角度去看,当前线程加入 Condition 的等待队列,该过程如下图所示:

在这里插入图片描述
如图所示,同步队列的首节点并不会直接加入等待队列,而是通过 addConditionWaiter() 方法把当前线程构造成一个新的节点并将其加入等待队列中。

4.3.通知

(1)调用 Condition 的 signal() 方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。ConditionObject 的 signal() 方法的代码如下所示:

public abstract class AbstractQueuedSynchronizer 
	extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
	
	//...

	public class ConditionObject implements Condition, java.io.Serializable {

		//...
		
	    public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }
	}	    
}

调用该方法的前置条件是当前线程必须获取了锁,可以看到 signal() 方法进行了 isHeldExclusively() 检查,也就是当前线程必须是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用 LockSupport 唤醒节点中的线程。节点从等待队列移动到同步队列的过程如下图所示:

在这里插入图片描述

(3)通过调用同步器的 enq(Node node) 方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用 LockSupport 唤醒该节点的线程。被唤醒后的线程,将从 await() 方法中的 while 循环中退出(isOnSyncQueue(Node node) 方法返回 true,节点已经在同步队列中),进而调用同步器的 acquireQueued() 方法加入到获取同步状态的竞争中。

(4)成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的 await() 方法返回,此时该线程已经成功地获取了锁。Condition的 signalAll() 方法,相当于对等待队列中的每个节点均执行一次 signal() 方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1191773.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学者观察 | 联邦学习与区块链、大模型等新技术的融合与挑战-北京航空航天大学童咏昕

导语 当下,数据已成为经济社会发展中不可或缺的生产要素,正在发挥越来越大的价值。但是在数据使用过程中,由于隐私、合规或者无法完全信任合作方等原因,数据的拥有者并不希望彻底和他方共享数据。为解决原始数据自主可控与数据跨…

Mysql 和 Redis 数据如何保持一致

先阐明一下Mysql和Redis的关系:Mysql是数据库,用来持久化数据,一定程度上保证数据的可靠性;Redis是用来当缓存,用来提升数据访问的性能。 关于如何保证Mysql和Redis中的数据一致(即缓存一致性问题&#xf…

企业实施MES管理系统会增加哪些工作量

随着制造业的快速发展,越来越多的企业开始关注如何通过技术手段提高生产效率和质量。MES管理系统作为支撑企业生产管理的关键系统,受到很多企业的青睐。然而,对于是否部署MES管理系统,很多企业存在顾虑,担心其会增加工…

Python 3D建模指南【numpy-stl | pymesh | pytorch3d | solidpython | pyvista】

想象一下,我们需要用 python 编程语言构建某个对象的三维模型,然后将其可视化,或者准备一个文件以便在 3D 打印机上打印。 有几个库可以解决这些问题。 让我们看一下如何在 python 中从点、边和图元构建 3D 模型。 如何执行基本 3D 建模技术&…

overflow: auto滚动条跳到指定位置

点击对应模块跳转页面,滚动到对应模块,露出到可视范围 代码: scrollToCurrentCard() {// treeWrapper是包裹多个el-tree组件的父级元素,也是设置overflow:auto的元素let treeWrapper document.getElementsByClassName(treeWrapp…

Kafka JNDI 注入分析(CVE-2023-25194)

Apache Kafka Clients Jndi Injection 漏洞描述 Apache Kafka 是一个分布式数据流处理平台,可以实时发布、订阅、存储和处理数据流。Kafka Connect 是一种用于在 kafka 和其他系统之间可扩展、可靠的流式传输数据的工具。攻击者可以利用基于 SASL JAAS 配置和 SAS…

赞不绝口!飞凌嵌入式全新子品牌ElfBoard好评如潮

飞凌嵌入式凭借十多年的企业级板卡开发与服务经验,深挖嵌入式学习市场的需求和痛点推出全新子品牌ElfBoard,旨在为嵌入式学习爱好者创造更具价值的学习体验。 ElfBoard旗下ELF 1及ELF 1S两款新品已经上市1个月了,两款开发板产品凭借高性价比…

假如我们进入了时间循环,那么如何在时间循环里做最优决策?

👨‍🎓博主简介 🏅云计算领域优质创作者   🏅华为云开发者社区专家博主   🏅阿里云开发者社区专家博主 💊交流社区:运维交流社区 欢迎大家的加入! 🐋 希望大家多多支…

温湿度监测更方便,食品物流很多都这样做!

在当今高度技术化的社会,温度监控不仅仅是一项科技应用,更是各行各业中确保稳定运作和产品质量的重要环节。 温度监控系统的应用不仅有助于维护生产条件,提高效率,还对确保产品质量和符合行业标准起着至关重要的作用。 客户案例 …

直播实时数仓基于DataLeap开放平台在发布管控场景的业务实践

更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群 背景 业务背景 随着字节业务的高速增长,业务场景越来越丰富,业务基于数据做的决策也越来越多,对数据的时效性要求也越来越高。…

CSDN中调整图片和文本样式

1.调整图片比例 插入图片后,觉得图片比例不协调,想改小点。只需要在文件后缀加个参数即可:?pic_center 60x。 NOTE:等号左边一定要加个空格,否则格式不生效 2.修改字体颜色 如上 NOTE:等号左边一定要…

搭建完全分布式Hadoop

文章目录 一、Hadoop集群规划二、在主节点上配置Hadoop(一)登录虚拟机(二)设置主机名(三)主机名与IP地址映射(四)关闭与禁用防火墙(五)配置免密登录&#xff…

Linux Centos配置邮件发送

Linux Centos配置邮件发送 这里使用的是外部发送邮件方式,也就是使用自己的账号发送 第一步 首先要开启STMP授权码,以QQ邮箱为例 配置文件 vim /etc/mail.rc找到之后在最下面添加如下 #邮箱set from3324855376qq.com #默认smtp发送,stmp…

简单聊聊java中各种常量池

一 引子 小试身手 首先我们来看一道题 Integer i1 127; Integer i2 127;System.out.println(i1 i2); //这种调用底层实际是执行的Integer.valueOf(127),里面用到了IntegerCache对象池//值大于127时,不会从对象池中取对象 Integer i3 128; Integ…

4S店汽车行业万能通用小程序源码系统 在线预约试驾+购车计算器 源码完全开源可二次开发

随着互联网技术的发展和普及,越来越多的消费者开始依赖于互联网进行消费。传统的汽车销售模式也正在经历着数字化转型,以适应消费者需求的变化。这款小程序源码系统就是为帮助汽车4S店等销售商实现数字化转型而开发的。 以下是部分核心功能的代码模块&a…

合成数据在医疗保健行业的案例研究

从机器人辅助手术到医学成像技术,人工智能在医疗保健领域的应用正在迅速改变医疗保健行业,并改善服务成本和服务质量。例如,埃森哲表示,到 150 年,人工智能临床健康应用每年可以为美国医疗保健行业节省 2026 亿美元。 …

C++中的函数重载:多功能而强大的特性

引言 函数重载是C编程语言中的一项强大特性,它允许在同一个作用域内定义多个同名函数,但这些函数在参数类型、个数或顺序上有所不同。本文将深入探讨函数重载的用法,以及它的优势和应用场景。 正文 在C中,函数重载是一项非常有…

Spring Security使用总结八,Security的第二个功能授权,不同的角色访问不同的资源

前面五章基本都是给认证做铺垫的,这一章是security的另一个硬菜:授权,你在我这里注册,成为唯爱痞,我给你个令牌,你可以访问我资源,但是不能所有资源都给你,于是就有了授权,你只能访问我让你访问的资源,我不让你访问的资源,你一点都别想看。这里就出现了角色,不同的…

一个不用充钱也能让你变强的 VSCode 插件!!!

今天给大家推荐一款不用充钱也能让你变强的 vscode 插件 通义灵码(TONGYI Lingma),可以称之为 copilot 的替代甜品 💪 前言 之前一直使用的 GitHub Copilot,虽然功能强大,但是收费相对来说有点贵&#xf…

C 语言 while 和 do...while 循环

在本教程中,您将在示例的帮助下学习在C语言编程中创建while和do ... while循环。 在编程中,循环用于重复代码块,直到满足指定条件为止。 C语言编程具有三种类型的循环。 for循环 while循环 do... while循环 在上一教程中,我…