Thread-Pre-Message设计模式

1.Thread-Pre-Message 设计模式

简单来说,就是为每个任务分配一个独立的线程,最简单的分工方法

2.并发领域3个核心问题

分工,同步,互斥

同步,互斥源自微观,分工来自宏观

Thread-Pre-Message其实就是一种分工模式

3.映射现实世界

教育小朋友搞不定,委托学校老师
忙着写Bug,没时间买别墅,委托房产中介

4.HttpServer

自编HttpServer,主线程中接受请求,不能处理http请求,如果处理,就同时只能处理一个请求,太慢了。代办思路,创建子线程,委托子线程去处理http请求

5.这种委托他人办理的方式,就是Thread-Pre-Message设计模式

6.伪代码


final ServerSocketChannel  = 
  ServerSocketChannel.open().bind(
    new InetSocketAddress(8080));
//处理请求    
try {
  while (true) {
    // 接收请求
    SocketChannel sc = ssc.accept();
    // 每个请求都创建一个线程
    new Thread(()->{
      try {
        // 读Socket
        ByteBuffer rb = ByteBuffer
          .allocateDirect(1024);
        sc.read(rb);
        //模拟处理请求
        Thread.sleep(2000);
        // 写Socket
        ByteBuffer wb = 
          (ByteBuffer)rb.flip();
        sc.write(wb);
        // 关闭Socket
        sc.close();
      }catch(Exception e){
        throw new UncheckedIOException(e);
      }
    }).start();
  }
} finally {
  ssc.close();
}   

客户端发送什么,服务端返回什么
但是上面伪代码不具有可行性,是因为java语言的问题,java中的线程是重量级的线程,创建成本高,而且创建线程比较耗时,而且占用内存也大。为每个请求创建一个新的线程也不适合高并发场景。

7.线程池优化思路

引入线程池会增加复杂度

8.java中的线程

java中的线程和操作系统是一一对应的,这种做法本质上是将java线程的调度权交给操作系统,操作系统在这方面比较成熟,好处是稳定,可靠。但是也继承了系统线程的缺点:创建成本高。
所以java并发包提供了线程池工具类。这个思路在很长的时间内都是很稳妥的方案。

9.轻量级线程-协程

GO语言,lua,本质就是轻量级线程,创建成本低,基本与创建对象成本相似。创建速度于内存占用相比较操作系统的线程来说有一个数量级提升,基于协程实现Thread-Per-Message 模式完全没问题。一个请求一个线程。频繁创建协程,销毁协程序,几万个协程都可以接受。

10.java的协程方案,Fiber(暂未商用,稳可靠性,稳定性还有待更多实践验证)

伪代码


final ServerSocketChannel ssc = 
  ServerSocketChannel.open().bind(
    new InetSocketAddress(8080));
//处理请求
try{
  while (true) {
    // 接收请求
    final SocketChannel sc = 
      ssc.accept();
    Fiber.schedule(()->{
      try {
        // 读Socket
        ByteBuffer rb = ByteBuffer
          .allocateDirect(1024);
        sc.read(rb);
        //模拟处理请求
        LockSupport.parkNanos(2000*1000000);
        // 写Socket
        ByteBuffer wb = 
          (ByteBuffer)rb.flip()
        sc.write(wb);
        // 关闭Socket
        sc.close();
      } catch(Exception e){
        throw new UncheckedIOException(e);
      }
    });
  }//while
}finally{
  ssc.close();
}

ulimit -u 512,修改用户能创建的最大进程数(包括线程)设置为512
使用ab压测,测试
ab -r -c 20000 -n 200000
-r 出错继续
-c 并发2万
-n 总共20万个请求

压测结果:


Concurrency Level:      20000
Time taken for tests:   67.718 seconds
Complete requests:      200000
Failed requests:        0
Write errors:           0
Non-2xx responses:      200000
Total transferred:      16400000 bytes
HTML transferred:       0 bytes
Requests per second:    2953.41 [#/sec] (mean)
Time per request:       6771.844 [ms] (mean)
Time per request:       0.339 [ms] (mean, across all concurrent requests)
Transfer rate:          236.50 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0  557 3541.6      1   63127
Processing:  2000 2010  31.8   2003    2615
Waiting:     1986 2008  30.9   2002    2615
Total:       2000 2567 3543.9   2004   65293

程序依然良好运行,同样使用new Thread()实现,512并发扛不住,就直接OOM了

11.并发编程分工问题

工具类中有

Future,CompletableFuture,CompletionService,Fork/Join

12.不需要并发读那么高的场景可以使用Thread-Pre-Message

例如定时任务

12.思考

Thread-Per-Message 为每个人物创建一个线程,高并发中,很容易OOM,那么怎么能快速解决?

(1)临时解决,修改jvm内存配置,增大jvm新生代大小
(2)长期解决,引入NIO,使用netty
(3)引入线程池控制,在请求端添加限流模块semaphore,自我保护
(4)tomcat用的线程池的思路解决高并发问题
(5)java中的线程是内核空间,协程是用户空间

上一篇:Java IO学习笔记二:DirectByteBuffer与HeapByteBuffer


下一篇:利用FDW进行ORACLE到Postgresql的数据迁移