Java 并发编程实战
王宝令
资深架构师
72486 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 51 讲
学习攻略 (1讲)
Java 并发编程实战
15
15
1.0x
00:00/00:00
登录|注册

37 | 设计模式模块热点问题答疑

生产者-消费者模式
Worker Thread模式
Thread-Per-Message模式
Balking模式
Guarded Suspension模式
线程本地存储模式
Copy-on-Write模式
Immutability模式
三种最简单的分工模式
多线程版本IF的设计模式
避免共享的设计模式
并发设计模式

该思维导图由 AI 生成,仅供参考

多线程设计模式是前人解决并发问题的经验总结,当我们试图解决一个并发问题时,首选方案往往是使用匹配的设计模式,这样能避免走弯路。同时,由于大家都熟悉设计模式,所以使用设计模式还能提升方案和代码的可理解性。
在这个模块,我们总共介绍了 9 种常见的多线程设计模式。下面我们就对这 9 种设计模式做个分类和总结,同时也对前面各章的课后思考题做个答疑。

避免共享的设计模式

Immutability 模式Copy-on-Write 模式线程本地存储模式本质上都是为了避免共享,只是实现手段不同而已。这 3 种设计模式的实现都很简单,但是实现过程中有些细节还是需要格外注意的。例如,使用 Immutability 模式需要注意对象属性的不可变性,使用 Copy-on-Write 模式需要注意性能问题,使用线程本地存储模式需要注意异步执行问题。所以,每篇文章最后我设置的课后思考题的目的就是提醒你注意这些细节。
《28 | Immutability 模式:如何利用不变性解决并发问题?》的课后思考题是讨论 Account 这个类是不是具备不可变性。这个类初看上去属于不可变对象的中规中矩实现,而实质上这个实现是有问题的,原因在于 StringBuffer 不同于 String,StringBuffer 不具备不可变性,通过 getUser() 方法获取 user 之后,是可以修改 user 的。一个简单的解决方案是让 getUser() 方法返回 String 对象。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文总结了多线程设计模式模块中的热点问题和答疑。文章首先介绍了多线程设计模式的重要性和应用价值,然后对9种常见的多线程设计模式进行了分类和总结。其中包括避免共享的设计模式、多线程版本IF的设计模式以及三种最简单的分工模式。每种设计模式都针对实际应用中可能遇到的问题进行了详细讨论和解答。 在避免共享的设计模式中,文章提到了Immutability模式、Copy-on-Write模式和线程本地存储模式,并强调了实现过程中需要注意的细节和注意事项。在多线程版本IF的设计模式中,文章讨论了Guarded Suspension模式和Balking模式,以及在实现过程中可能出现的性能和竞态条件问题。在三种最简单的分工模式中,文章重点强调了Thread-Per-Message模式、Worker Thread模式和生产者-消费者模式的实现细节和注意事项。 此外,文章还提到了如何优雅地终止线程以及推荐了一本相关的并发编程入门书籍。总的来说,本文通过对多线程设计模式模块中的热点问题进行解答,为读者提供了深入了解并发设计模式的机会,并鼓励读者在留言区分享想法和思考过程。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 并发编程实战》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(16)

  • 最新
  • 精选
  • 拯救地球好累
    并发设计模式是前人在做并发编程时已经归纳好的,在不同场景下具有可行性的设计模式,我们在设计并发程序时应当优先考虑这些设计模式(以及这些设计模式的组合)。 对各类并发设计模式,考量其核心思想、核心技术、trade-off、适用场景、与其他设计模式的对比等。 首先,应当考虑没有共享的模式,这类方式用一些技术手段来避免并发编程中需要考虑的同步、互斥等问题,有些模式的实现也被称为无锁机制,其简单且不易出错。 Immutability模式充分利用了面向对象的封装特性,将类的mutator的入口全部取消,自身的状态仅允许创建时设置。状态的改变通常通过新建一个对象来达成,为了避免频繁创建新对象,通常通过享元模式或对象池来解决该问题。因此,其适用于对象状态较少改变或不变的场景,需要对一定的内存overhead可容忍。 COW模式通过写时拷贝的方式保证读取时候的无阻塞及多线程读写时的无共享,由于其写入时的拷贝机制和加锁机制(JAVA中),因此仅适合于读多写非常少的场景。相比于Immutability模式,COW将引用指向新对象的操作封装在了内部(JAVA中)来实现一定的可变性。 线程本地存储模式利用线程本地存储空间(TLAB)来存储线程级别的对象以保证各线程操作对象的隔离性,一定程度上可以等同于能够携带上下文信息的局部变量。JAVA中是在用户空间实现的ThreadLocal控制的,目前的实现可以保证map的生命周期与各Thread绑定,但Value需要我们手动remove来避免内存泄漏。 其次,从分工、同步、互斥三个角度来看几个设计模式。 从分工的角度看,以下三种模式在对线程工作粒度的划分上逐渐变细。 Thread-per-message模式通过一消息/请求一线程的方式处理消息/请求,这种模式要求线程创建/销毁overhead低且线程占用内存的overhead也低,因此在overhead高时需要保证线程的数量不多,或者采用更轻量级的线程(如协程)来保证。 Worker Thread模式相当于在Thread-per-message模式的基础上让消息/请求与threads的工厂打交道,在JAVA中可以理解为线程池,通过将同类消息/请求聚类到某类工厂(也有工厂模式的意思在)来为这类消息/请求提供统一的服务(定量的线程数、统一的创建方法、统一的出错处理等),当然,它依然有Thread-per-message中需要控制线程占用内存的问题。 生产者-消费者模式在Woker Thread模式的基础上加入了对消息/请求的控制(大部分使用队列来控制),并划定了生产者线程和消费者线程,其中它也包含了同步和互斥的设计,在JAVA中的线程池中也可见一斑。这类设计常见于MQ中。 从同步和互斥的角度看,多线程版本的if被划分为了两种模式(Guarded Suspension模式和Balking模式)。 Guarded Suspension模式是传统的等待-通知机制的实现,非常标准化,JAVA中则依赖管程实现了各种工具类来保证多线程版本if的正确性。 Balking模式依赖于互斥保证多线程版本if的正确性。 两阶段终止模式在线程粒度的管理中通过中断操作和置位标记来保证正常终止,JAVA中在线程池粒度的管理中可以通过SHUNDOWN方法来对线程池进行操作,源码中可以看到,其实质也是通过第一种方式来达成目的的。

    作者回复: 👍

    2019-08-10
    4
    55
  • 青莲
    老师想请问下,如果jvm挂了,有没有好的办法能记录下线程池当前未处理的任务

    作者回复: 没有好的办法,可以通过分布式来解决,把未处理的任务先放到数据库里,处理完从数据库删除

    2019-05-25
    12
  • PJ ◕‿◕
    老师好 能不能后面讲一讲分布式锁相关的东西,比如实现方案,原理和场景之类的

    作者回复: 方案就是利用zk,redis,db,也可以用atomix这样的工具类自己做集群管理,网上有很多资料,最近实在太忙了😂😂😂

    2019-05-23
    10
  • null
    老师,您好! 我有一个批跑任务,第一次调用 start() 方法启动任务,当任务跑完后,调用 stop() 方法,正常退出线程池。 当下一次再调用 start() 方法启动任务时,报: java.util.concurrent.RejectedExecutionException: com.xxx.LoggerService$$Lambda$12/690901601@72f8abb rejected from java.util.concurrent.ThreadPoolExecutor@9e8742e[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 1] 错误位置:ThreadPoolExecutor.java:2047 请问老师,当每次任务运行完毕之后,我想正常退出线程池,也希望下一次运行时,能继续正常运行,该如何做呢? 谢谢老师 下面是 demo: @Service public class LoggerService { // 用于终止日志执行的“毒丸” final LogMsg poisonPill = new LogMsg(LEVEL.ERROR, ""); // 任务队列 final BlockingQueue<LogMsg> bq = new LinkedBlockingQueue<>(); // 只需要一个线程写日志 ExecutorService es = Executors.newFixedThreadPool(1); // 启动写日志线程 public void start() { System.out.println("启动日志服务"); this.es.execute(() -> { try { while (true) { System.out.println("获取日志内容"); LogMsg log = bq.poll(5, TimeUnit.SECONDS); // 如果是“毒丸”,终止执行 if (poisonPill.equals(log)) { break; } // 省略执行逻辑 } } catch (Exception e) { } finally { } }); } // 终止写日志线程 public void stop() { System.out.println("关闭日志服务"); // 将“毒丸”对象加入阻塞队列 bq.add(poisonPill); es.shutdown(); } // 日志级别 enum LEVEL { INFO, ERROR } class LogMsg { LEVEL level; String msg; // 省略构造函数实现 LogMsg(LEVEL lvl, String msg) { } // 省略 toString() 实现 } }

    作者回复: 下次运行时重建线程池。你关闭线程池的原因是什么?

    2019-06-15
    3
  • Monday
    说实话老师的学习框架,总结归纳能力佩服佩服。每个小结都能串成串。 感觉thread-per-message,work-thread模式都是属于生产-消费者模式。 前者属于不限线程,不重复利用(有点勉强)。 后者属于单生产者,多消费者

    作者回复: ������������

    2020-12-22
  • null
    老师,您好! 文章示例中,使用毒丸对象终止线程的场景是单线程。 如果是多线程的情况,如何也让其余线程优雅退出呢? 谢谢老师

    作者回复: 《Java并发编程实战》里有详细的介绍,你可以参考一下

    2019-08-23
    2
  • yang
    喜欢宝令老师的专栏

    作者回复: 😄

    2019-07-09
    2
  • null
    作者: 下次运行时重建线程池。你关闭线程池的原因是什么? 谢谢老师回复!! 每天凌晨跑结算数据,每天只跑一次,就想着跑完任务之后,关闭线程池,这样就不会再占用服务器资源了。

    作者回复: 这种情况可能没必要用线程池,如果需要,可以设置合适的corepoolsize和keepalivetime,也可以重建

    2019-06-15
  • 缪文
    毒丸对象,我也用过,就是一个可以通过外部接口或消息通知还写的bean,需要终止时设置为终止状态,不终止时是正常状态,消费线程在读到终止状态时直接跳过任务执行,线程也就完成终止了

    作者回复: 👍

    2019-05-23
  • coolrandy
    老师好 能不能后面讲一讲分布式锁相关的东西,比如实现方案,原理和场景之类的
    2019-05-23
    1
    30
收起评论
显示
设置
留言
16
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部