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

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

相关文章

一文带你了解SpringBoot中启动参数的各种用法

《一文带你了解SpringBoot中启动参数的各种用法》在使用SpringBoot开发应用时,我们通常需要根据不同的环境或特定需求调整启动参数,那么,SpringBoot提供了哪些方式来配置这些启动参... 目录一、启动参数的常见传递方式二、通过命令行参数传递启动参数三、使用 application.pro

Java使用多线程处理未知任务数的方案介绍

《Java使用多线程处理未知任务数的方案介绍》这篇文章主要为大家详细介绍了Java如何使用多线程实现处理未知任务数,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 知道任务个数,你可以定义好线程数规则,生成线程数去跑代码说明:1.虚拟线程池:使用 Executors.newVir

一文带你深入了解Python中的GeneratorExit异常处理

《一文带你深入了解Python中的GeneratorExit异常处理》GeneratorExit是Python内置的异常,当生成器或协程被强制关闭时,Python解释器会向其发送这个异常,下面我们来看... 目录GeneratorExit:协程世界的死亡通知书什么是GeneratorExit实际中的问题案例

JAVA封装多线程实现的方式及原理

《JAVA封装多线程实现的方式及原理》:本文主要介绍Java中封装多线程的原理和常见方式,通过封装可以简化多线程的使用,提高安全性,并增强代码的可维护性和可扩展性,需要的朋友可以参考下... 目录前言一、封装的目标二、常见的封装方式及原理总结前言在 Java 中,封装多线程的原理主要围绕着将多线程相关的操

Python中多线程和多进程的基本用法详解

《Python中多线程和多进程的基本用法详解》这篇文章介绍了Python中多线程和多进程的相关知识,包括并发编程的优势,多线程和多进程的概念、适用场景、示例代码,线程池和进程池的使用,以及如何选择合适... 目录引言一、并发编程的主要优势二、python的多线程(Threading)1. 什么是多线程?2.

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

SpringBoot中使用 ThreadLocal 进行多线程上下文管理及注意事项小结

《SpringBoot中使用ThreadLocal进行多线程上下文管理及注意事项小结》本文详细介绍了ThreadLocal的原理、使用场景和示例代码,并在SpringBoot中使用ThreadLo... 目录前言技术积累1.什么是 ThreadLocal2. ThreadLocal 的原理2.1 线程隔离2

Java多线程父线程向子线程传值问题及解决

《Java多线程父线程向子线程传值问题及解决》文章总结了5种解决父子之间数据传递困扰的解决方案,包括ThreadLocal+TaskDecorator、UserUtils、CustomTaskDeco... 目录1 背景2 ThreadLocal+TaskDecorator3 RequestContextH

C#比较两个List集合内容是否相同的几种方法

《C#比较两个List集合内容是否相同的几种方法》本文详细介绍了在C#中比较两个List集合内容是否相同的方法,包括非自定义类和自定义类的元素比较,对于非自定义类,可以使用SequenceEqual、... 目录 一、非自定义类的元素比较1. 使用 SequenceEqual 方法(顺序和内容都相等)2.