点击上方 好好学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面试」即可获取

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

  1. 倒排索引原理_拜托,面试请不要再问我分布式搜索引擎的架构原理!

    欢迎关注头条号:石杉的架构笔记 周一至周五早八点半!精品技术文章准时送上!!! 精品学习资料获取通道,参见文末 目录 (1)倒排索引到底是啥? (2)什么叫分布式搜索引擎? (3)ElasticSea ...

  2. c 取数组 最大值 算法_拜托,面试别再问我最大值最小值了!!!

    如何从n个数里找到最大值? 很容易想到,用一个循环就能搞定. int find_max(int arr[n]){     int max = -infinite;     for(int i=0; i ...

  3. c语言减治法求a的n次方算法,拜托,面试别再问我斐波那契数列了!!!

    面试中,问得比较多的几个问题之一,求斐波那契数列f(n)? 画外音:姐妹篇 <拜托,面试别再问我TopK了!!!> <拜托,面试别再让我数1了!!!> 什么是斐波那契数列? 斐 ...

  4. 拜托,面试别再问我斐波那契数列了!!!

    面试中,问得比较多的几个问题之一,求斐波那契数列f(n)? 画外音:姐妹篇 <拜托,面试别再问我TopK了!!!> <拜托,面试别再让我数1了!!!> 什么是斐波那契数列? 斐 ...

  5. 拜托,面试别再问我时间复杂度了!!!

    最烦面试官问,"为什么XX算法的时间复杂度是OO",今后,不再惧怕这类问题. 快速排序分为这么几步: 第一步,先做一次partition: partition使用第一个元素t=ar ...

  6. 请别再问我什么是分布式事务

    点击上方蓝色字体,选择"设为星标" 优质文章,及时送达 本文来源:https://dwz.cn/730BLvt0 1 基础概念 1.1 什么是事务 什么是事务?举个生活中的例子:你 ...

  7. 面试官,不要再问我“Java虚拟机类加载机制”了

    关于Java虚拟机类加载机制往往有两方面的面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程.其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解. 面试题试水 现在有这样一道判断程 ...

  8. 面试官再问我如何保证 RocketMQ 不丢失消息,这回我笑了!

    0x00. 消息的发送流程 一条消息从生产到被消费,将会经历三个阶段: 生产阶段,Producer 新建消息,然后通过网络将消息投递给 MQ Broker 存储阶段,消息将会存储在 Broker 端磁 ...

  9. 不要再问我“Java GC垃圾回收机制”了

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! Java GC垃圾回收几乎是面试必问的JVM问题之一,本篇文章带领大家了解Java GC的底 ...

最新文章

  1. python编程入门教学下载-Python编程从入门到实践的PDF教程免费下载
  2. 机器学习——支持向量机SVM之非线性模型(低维到高维映射)
  3. 操作系统和数据库的知识梳理(思维导图)
  4. linux gnome_GNOME,生日快乐:喜欢这个Linux桌面的8个理由
  5. c python boost.python_如何利用Boost.Python实现Python C/C++混合编程详解
  6. linux下监控网络连接,Linux网络监控工具--netstat及网络连接分析
  7. au人声处理_Audacity音频处理
  8. MFC中让自定义的类能响应消息
  9. 一种基于频域滤波法消除干扰项与角谱法重构技术的数字全息显微台阶形貌测量实例分析
  10. label怎么换行 vb_C#与VB.NET换行符的对比及某些string在label能正常换行,在textbox不能换行的问题...
  11. android中正则表达式截取html中的video标签
  12. 城市天际线伊甸园39W人口存档
  13. 徙步藏东南不是江南胜似江南
  14. laragon集成环境使用,跑起laravel项目
  15. 【Linux】定时任务crontab和at命令详解
  16. 推荐用于环境识别的机器人摄像头
  17. 女生适合学软件测试吗?这个工作压力大不大?
  18. Android系列之Navigation的目的地(Destination)
  19. 拼图游戏(8 puzzle)
  20. HASH和HMAC(4):SHA-224和SHA-256算法原理

热门文章

  1. JavaScript的作用域与闭包
  2. Centos7:查看某个端口被哪个进程占用
  3. AB1601中断的问题
  4. MATLAB之简谐信号声音的生成及其调制性
  5. Hyperledger Fabric 管道(2) 管道的操作
  6. 数学建模——智能优化之遗传算法详解Python代码
  7. 2021年第二届“华数杯”全国大学生数学建模竞赛
  8. buuctf 文件中的秘密
  9. OpenSSL“心脏出血”漏洞
  10. php中单引号与双引号的区别