第11章| Java并发编程实践

生产者和消费者模式

该模式通过平衡生产者线程与消费者线程的工作能力来提高程序整体处理数据的速度

生产者和消费者模式概念

生产者和消费者模式是通过一个容器来解决生产者与消费者的强耦合问题。 生产者与消费者不直接通信,而是通过阻塞队列通信,所以生产者生产完数据后不用等待消费者处理,直接扔给阻塞队列;同时消费者不找生产者要数据,而是从阻塞队列处取数据。这里的阻塞队列相当于一个缓冲区,平衡了生产者与消费者的处理能力。

多生产者和多消费者场景

多生产者多消费者模式

场景:在长连接服务器中,生产者1负责将所有客户端发送的消息存放在阻塞队列1中,消费者1从队列读取消息,然后通过消息ID进行散列得到N个队列中的一个,再根据编号将消息存在不同的队列中。每个阻塞队列会被分配一个消费线程,如果消费者3无法消费消息,会将消息重新抛回到阻塞队列1中,交给其他消费者处理。

线程池与生产消费者模式

Java线程池类是一种高明的生产消费者模式实现方式,生产者将任务丢给线程池,线程池创建线程并处理任务,如果将要运行的任务数大于线程池的基本线程数就把任务扔给阻塞队列。这种做法要比只用一个阻塞队列来实现生产者和消费者模式高级,因为消费者能处理的就直接处理掉了,速度更快。而生产者消费者模式需要生产者先存,消费者后取,速度慢。

系统可用线程池是实现多生产者消费者模式:比如,创建N个不同规模的Java线程池来处理不同性质的任务,比如线程池1将数据读到内存之后,交给线程池2里的线程继续处理压缩数据。线程池1主要处理IO密集型任务,线程池2主要处理CPU密集型任务。

线上问题定位

线上在不能调试代码的情况下,定位问题只能看日志系统状态dump线程。

top命令(查看每个线程情况)

  • 输入top
    Java程序应用,只需关注COMMAND栏为Java的性能数据

  • 在输入top后,再按数字1
    查看每个CPU的性能数据,其中CPU参数含义如下:

    参数 描述
    us 用户空间占用CPU百分比
    sy 内核空间占用CPU百分比
    ni 用户进程空间内改变过优先级的进程占用CPU百分比
    id 空闲CPU百分比
    wa 等待输入/输出的CPU时间百分比
  • 在输入top后,再按大写字母H
    查看每个线程的性能信息,可能出现3中情况

    • 某个线程CPU利用率一直100%,说明这个线程有可能是死循环,需要记住PID作进一步分析。

      • 该情况可能由GC造成,可用jstat命令查看GC情况,看是否由与持久代或年老代满了,产生了Full GC, 导致CPU利用率持续居高不下。示例命令:jstat -gcutil 31177(lvmid, 本地jvm标识id,可用jps查看) 1000(统计间隔,ms) 5(下方打印个数),输出各个参数含义如下:

        参数 描述
        S0 年轻代中第一个survivor(幸存区)已使用的占当前容量百分比
        S1 年轻代中第二个survivor(幸存区)已使用的占当前容量百分比
        E 年轻代中Eden(伊甸园)已使用的占当前容量百分比
        O old代已使用的占当前容量百分比
        P perm代已使用的占当前容量百分比
        YGC 从应用程序启动到采样时年轻代中gc次数
        YGCT 从应用程序启动到采样时年轻代中gc所用时间(s)
        FGC 从应用程序启动到采样时old代(全gc)gc次数
        FGCT 从应用程序启动到采样时old代(全gc)gc所用时间(s)
        GCT 从应用程序启动到采样时gc用的总时间(s)
      • 还可以dump线程,分析究竟是哪个线程、执行什么代码造成的CPU利用率高。示例命令:jstack 31177 > ~/dump

      注意:dump出来的线程ID(nid)是16进制的,而top命令看到的是10进制的,所以对比时需要转换一下!

    • 某个线程一直在top10位置,说明这个线程可能存在性能问题

    • CPU利用率高的几个线程一直在变换,说明并不是一个线程导致的CPU利用率高

性能测试

常用命令

  • 查看有多少台机器连接到port端口上:netstat -nat | grep port -c,其中-n数字化显示localhost等信息,-a显示所有,-t表示tcp协议,grep port -c表示含有port的行数

  • 查看Java线程数量:ps -eLf | grep java -c,其中-e显示所有线程,-L显示轻量级或非轻量级线程,-f格式化显示

  • 查看网络流量:cat /proc/net/dev

  • 查看系统平均负载:cat /proc/loadavg

  • 查看系统内存情况:cat /proc/meminfo

  • 查看CPU利用率:cat /proc/stat

异步任务池

使用场景:如果一个任务扔进线程池之后,运行线程池的程序重启了,那么线程池中的任务将被丢失。另外,线程池只能处理本机任务,在集群环境下不能有效调用所有机器的任务。因此需要结合线程池开发一个异步任务处理池,设计图如下:

异步任务池设计图

任务状态有如下几种:

  • 创建:提交给任务池之后的状态
  • 执行中:任务池从数据库中拿到任务执行时的状态
  • 重试:执行任务出错时,程序显示性地告诉任务池需要重新执行,并设置下一次执行时间
  • 挂起:当一个任务需要依赖其他任务时,此时可将该任务挂起,等待依赖任务执行完毕,收到消息后继续执行
  • 终止:执行任务失败,让线程池停止执行这个任务,并设置错误消息告诉给调用端
  • 执行完成:任务执行结束

任务池的任务隔离:使用不同的线程池处理不同的任务,或不同线程池处理不同优先级的任务。如果任务类型非常少,建议采用任务类型隔离任务;如果任务类型非常多,比如几十个,简易采用优先级隔离任务。

任务池的重试策略:根据不同的任务类型设置不同的重试策略。对实时性要求不高的任务,可采用默认隔离策略。重试时间间隔随着次数的增加,时间不断增长,从几秒到几分钟再到几小时。每个任务类型可以设置执行该任务类型线程池的最大和最小线程数、最大重试次数。

使用任务池的注意事项任务必须无状态,即任务不能在执行任务的机器中保存数据。

  • OSS: Object Storage Service,对象文件存储
  • SFTP: SSH File Transfer Protocol,安全文件传送协议

异步任务的属性主要有:

  • 任务名称
  • 下次执行时间
  • 已执行次数
  • 任务类型
  • 任务优先级
  • 执行时的报错信息(用于快速定位问题)
码哥 wechat
欢迎关注个人订阅号:「码上行动GO」