Java并发编程实战
王宝令
资深架构师
立即订阅
15062 人已学习
课程目录
已完结 50 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 你为什么需要学习并发编程?
免费
学习攻略 (1讲)
学习攻略 | 如何才能学好并发编程?
第一部分:并发理论基础 (13讲)
01 | 可见性、原子性和有序性问题:并发编程Bug的源头
02 | Java内存模型:看Java如何解决可见性和有序性问题
03 | 互斥锁(上):解决原子性问题
04 | 互斥锁(下):如何用一把锁保护多个资源?
05 | 一不小心就死锁了,怎么办?
06 | 用“等待-通知”机制优化循环等待
07 | 安全性、活跃性以及性能问题
08 | 管程:并发编程的万能钥匙
09 | Java线程(上):Java线程的生命周期
10 | Java线程(中):创建多少线程才是合适的?
11 | Java线程(下):为什么局部变量是线程安全的?
12 | 如何用面向对象思想写好并发程序?
13 | 理论基础模块热点问题答疑
第二部分:并发工具类 (14讲)
14 | Lock和Condition(上):隐藏在并发包中的管程
15 | Lock和Condition(下):Dubbo如何用管程实现异步转同步?
16 | Semaphore:如何快速实现一个限流器?
17 | ReadWriteLock:如何快速实现一个完备的缓存?
18 | StampedLock:有没有比读写锁更快的锁?
19 | CountDownLatch和CyclicBarrier:如何让多线程步调一致?
20 | 并发容器:都有哪些“坑”需要我们填?
21 | 原子类:无锁工具类的典范
22 | Executor与线程池:如何创建正确的线程池?
23 | Future:如何用多线程实现最优的“烧水泡茶”程序?
24 | CompletableFuture:异步编程没那么难
25 | CompletionService:如何批量执行异步任务?
26 | Fork/Join:单机版的MapReduce
27 | 并发工具类模块热点问题答疑
第三部分:并发设计模式 (10讲)
28 | Immutability模式:如何利用不变性解决并发问题?
29 | Copy-on-Write模式:不是延时策略的COW
30 | 线程本地存储模式:没有共享,就没有伤害
31 | Guarded Suspension模式:等待唤醒机制的规范实现
32 | Balking模式:再谈线程安全的单例模式
33 | Thread-Per-Message模式:最简单实用的分工方法
34 | Worker Thread模式:如何避免重复创建线程?
35 | 两阶段终止模式:如何优雅地终止线程?
36 | 生产者-消费者模式:用流水线思想提高效率
37 | 设计模式模块热点问题答疑
第四部分:案例分析 (4讲)
38 | 案例分析(一):高性能限流器Guava RateLimiter
39 | 案例分析(二):高性能网络应用框架Netty
40 | 案例分析(三):高性能队列Disruptor
41 | 案例分析(四):高性能数据库连接池HiKariCP
第五部分:其他并发模型 (4讲)
42 | Actor模型:面向对象原生的并发模型
43 | 软件事务内存:借鉴数据库的并发经验
44 | 协程:更轻量级的线程
45 | CSP模型:Golang的主力队员
结束语 (1讲)
结束语 | 十年之后,初心依旧
用户故事 (2讲)
用户来信 | 真好,面试考到这些并发编程,我都答对了!
3 个用户来信 | 打开一个新的并发世界
Java并发编程实战
登录|注册

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

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

避免共享的设计模式

Immutability 模式Copy-on-Write 模式线程本地存储模式本质上都是为了避免共享,只是实现手段不同而已。这 3 种设计模式的实现都很简单,但是实现过程中有些细节还是需要格外注意的。例如,使用 Immutability 模式需要注意对象属性的不可变性,使用 Copy-on-Write 模式需要注意性能问题,使用线程本地存储模式需要注意异步执行问题。所以,每篇文章最后我设置的课后思考题的目的就是提醒你注意这些细节。
《28 | Immutability 模式:如何利用不变性解决并发问题?》的课后思考题是讨论 Account 这个类是不是具备不可变性。这个类初看上去属于不可变对象的中规中矩实现,而实质上这个实现是有问题的,原因在于 StringBuffer 不同于 String,StringBuffer 不具备不可变性,通过 getUser() 方法获取 user 之后,是可以修改 user 的。一个简单的解决方案是让 getUser() 方法返回 String 对象。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Java并发编程实战》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(13)

  • coolrandy
    老师好 能不能后面讲一讲分布式锁相关的东西,比如实现方案,原理和场景之类的
    2019-05-23
    15
  • 拯救地球好累
    并发设计模式是前人在做并发编程时已经归纳好的,在不同场景下具有可行性的设计模式,我们在设计并发程序时应当优先考虑这些设计模式(以及这些设计模式的组合)。
    对各类并发设计模式,考量其核心思想、核心技术、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
  • 青莲
    老师想请问下,如果jvm挂了,有没有好的办法能记录下线程池当前未处理的任务

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

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

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

    2019-05-23
    3
  • 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
    1
  • 星辰
    青莲同学,当老师说没有好的办法的时候,不知道为啥,我总想笑😂😂😂
    2019-06-10
    1
  • aoe
    老师推荐的书都挺好
    2019-11-19
  • null
    老师,您好!
    文章示例中,使用毒丸对象终止线程的场景是单线程。
    如果是多线程的情况,如何也让其余线程优雅退出呢?
    谢谢老师

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

    2019-08-23
  • 星辰
    喜欢宝令老师的专栏

    作者回复: 😄

    2019-07-09
    1
  • null
    作者: 下次运行时重建线程池。你关闭线程池的原因是什么?

    谢谢老师回复!!
    每天凌晨跑结算数据,每天只跑一次,就想着跑完任务之后,关闭线程池,这样就不会再占用服务器资源了。

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

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

    作者回复: 👍

    2019-05-23
  • 强哥
    很期待接下来两个模块的深入讲解!
    2019-05-23
  • 张三
    打卡!
    2019-05-23
收起评论
13
返回
顶部