换个角度看Java的Threa

在Java的世界中,新开一个线程很容易,你只需要用下面的代码。

1
2
Thread thread = new Thread(() -> {while(true) {doSomeThing...}});
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.html
https://blog.csdn.net/rickiyeat/article/details/78906366

NIO配置: 自己使用Netty来实现

public class NettyHttpController {

    public void run() throws InterruptedException {

        // accept 线程池,默认为1
        EventLoopGroup boss = new NioEventLoopGroup();
        // selector 线程池,
        EventLoopGroup worker = new NioEventLoopGroup(Integer CPU_Num);

        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(boss, worker);
        bootstrap.channel(NioServerSocketChannel.class);

        bootstrap.childHandler(new ChannelInitializer<Channel>() {
            @Override
            protected void initChannel(Channel channel) throws Exception {
                ChannelPipeline channelPipeline = channel.pipeline();
                
                // 业务线程池,伪代码,这里需要开发考虑
                channel.add(new Executor(() -> {
                    doService();
                    ....
                    channel.respnse("调用成功");
                }));
            }
        });

        ChannelFuture channelFuture = bootstrap.bind(9999).sync();
    }

    public static void main(String[] args) throws InterruptedException {
        new NettyHttpController().run();
    }

}

线程模型

如果不考虑网络IO等因素,只考虑多线程业务系统的情况,又该如何处理线程池呢?是需要模仿NIO对线程进行分块处理还是怎么样呢?

在代码世界中,如果语言能力比不上人家,那么构建一个好的模型,同样能够击败对手。在《七周七并发》这本书中就推荐了7种并发模型。比较类似的,就是CSP模型(管道模型)和Actor模型,使用Go和Java分别能很好的实现这两者。

CSP模型

  1. 接收int 数组,并返回一个channel

    func gen(nums ...int) <-chan int {
        out := make(chan int)
        go func() {
            for _, n := range nums {
                out <- n
            }
            close(out)
        }()
        return out
    }
  2. 从channel中接收数据,并进行2次方,并放到一个新的channel

    func sq(in <-chan int) <-chan int {
        out := make(chan int)
        go func() {
            for n := range in {
                out <- n * n
            }
            close(out)
        }()
        return out
    }
  3. 来看例子一: 如何使用刚刚的两个channel

  4. func main() {
        c := gen(2, 3)      // 函数一
        out := sq(c)        // 函数二
    
        // 消费输出结果
        fmt.Println(<-out) // 4
        fmt.Println(<-out) // 9
    }
  5. 例子二: channel和Actor不同的是,channel关注channel,而不是接收消息的主体。因此,你还可以将一个channel发送给多个函数消费。

    func main() {
        in := gen(2, 3)
    
        // 启动两个 sq 实例,即两个goroutines处理 channel "in" 的数据
        c1 := sq(in)
        c2 := sq(in)
    
        // merge 函数将 channel c1 和 c2 合并到一起,这段代码会消费 merge 的结果
        for n := range merge(c1, c2) {
            fmt.Println(n) // 打印 4 9, 或 9 4
        }
    }

Actor模型

不同于channel,Actor模型更加类似人类世界的交互模式。任何的交互都是异步的,都需要容忍失败。

public class Hello1 {

    public static void main(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();
    }
 
    private static class Hello extends UntypedActor {
    	
        public void onReceive(Object message) throws Exception {
            if (message instanceof String) {
                System.out.println("Hello " + message);
            }
        }
    }
}

对于我而言,Actor更类似于将消息系统引入到了单机业务中来,他更面向于各种复杂的情况。更类似一个完整的业务情况。

总结

本文从Java线程的实现,到NIO模式线程池配置,再到并发模型,来讲解了如何使用多线程。可以看到一个同步线程是最最简单,但是对于CPU而言,浪费类很多调度时间。如果对业务进行抽象,合理进行建模。无论是针对业务,还是针对系统性能,都能有很大的提升。

转自:https://runningegg.cn/2018/12/22/%E6%8D%A2%E4%B8%AA%E8%A7%92%E5%BA%A6%E7%9C%8BJava%E7%9A%84Thread/#more

猜你喜欢

转载自blog.csdn.net/pythias_/article/details/86217191