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中的线程是内核空间,协程是用户空间