多线程——面试中又一个常考的内容(9)

张开发
2026/4/10 10:18:42 15 分钟阅读

分享文章

多线程——面试中又一个常考的内容(9)
上次我们初步的完成了一个生产者消费者模型现在我们继续。可是在生产者消费者模型的优势的背后还存在着诸如服务器结构更复杂以及执行效率低的劣势因此我们要在这两个地方入手来优化这个模型。生产者消费者模型的优化1.wait阻塞等待与notify通知唤醒这里之前的if条件之下的return改为wait是为了控制唤醒的时机队列不满的时候才要唤醒也就是当其他线程成功执行take的时候。同理在take方法内部想要从队列中取出元素当队列为空的时候才要唤醒也就是当其他线程执行成功put的时候。当然有wait等待就一定要有notify唤醒。其中put方法的notify唤醒take方法的waittake方法的notify唤醒put方法的wait。这样就实现了这样的效果比如有若干个线程使用这个队列要么所有的线程阻塞在put要么所有的线程阻塞在take不可能有一些线程阻塞在put一些阻塞在take出现既空又满的现象。2.条件改为while循环可是关于if这个条件判断IDEA建议替换为while这是为什么呢本来if条件下wait是用来确保接下来的操作是有意义的正常来说wait的唤醒是通过另一个线程执行put另一个线程执行put成功了此处的size肯定不是0。可是图中的wait不一定只是被notify唤醒还可能被Interrupt这样的方法给中断比如size等于0时在wait里加参数1000那么在1秒之后即使notify不唤醒waitwait也就继续执行执行到size--队列中的元素个数就变成-1了。如果使用if作为wait的判定条件此时就存在wait被提前唤醒的风险。所以我们要加while当判断条件变成了while之后就变成了一个循环这里的循环是为了“二次验证”判定当前的条件是否成立wait之前先判定一次wait唤醒后也判定一次再确认一下队列是否不空。这里有人提了一个非常有建设性的问题在多线程下唤醒的不是随机调度的wait吗这里回答一下执行到notify的线程必然是唤醒别人的一个线程不可能在wait阻塞的情况下执行notify的而对于一个线程的put唤醒其他线程的put其他线程已经put阻塞了这个时候又来一个新的put也是在条件阻塞队列满了的情况。如果只是一个线程take一个线程put不会出现自己唤醒自己的情况而在多个线程put多个线程take的时候确实是有风险的但是可以通过while循环判定条件避免这样的唤醒给程序带来的负面影响。线程池之前我们学习过常量池这是字符串常量在Java程序最初创建的时候就已经准备好等程序运行的时候这样的常量也就加载到内存中了。这样也就省下了构造与销毁的开销。在计算机中“池”这个词就只有这一个意思表示的含义都是一样的。线程池就是为了让我们高效的创建与销毁线程的。最初引入线程的原因是频繁创建与销毁进程太慢了随着互联网的发展随着我们对性能的要求进一步提高现在我们觉得频繁的创建与销毁线程开销也有些不能接受了。这里解决方案有两个1线程池2协程纤程轻量级线程但没有普遍化线程池是把线程提前创建好放到一个地方放到类似于数组。需要用的时候随时去取用完了还回池子中的存储核心线程与非核心线程的池子。可是这个还要创建池子为什么我们认为直接创建线程开销比从池子里取线程的开销更大呢这里我们先了解操作系统的用户态和内核态重要的是内核。一个操作系统分为内核与配套的应用程序而内核包含操作系统中的各种核心功能如管理硬件设备、给软件提供稳定的运行环境等。一个操作系统内核就是一份一份内核要给所有的应用程序提供服务支持。不妨我们看下图银行中的业务比如你要复印文件有两种方式1直接找柜员让他帮忙复印直接创建销毁线程2自助复印机复印线程池方式一的话柜员在帮你复印的过程中或者复印之前他可能会做其他事情比如向领导汇报工作、上撤硕吃饭等等而且一个柜员要应付多个客户的请求给你办完事情花的时间就长一点方式二自助复印机就专心打印不干其他的事情。如果有一段代码是应用程序中自动完成的整个执行过程是可控的方式二而如果有一段代码需要进入到内核中由内核负责完成一系列工作这个过程是不可控的代码干预不了方式一。通常我们认为可控的过程要比不可控的过程更高效。于是从线程池取线程纯应用程序代码就可以完成可控的而从操作系统创建新线程就需要操作系统内核配合完成不可控的。使用线程池就可以省下应用程序切换到内核中运行这样的开销。基本语法在Java标准库里也提供了直接使用的线程池ThreadPoolExecutor线程池里准备好一些线程让这些线程执行一些任务。核心方法submitRunnable通过Runnable描述一段要执行的任务通过submit任务放到线程池中此时线程池中的线程就会执行这样的任务构造这个类的时候构造方法比较麻烦Java的线程池里包含几个线程是可以动态调整的任务多的时候自动扩容成更多的线程任务少的时候把额外的线程干掉节省资源。下面一道经典的面试题解释一下线程池中这些参数的含义一共有六个1.核心线程数2.最大线程数3.非核心线程允许空闲的最大时间4.枚举这个主要是相关的单位5.工厂模式比如一个点的坐标可以用平面直角坐标系x,y也可以用极坐标r,a上图是正确的写法可是当我们不用静态方法时就构成了重载工厂方法的核心通过静态方法把构造对象new的过程、各种属性初始化的过程封装起来了。提供多组静态方法实现不同情况的构造。而提供工厂方法的类就可以称为“工厂类”。6.拒绝策略我们知道submit把任务添加到任务队列中任务队列就是阻塞队列。队列满了的时候再添加就会发生阻塞但是我们在实际工作中一般不希望程序阻塞太多因为那样会影响线程执行效率。如果调用submit就阻塞就会使这个线程没法干别的事了这不是一个好选择这个线程要响应用户的请求阻塞了用户收不到请求的响应用户等了很久直观上看到的现象就是“卡了”与其说是卡了不如直接告诉我“加载失败”反正早晚都是这样的结果。因此对于线程池来说发现入队操作时队列满了不会真的触发“入队列操作”不会真的阻塞而是执行拒绝策略相关的代码。而拒绝策略主要有四种类表示这四种要重点掌握。所以Java标准库也提供了另一组类针对ThreadPoolExecutor也是基于工厂设计模式进行了进一步封装以简化线程池的使用。用Executors来访问以下方法那么今天的内容到这里就结束了明天我们继续。我的gitee链接https://gitee.com/QQ2240635095/java4_8.git

更多文章