在Java的世界中,新开一个线程很容易,你只需要用下面的代码。
1.Thread thread =newThread(() -> {while(true) {doSomeThing...}});2.thread.run();
在深入一点,你应该知道Thread.run()调用了Linux的fork()函数,从父线程中copy出一个一摸一样的子线程,然后开出了一个新的线程。
但是呢?在深入思考一下,提一个问题:
为啥说Go可以随意Goroutine几百几万个?而Java确不建议开那么多的线程,都是建议使用线程池来处理问题?或者说代码里面随意的使用new Thread()可以吗?
答案就是: Go提供的线程是协程是语言层面的,线程切换不必牵涉CPU上下文切换(而且还提供了Forkjoin机制,当有单个协程阻塞,可以分配),Java的线程使用的是Linux的fork(),会涉及CPU上下文切换。
关于协程
关于Go的协程,更加细致的介绍可以看这两篇文章:
https://blog.csdn.net/truexf/article/details/50510073
https://www.jianshu.com/p/533d58970397
可以看到,由于Java的线程切换,涉及从用户态切换到内核态,多了一步操作。如果Java的线程过多就会造成CPU频繁上下文切换,浪费额外时间。所以,开发协程API对于语言来说还是有不小的价值。(是不是觉得Java有点弱,协程都不支持。)
因此在Java世界还是需要合理的使用线程,无论是GC线程还是业务线程。比较经典的例子就是,web服务器的线程池配置。当我们知道Java开了过多的线程之后反而会减低性能,那么我们的web服务器应该如何调优线程池配置?
以NIO模型为例,分为Accept、Selector、还有业务线程,这里的线程池该如何分配呢?
NIO配置: Jetty
全局使用一个线程池QueuedThreadPool,而最小线程数8最大200,Acceptor线程默认1个,Selector线程数默认2个
NIO配置: undertow
undertow 配置的是 Acceptor 递归也就是线程数 1,IO worker是CPU核数,而工作线程数是CPU * 8;
参考:https://www.cnblogs.com/maybo/p/7784687.htmlhttps://blog.csdn.net/rickiyeat/article/details/78906366
NIO配置: 自己使用Netty来实现
publicclassNettyHttpController{publicvoidrun()throwsInterruptedException{// accept 线程池,默认为1EventLoopGroup boss =newNioEventLoopGroup();// selector 线程池,EventLoopGroup worker =newNioEventLoopGroup(Integer CPU_Num);ServerBootstrap bootstrap =newServerBootstrap();bootstrap.group(boss, worker);bootstrap.channel(NioServerSocketChannel.class);bootstrap.childHandler(newChannelInitializer() {@OverrideprotectedvoidinitChannel(Channel channel)throwsException{ChannelPipeline channelPipeline = channel.pipeline();// 业务线程池,伪代码,这里需要开发考虑channel.add(newExecutor(() -> {doService();....channel.respnse("调用成功");}));}});ChannelFuture channelFuture = bootstrap.bind(9999).sync();}publicstaticvoidmain(String[] args)throwsInterruptedException{newNettyHttpController().run();}}
线程模型
如果不考虑网络IO等因素,只考虑多线程业务系统的情况,又该如何处理线程池呢?是需要模仿NIO对线程进行分块处理还是怎么样呢?
在代码世界中,如果语言能力比不上人家,那么构建一个好的模型,同样能够击败对手。在《七周七并发》这本书中就推荐了7种并发模型。比较类似的,就是CSP模型(管道模型)和Actor模型,使用Go和Java分别能很好的实现这两者。
CSP模型
接收int 数组,并返回一个channel
funcgen(nums ...int)<-chanint{out :=make(chanint)gofunc(){for_, n :=rangenums {out <- n}close(out)}()returnout}
从channel中接收数据,并进行2次方,并放到一个新的channel
funcsq(in <-chanint)<-chanint{ out :=make(chanint)gofunc(){forn :=rangein { out <- n * n }close(out) }()returnout}
来看例子一: 如何使用刚刚的两个channel
funcmain() {c :=gen(2, 3) // 函数一 out :=sq(c) // 函数二 // 消费输出结果 fmt.Println(<-out) //4fmt.Println(<-out) //9}
例子二: channel和Actor不同的是,channel关注channel,而不是接收消息的主体。因此,你还可以将一个channel发送给多个函数消费。
funcmain() {in :=gen(2, 3) // 启动两个 sq 实例,即两个goroutines处理 channel"in"的数据 c1 :=sq(in) c2 :=sq(in) // merge 函数将 channel c1 和 c2 合并到一起,这段代码会消费 merge 的结果 for n := rangemerge(c1, c2) { fmt.Println(n) // 打印49, 或94}}
Actor模型
不同于channel,Actor模型更加类似人类世界的交互模式。任何的交互都是异步的,都需要容忍失败。
publicclassHello1{publicstaticvoidmain(String[] args){ ActorSystem system = ActorSystem.create("actor-demo-java"); ActorRef hello = system.actorOf(Props.create(Hello.class));// 需要定义接收者hello.tell("Bob", ActorRef.noSender());// 有一个线程单独处理,都是异步的,需要容忍失败try{ Thread.sleep(1000); }catch(InterruptedException e) {/* ignore */} system.shutdown(); }privatestaticclassHelloextendsUntypedActor{publicvoidonReceive(Object message)throwsException{if(messageinstanceofString) { System.out.println("Hello "+ message); } } }}
对于我而言,Actor更类似于将消息系统引入到了单机业务中来,他更面向于各种复杂的情况。更类似一个完整的业务情况。
总结
本文从Java线程的实现,到NIO模式线程池配置,再到并发模型,来讲解了如何使用多线程。可以看到一个同步线程是最最简单,但是对于CPU而言,浪费类很多调度时间。如果对业务进行抽象,合理进行建模。无论是针对业务,还是针对系统性能,都能有很大的提升。
当真正开始学习的时候难免不知道从哪入手,导致效率低下影响继续学习的信心。
但最重要的是不知道哪些技术需要重点掌握,学习时频繁踩坑,最终浪费大量时间,所以有一套实用的视频课程用来跟着学习是非常有必要的。
为了让学习变得轻松、高效,今天给大家免费分享一套阿里架构师传授的一套教学资源。帮助大家在成为架构师的道路上披荆斩棘。
这套视频课程详细讲解了(Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构)等这些成为架构师必备的内容!
而且还把框架需要用到的各种程序进行了打包,根据基础视频可以让你轻松搭建分布式框架环境,像在企业生产环境一样进行学习和实践。
资源大放送!
可以直接加群960439918获取免费架构资料
希望能帮助到那些工作了的朋友在学习能提供一些帮助。有需要这些免费架构资料和面试题资料的可以加群:960439918获取哦!点击链接加入群聊【java高级架构交流群】:https://jq.qq.com/?_wv=1027&k=5fozFzF
网友评论