java中用Executor代替Thread的四大理由

Executor VS Thread

在组建java程序时候关于任务执行方面第一个要考虑的就是任务边界的划分,即把执行的程序分成合理的任务,理想的情况是这些任务的状态,结果不会影响到其它任务。

以java最擅长的服务器端开发为例子,服务器端要做到两点:高并发(good throughput)、低延迟(good responsiveness)。高并发是服务提供者的需要,我们想让我们的程序承载更多的用户来分担服务器开销;低延迟是用户的需求,用户想尽快的看到服务器对请求的响应结果。同样服务器的任务划分也是比较清晰和明确的:我们可以把用户的一个请求,作为一个任务。在高并发、低延迟的要求下,我们来设计下服务器的并发框架。
首先能想到的是单线程模式,程序如下:

public class SingleThreadWebServer{
    public static void main(String[] args) throws Exception{
        ServerSocket server = new ServerSocket(8900);

        while(true){
            Socket socket = server.accept(); 
            handlerRequest(socket);
        }
    }
}

这个程序中,每个客户端请求和其处理都在一个程序中执行,在handlerRequest方法比较耗时的情况下,用户将感觉到卡顿,没能做到低延迟;同时因为同一时间只能有一个客户端请求被处理,服务器也没能做到高并发。在真实的生产环境中,除了GUI程序,基本上不会用到这种模型框架。

因为handlerRequest方法可能造成延迟,我们改进成程序如下:

public class ThreadPerTaskWebServer{
    public static void main(String[] args){
        ServerSocket serverSocket = new ServerSocket();
        while(true){
            Socket socket = serverSocket.accpet();
            new Thread(){
                @Override
                public void run(){
                    handlerReqeust(socket);
                }
            }.start();
        }
    }
}

此模式下将任务接受和任务执行分别放在不同的线程中来执行,main方法所在的线程主要负责接受客户端请求,具体的任务执行放在单独的一个新的线程中来执行。此模式可以很好的满足服务器高并发,低延迟的设计需求。但还是存在三个缺点:
1、线程生命周期成本太大(thread lifecycle overhead):此模式中,每个请求都要创建一个新的处理线程,当任务处理完毕后,此线程还要被销毁。但是线程的创建和销毁并不是没有成本的。线程的创建需要向jvm申请一些资源,这就给处理客户端请求带来了一些延迟。如果需要此线程处理的任务都是一些短时间的任务,那么线程的创建时间将会称为主要的延迟。
2、资源的消耗严重(Resource consumption):线程是需要消耗资源的,比如内存。当线程的数量超过了处理器的核数,多余的线程将阻塞,这将造成资源的浪费同时给gc造成极大的压力,同时线程之间竞争CPU也会造成一些性能上的牺牲。
3、影响程序稳定性(stability):程序能创建多少线程是有限制的,如果没有限制的创建过多的线程,很可能会得到OutOfMemoryError。

造成上述问题的根本原因是系统没有对程序中可创建的线程数量做限制,对上述程序改进如下:

public class TaskExecutionWebServer{
    private static Executor executor = new Exectutors.newFixedThreadPool(100); //1
    public static void main(String[] args){
        ServerSocket serverSocket = new ServerSocket(9090);
        while(true){
            Socket socket = serverSocket.accpet();
            Runnable task = new Runnable(){
                public void run(){
                    handlerRequest(socket);
                }
            }
            executor.execute(task);
        }
    }
}

Executor 是一个接口,其只有一个方法execute。execute方法用来执行Runnable实例的,我们只需要把我们的handlerRequest方法的方法封装在一个Runnable实例中,然后把这个任务提交给Executor,具体的任务运行管理,交给Executor即可。同时我们在//1处可以看到我们最多可以创建
的线程数量被限定为100个。这就没有了ThreadPerTaskWebServer中的无限制创建线程的问题了。同时把任务的提交和任务的执行解耦还给我们带来了灵活的执行任务策略。我们可以很容易的把执行策略编程单线程模式,只需要把 //1处 exectuor变成一个OneThreadExecutor实例:

public class OneThreadExecutor implements Executor {
    public void execute(Runnable task){
        task.run();
  }
}

我们还可以把执行策略变成我们之前的一个任务一个线程的模式,只需要把 //1处 executor变成ThreadPerTaskExecutor实例:

public class ThreadPerTaskExecutor implements Executor{
    public void execute(Runnable task){
        new Thread(task).start();
    }
}

使用Executor接口来执行任务,可以让我们在获得了高并发,低延迟的前提下,又有了很大的灵活性,很方便的更改执行策略。但是考虑一个程序完整的生命周期,不应该只有执行状态,还应该有关闭状态。当程序要关闭的时候,Executor应该能对其做出相应的响应,所以一个Executor也应该有自己的生命周期。为了描述Executor的生命周期,我们引进了ExecutorService接口,其继承了Executor接口。一个ExecutorService实例有三种状态:running、shutting down、terminated。ExecutorService实例在刚刚创建的时候就处在running状态;当调用shutdown方法的时候,ExecutorService实例会把状态置为shutting down,同时把正在执行的任务继续执行,当通过execute方法向其中添加新的任务时,ExecutorService实例可以忽略新添加的任务,或者抛出一个异常,具体取决于策略的选取,这在以后再说。实例中没有正在执行的任务的时候,实例就进入terminated状态。一个具有生命周期管理的WebServer服务器类如下:

public class LifeCycleWebServer{
    private final ExecutorService exec = ....;

    public static void main(String[] args) throws Exception{
        ServerSocket socket = new ServerSocket(9090);
        while(!exec.isShutdown()){
            Socket socket = socket.accpet();
            exec.execute(new Runnable(){
                    public void run(){
                        if(isShutdownRequest(socket)){
                            exec.shutdown();
                        }

                        handleRequest(socket);
                    }
                };)
        }
    }
}

在LifeCycleWebServer类中,我们可以通过一个特殊的客户端请求来让ExecutorService进入关闭状态。这样ExecutorService实例也有了生命周期,更能适合真实的开发需求。

通过以上比较可知,用Executor框架执行任务的好处如下:
1、能够实现高并发和低延迟。
2、因为Executor的实现类可以很好的管理其线程的创建数量,可能任务提交给Executor的时候,已经有可以执行此任务的线程了,这样减少了
线程创建带来的延迟。因为Executor实例限制了,创建线程的数量,减少了线程之间对CPU的竞争,也在一定程度上提高了系统性能。对线程创建数量的限制,保证了系统的稳定性。
3、更加灵活的执行策略,因为Executor实现了任务的提交和执行的解耦,可以让我们灵活的更换任务的执行策略。
4、继承Executor接口的ExecutorService增加了生命周期的管理,更加贴近真实的生产开发。

综上所述,在java中执行任务首要考虑的是Executor,而不是Thread.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,494评论 18 139
  • 一.线程安全性 线程安全是建立在对于对象状态访问操作进行管理,特别是对共享的与可变的状态的访问 解释下上面的话: ...
    黄大大吃不胖阅读 811评论 0 3
  • 第一章 人类心灵与行动学 第一节 导论 1.认识论的永恒基础 人性,在我们现在所生活的这个宇宙变化时代的人性,既不...
    湖北立春阅读 1,401评论 4 6
  • 1.有生之年狭路相逢终不能幸免,手心忽然长出纠缠的曲线。——王菲《流年》 2.尘埃落定之后,回忆别来挑拨。——刘若...
    LYQ1024阅读 1,103评论 5 23