拜托!不要再问我是否了解多线程了好吗

2023-12-15 07:08

本文主要是介绍拜托!不要再问我是否了解多线程了好吗,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

点击上方 好好学java ,选择 星标 公众号

重磅资讯、干货,第一时间送达

今日推荐:腾讯推出高性能 RPC 开发框架

个人原创100W+访问量博客:点击前往,查看更多

来源:https://www.cnblogs.com/yougewe/p/11408151.html

面试过程中,各面试官一般都会教科书式的问你几个多线程的问题,但又不知从何问起。于是就来一句,你了解多线程吗?拜托,这个好伤自尊的!

相信老司机们对于java的多线程问题处理,稳如老狗了。你问我了解不?都懒得理你。

不过,既然是面对的是面试官,那你还得一一说来。

今天我们就从多个角度来领略下多线程技术吧!

1. 为什么会有多线程?

其实有的语言是没有多线程的概念的,而java则是从一出生便有了多线程天赋。为什么?

多线程技术一般又被叫做并发编程,目的是为了程序运行得更快。

其基本原理是,是由cpu进行不同线程的调度,从而实现多个线程的同时运行效果。

多进程和多线程类似,只是多进程不会共享内存资源,切换开销更大,所以多线程是更明智的选择。

而在计算机出现早期,或者也许你也能找到单核的cpu,这时候的多线程是通过不停地切换唯一一个可以运行的线程来实现的,由于切换速度比较快,所以感觉就是多线程同时在运行了。在这种情况下,多线程与多进程等同的。但是,至少也让用户有了可以同时处理多任务的能力了,也是很有用的。

而当下的多核cpu时代,则是真正可以同时运行多个线程的时代,什么四核八线程,八核八线程.... 意味着可以同时并行n个线程。如果我们能让所有可用的线程都利用起来,那么我们的程序运行速度或者说整体性能将会得到极大提升。这是我们技术人员的目标。

2. 多线程就一定快吗?(简略)

看起来,多线程确实挺好,但是凡事皆有度。过尤不及。

如果只运行与cpu能力范围内的n线程,那是绝对ok的。但当你线程数超过这个n时,就会涉及到cpu的调度问题,调度时即会涉及一个上下文切换问题,这是要耗费时间和资源的东西。当cpu疲于奔命调度切换时,则多线程就是一个负担了。

3. 多线程主要注意什么问题?(简略)

多线程要注意的问题多了去了,毕竟这是一门不简单的学问,但是我们也可以总结下:

1. 线程安全性问题;如果连正确性都无法保障,谈性能有何意义?   2. 资源隔离问题;是你就是你的,不是你的就不是你的。   3. 可读性问题;如果为了多线程,将代码搞得一团糟,是否值得?   4. 外部环境问题;如果外部环境很糟糕,那么你内部性能再好,你能把压力给外部吗?

返回顶部

4. 创建多线程的方式?(简略)

这个问题确实有点low, 不过也是一个体现真实实践的地方!

1. 继承Thread类,然后 new MyThread.start(); 2. 继承Runnable类, 然后 new Thread(runnable).start(); 3. 继承Callable类,然后使用 ExecutorService.submit(callable); 4. 使用线程池技术,直接创建n个线程,将上面的方法再来一遍,new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); 简化版: Executors.newFixedThreadPool(n).submit(runnable);

5. 来点实际的场景?(重点)

理论始终太枯燥,不如来点实际的。

有同学说,我平时就写写业务代码,而业务代码基本由用户触发,一条线程走到底,哪来的多线程实践?

好,我们可以就这个问题来说下,这种业务的多线程:

1. 比如一个http请求,对应一个响应,如果不使用多线程,会怎么样?我们可以简单地写一个socket服务器,进行处理业务,但是这绝对不是你想看到的。比如我们常用的 spring+tomcat, 哪里没有用到多线程技术?

     http-nio-8080-exec-xxx #就是一个线程池中例子。

2. 任何一个java应用,启动起来之后,都会有很多的GC线程运行,这难道不是多线程?如:

     "G1 Main Concurrent Mark GC Thread" os_prio=0 tid=0x00007fb91008f000 nid=0x40e7 runnabl "Gang worker#0 (Parallel GC Threads)" os_prio=0 tid=0x00007fb910061800 nid=0x40de runnable

如上这些多线程场景吧,面试官说,就算你了解其原理,那也不算是你的。你有真正使用过多线程吗?

接下来,我们就来说道说道,实际业务场景中,有哪些是我们可能会用上的,供大家参考:

看下多线程中几个有趣或者经典的场景用法!

场景1. 我有一个发邮件的功能,用户操作成功后,我给他发送邮件,如何高效稳定地完成?

场景2. 我有m个线程在循环执行主方法,为实现高效处理,将分离n*m个子线程执行相关联流程,要求子线程必须等到主线程执行完成后才能执行,如何保证?

场景3. 某合作公司要求请求其api的qps不得大于n,如何保证?

场景4. 一个大任务如何提高响应速度?

场景5. 我有n个线程同时开始处理一个事务,要求至少等到一个线程执行完毕后,才能进行响应返回,如何高效处理?

场景6. 抽象任务,后台运行处理任务多线程?

大家应该已经见过世面了,这点问题还不至于,对吧。那你可以拿出你的方案了。

下面是我的解决方案:

场景1. 我有一个发邮件的功能,用户操作成功后,我给他发送邮件,如何高效稳定地完成? 场景1解决:(常规型)

这个可以说最实用最简单的多线程应用场景了,不过现在进行微服务化之后,可能会有一些不同。换汤不换药。

针对C端用户的多线程,我们是不建议使用 new Thread() 这种方式的,线程池是个常用伎俩。

    ExecutorService mailExecutors = Executors.newFixedThreadPool(20); public void sendMail() {mailExecutors.submit(() -> { // do send mail biz, http, rpc,...System.out.println("sending mail");});}

场景2. 我有m个线程在循环执行主方法,为实现高效处理,将分离n*m个子线程执行相关联流程,要求子线程必须等到主线程执行完成后才能执行,如何保证? 场景2解决:(所有等待型)

主任务,只管调度子线程,在子线程使用闭锁在适当的地方进行等待,主线程循环分配完成后,打开闭锁,放行所有子线程即可。

具体代码如下:

    private void mainWork() { try {resetRedisZsetLockGate(); for (String linkTraceCacheKey : expiredKeys) {subWork(linkTraceCacheKey);}} finally {releaseRedisZsetLock();}} private void subWork(String linkTraceCacheKey) {deleteService.execute(new Runnable() {@Override public void run() { // do other bizblockingWaitRedisZsetLock();postSth(linkTraceCacheKey);}});} /** * 重置锁网关,每次主方法的调度都将得到一个私有的锁 */private void resetRedisZsetLockGate() {redisZsetScanLockGate = new CountDownLatch(1);} /** * 阻塞等待 锁 */private void blockingWaitRedisZsetLock() { final CountDownLatch myGate = redisZsetScanLockGate; try {myGate.await();} catch (InterruptedException e) {logger.error("等待锁中断异常", e);Thread.currentThread().interrupt();}} /** * 释放锁 */private void releaseRedisZsetLock() { final CountDownLatch myGate = redisZsetScanLockGate;myGate.countDown();}

场景3. 某合作公司要求请求其api的qps不得大于n,如何保证? 场景3解决:(流量控制型、有限资源型)

这种问题准确的说,使用单机的多线程还是有点难控制的,但是我们只是为了讲清道理,具体(集群)做法只要稍做变通即可。

简单点说,就是作用一个 Semphore 信号量进行数量控制,当数量未到时,直接多线程并发请求,到达限制后,则等待有空闲位置再进行!


public class AbstractConcurrentSimpleLiteJobBase { /** * 并发查询:5 , 动态配置化 */private final Semaphore maxConcurrentQueryLock; /** * 同步等待结束锁,视情况使用,同一个线程可能提交多次任务,由同一个 holder 管理 */private final ThreadLocal<List<Future<?>>> endGateTaskFutureContainer = new ThreadLocal<>();@Resource private ThreadPoolTaskExecutor threadPoolTaskExecutor; public AbstractConcurrentSimpleLiteJobBase() {maxConcurrentQueryLock = new Semaphore(getMaxConcurrentThreadNum());} /** * 获取最大允许的并发数,子类可自定义, 默认:5** @return 最大并发数 */protected int getMaxConcurrentThreadNum() { return 5;} /** * 提交一个任务到线程池执行** @param task 任务 */protected void submitTask(Runnable task) { // 考虑是否要阻塞等待结果Future<?> future1 =  threadPoolTaskExecutor.submit(() -> { try {maxConcurrentQueryLock.acquire();} catch (InterruptedException ie) { // ignore...log.error("【任务运行】异常,中断", ie);Thread.currentThread().interrupt(); return;} try {task.run();} finally {maxConcurrentQueryLock.release();}});endGateCountDown(future1);} /** * 等待线程结果完成,并清理 gate 信息 */private void awaitForComplete() { try { // 同步等待执行完成,防止并发任务执行for(Future<?> future1 : endGateTaskFutureContainer.get()) {future1.get();}endGateTaskFutureContainer.remove();} catch (ExecutionException e) {log.error("【任务执行】异常,抛出异常", e);} catch (InterruptedException e) {log.error("【任务执行】异常,中断", e);}}}

场景4. 一个大任务如何提高响应速度? 场景4解决:(大任务拆分型)

针对大任务的处理,基本想到的都是类似于分布式计算之类的东西(map/reduce),在java单机操作来说,标准的解决方案是 Fork/Join 框架。


public class MyForkJoinTask extends RecursiveTask<Integer> { //原始数据private List<Integer> records; public MyForkJoinTask(List<Integer> records) { this.records = records;}@Override protected Integer compute() { //任务拆分到可接受程度后,运行处理逻辑if (records.size() < 3) { return doRealCompute();} // 否则一直往下拆分任务int size = records.size();MyForkJoinTask aTask = new MyForkJoinTask(records.subList(0, size / 2));MyForkJoinTask bTask = new MyForkJoinTask(records.subList(size / 2, records.size())); //两个任务并发执行invokeAll(aTask, bTask); //结果合并return aTask.join() + bTask.join();} /** * 真正任务处理逻辑 */private int doRealCompute() { try {Thread.sleep((long) (records.size() * 1000));} catch (InterruptedException e) {e.printStackTrace();}System.out.println("计算任务:" + Arrays.toString(records.toArray())); return records.size();} // 测试任务public static void main(String[] args) throws ExecutionException, InterruptedException {ForkJoinPool forkJoinPool = new ForkJoinPool(5);List<Integer> originalData = new ArrayList<>();originalData.add(1);originalData.add(2);originalData.add(3);originalData.add(4);originalData.add(5);originalData.add(6);originalData.add(7);originalData.add(8);originalData.add(9);originalData.add(10);originalData.add(11);originalData.add(12);originalData.add(13);MyForkJoinTask myForkJoinTask = new MyForkJoinTask(originalData); long t1 = System.currentTimeMillis();ForkJoinTask<Integer> affectNums = forkJoinPool.submit(myForkJoinTask);System.out.println("affect nums: " + affectNums.get()); long t2 = System.currentTimeMillis();System.out.println("cost time: " + (t2-t1));}
}

其实如果不用Fork/join 框架,也是可以的,比如我就只开n个线依次从数据源处取数据进行处理,最后将结果合并到另一个队列中。只是,这期间你得多付出多少努力才能做到 Fork/Join 相同的效果呢!

当然了,Fork/Join 的重要特性是: 使用了work-stealing算法。Worker线程跑完任务后,可以从其他还在忙着的线程去窃取任务。

你要愿意造轮子,也是可以的。

场景5. 我有n个线程同时开始处理一个事务,要求至少等到一个线程执行完毕后,才能进行响应返回,如何高效处理? 场景5解决:(至少一个返回型)

初步思路: 主任务中,使用一个闭锁,CountDownLatch(1); 所有子线程执行完成,调用 latch.countDown(); 开启一次闭锁。主任务执行完成后,调用 latch.await(); 阻塞等待,当有任意一个子线程打开闭锁后,就可以返回了。

但是这个是有问题的,即这个锁只会有一次生效机会,后续的完成动作并不会有实际意义,因此只能换一个方式。

使用回调实现,就容易多了,只要一个任务完成,就做一次回调,主任务如果分配完成后,发现有空闲的任务槽,就立即进行下一次分配即可,没有则等到有再进行分配工作。

具体代码如下:


public class TaskDispatcher { /** Main lock guarding all access */final ReentrantLock lock; /** Condition for waiting assign */private final Condition finishedTaskNotEmpty; /** * 正在运行的任务计数器 */private final AtomicInteger runningTaskCounter = new AtomicInteger(0); /** * 新完成的任务计数器,当被重新分派后,此计数将会被置0 */private Integer newFinishedTaskCounter = 0; private void consumLogHub(String shards) throws InterruptedException {resetConsumeCounter();String[] shardList = shards.split(","); for (int i = 0; i < shardList.length; i++) {String shard = shardList[i]; int shardId = Integer.parseInt(shard);LogHubConsumer consuemr = getConsuemer(shardId); if(consuemr.startNewConsumeTask(this)) {runningTaskCounter.incrementAndGet();}}cleanConsumer(Arrays.asList(shardList)); // 没有一个任务已完成,阻塞等待一个完成if(runningTaskCounter.get() > 0) { if(newFinishedTaskCounter == 0) {waitAtLeastOnceTaskFinish();}}} /** * 重置消费者计数器 */private void resetConsumeCounter() {newFinishedTaskCounter = 0;} /** * 阻塞等待至少一个任务执行完成** @throws InterruptedException 中断 */private void waitAtLeastOnceTaskFinish() throws InterruptedException {lock.lockInterruptibly(); try { while (newFinishedTaskCounter == 0) {finishedTaskNotEmpty.await();}} finally {lock.unlock();}} /** * 通知任务完成(回调)** @throws InterruptedException 中断 */private void notifyTaskFinished() throws InterruptedException {lock.lockInterruptibly(); try {runningTaskCounter.decrementAndGet(); // 此处计数不可能小于0newFinishedTaskCounter += 1;finishedTaskNotEmpty.signal();} finally {lock.unlock();}} /** * 通知任务完成(回调)** @throws InterruptedException 中断 */public void taskFinishCallback() throws InterruptedException {notifyTaskFinished();}} public class ConsumerWorker { private Future<?> future;@Resource private ExecutorService consumerService; /** * 当查询结果为时的等待延时, 每次查询结果都会为空时,加大该延时, 直到达到设定的最大值为准 */private Long baseEmptyQueryDelayMills = 200L; private Long emptyQueryDelayMills = baseEmptyQueryDelayMills; /** * 调置最大延时为1秒 */private static final Long maxEmptyQueryDelayMills = 1000L; /** * 记数 */private void encounterEmptyQueryDelay() { if(emptyQueryDelayMills < maxEmptyQueryDelayMills) {emptyQueryDelayMills += 100L;}} private void resetEmptyQueryDelay() {emptyQueryDelayMills = baseEmptyQueryDelayMills;} // 开启一个消费者线程public boolean startNewConsumeTask(LogHubClientWork callback) { if(future==null || future.isCancelled() || future.isDone()) { //没有任务或者任务已取消或已完成 提交任务future = consumerService.submit(new Runnable() {@Override public void run() { try {Integer dealCount = doBizData(); if(dealCount == 0) {SleepUtil.millis(emptyQueryDelayMills);encounterEmptyQueryDelay();} else {resetEmptyQueryDelay();}} finally { try {callback.taskFinishCallback();} catch (InterruptedException e) {logger.error("处理完成通知失败,中断", e);Thread.currentThread().interrupt();}}}}); return true;} return false;}}

场景6. 抽象任务,后台运行处理任务多线程? 场景6解决:(业务相关类)

最简单也是最难的一种,根据具体业务类型做相应处理就好,主要考虑读写的安全性问题。

如上几个多线程的应用场景,是我在工作中切实用上的场景(所言非虚)。不过它们都有一个特点,即任务都是很独立的,即基本上不用太关心线程安全问题,这也是我们编写多线程代码时尽量要做的事。当然很多场景共享数据是一定的,这时候就更要注意线程安全了。

要做到线程安全也不是难事,比如足够好的封装,可以让你把关注点锁定在很小的范围内。

当然,为了线程安全,我们可能往往又会牺牲性能,这就看我们如何把握这些度了!互斥锁是最容易使用的锁,但是也是性能最差的锁。分段锁能够解决锁性能问题,但是又会给编写带来更大的困难。

多线程,不止要会写,还要会给自己填坑。

最后,再附上我历时三个月总结的 Java 面试 + Java 后端技术学习指南,笔者这几年及春招的总结,github 1.4k star,拿去不谢!下载方式1. 首先扫描下方二维码
2. 后台回复「Java面试」即可获取

这篇关于拜托!不要再问我是否了解多线程了好吗的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/495569

相关文章

浅析Rust多线程中如何安全的使用变量

《浅析Rust多线程中如何安全的使用变量》这篇文章主要为大家详细介绍了Rust如何在线程的闭包中安全的使用变量,包括共享变量和修改变量,文中的示例代码讲解详细,有需要的小伙伴可以参考下... 目录1. 向线程传递变量2. 多线程共享变量引用3. 多线程中修改变量4. 总结在Rust语言中,一个既引人入胜又可

shell脚本快速检查192.168.1网段ip是否在用的方法

《shell脚本快速检查192.168.1网段ip是否在用的方法》该Shell脚本通过并发ping命令检查192.168.1网段中哪些IP地址正在使用,脚本定义了网络段、超时时间和并行扫描数量,并使用... 目录脚本:检查 192.168.1 网段 IP 是否在用脚本说明使用方法示例输出优化建议总结检查 1

如何测试计算机的内存是否存在问题? 判断电脑内存故障的多种方法

《如何测试计算机的内存是否存在问题?判断电脑内存故障的多种方法》内存是电脑中非常重要的组件之一,如果内存出现故障,可能会导致电脑出现各种问题,如蓝屏、死机、程序崩溃等,如何判断内存是否出现故障呢?下... 如果你的电脑是崩溃、冻结还是不稳定,那么它的内存可能有问题。要进行检查,你可以使用Windows 11

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

Codeforces Round #113 (Div. 2) B 判断多边形是否在凸包内

题目点击打开链接 凸多边形A, 多边形B, 判断B是否严格在A内。  注意AB有重点 。  将A,B上的点合在一起求凸包,如果凸包上的点是B的某个点,则B肯定不在A内。 或者说B上的某点在凸包的边上则也说明B不严格在A里面。 这个处理有个巧妙的方法,只需在求凸包的时候, <=  改成< 也就是说凸包一条边上的所有点都重复点都记录在凸包里面了。 另外不能去重点。 int

多线程解析报表

假如有这样一个需求,当我们需要解析一个Excel里多个sheet的数据时,可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要提示解析完成。 Way1 join import java.time.LocalTime;public class Main {public static void main(String[] args) thro

easyui同时验证账户格式和ajax是否存在

accountName: {validator: function (value, param) {if (!/^[a-zA-Z][a-zA-Z0-9_]{3,15}$/i.test(value)) {$.fn.validatebox.defaults.rules.accountName.message = '账户名称不合法(字母开头,允许4-16字节,允许字母数字下划线)';return fal

【408DS算法题】039进阶-判断图中路径是否存在

Index 题目分析实现总结 题目 对于给定的图G,设计函数实现判断G中是否含有从start结点到stop结点的路径。 分析实现 对于图的路径的存在性判断,有两种做法:(本文的实现均基于邻接矩阵存储方式的图) 1.图的BFS BFS的思路相对比较直观——从起始结点出发进行层次遍历,遍历过程中遇到结点i就表示存在路径start->i,故只需判断每个结点i是否就是stop

速了解MySQL 数据库不同存储引擎

快速了解MySQL 数据库不同存储引擎 MySQL 提供了多种存储引擎,每种存储引擎都有其特定的特性和适用场景。了解这些存储引擎的特性,有助于在设计数据库时做出合理的选择。以下是 MySQL 中几种常用存储引擎的详细介绍。 1. InnoDB 特点: 事务支持:InnoDB 是一个支持 ACID(原子性、一致性、隔离性、持久性)事务的存储引擎。行级锁:使用行级锁来提高并发性,减少锁竞争

linux 判断某个命令是否安装

linux 判断某个命令是否安装 if ! [ -x "$(command -v git)" ]; thenecho 'Error: git is not installed.' >&2exit 1fi