多线程实践

任务执行

1、线程中执行任务
1.1、串行地执行任务

在应用程序中存在多种调度策略,最简单的一种是在单个线程中串行地执行各个任务。

    class SingleThreadWebServer{
        public static void main(String[] args)throws IOException{
            ServerSocked socket = new ServerSocket(80);
            while(true){
                Socket connection = socket.acept();
                handleRequest(connection);
            }
        }
    }

这种调度策略在访问量较低时,时可以应付的。但是这种机制无法提供高吞吐率和快速响应性。

1.2、为任务创建线程

通过为每一个请求任务创建一个新的线程来提供服务,从而实现更高的响应性。

    class SingleThreadWebServer{
        public static void main(String[] args)throws IOException{
            ServerSocked socket = new ServerSocket(80);
            while(true){
                final Socket connection = socket.accept();
                Runnable task = new Runnable(){
                    public void run(){
                        handleRequest(connection);
                    }
                }
                new Thread(task).start();
            }
        }
    }

在正常负载下,“为每个任务分配一个线程”的方法能提升串行执行的性能。只要请求的到达率不超过服务器的请求处理能力,那么这种方法就可以带来更高的吞吐率和更快的响应性。

1.3、无线创建线程的代价
  1. 线程生命周期的开销非常高(新线程将消耗大量的计算资源)
  2. 资源消耗(消耗系统资源,尤其是内存)
  3. 稳定性(最大线程数量,不同的平台可能不同)

综上所述,虽然效果有改进,但是都不尽人意。

2、Executor框架

Executor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程相当于消费者。

2.1、示例:基于Executor的Web服务器

基于前边的代码,这里创建一个固定长度的线程池,可以容纳100个线程。

    class TaskExecutionWebServer{
        private static fianl int NTHREADS = 100
        private static fianl Executor exec = Executor.newFixedThreadPool(NTHREADS);
        
        public static void main()throws IOExeception{
            ServerSocket socket = new ServerSocket(80);
            while(true){
                final Socket connection = socket.accept();
                Runnable task = new Runnable(){
                    public void run(){
                         handleRequest(connection);
                    }
                };
                exec.execute(task);
            }
        }
    }

线程池是指管理一组同构工作线程的资源池。工作队列中保存着所有等待执行的任务,工作者线程的任务很简单:从工作队列中获取一个任务,执行任务,人后返回线程池并等待下一个任务。这里的线程是重复利用的,不是每次新建的。

创建线程池的方法:

  • newFixedThreadPool 创建固定长度的线程池。
  • newCachedThreadPool 创建可缓存的线城市
  • newSingleThreadExecutor创建单个Executor
  • newScheduledThreadPool 创建固定长度的线程池,以延迟或者定时的方式执行任务。
2.2、执行策略
  • 在什么(what)线程中执行。
  • 任务按照什么(what)顺序执行(FIFO?)
  • 有多少个(how many)任务能并发执行。
  • 在队列中有多少个(how many)任务等待执行。
  • 如果系统过载需要拒绝任务,那么应该选择(which)哪一个任务?如何(how)通知应用程序有任务被拒绝。
  • 在一个任务执行之前和之后应该进行哪些(what)动作。
2.3、生命周期

由于Executor以异步方式来执行任务,因此在任何时刻,之前提交的任务的转台不是立即可见的。有些任务可能已经完成,有些可能正在执行,而其他的任务可能在队列中等待执行。当应用程序关闭时,可能采用最平缓的关闭方式(完成所有已经启动的任务,并且不再接受任何新的任务),也可能采用最粗暴的方式(关闭电源),为了解决生命周期的问题,Executor扩展了ExecutorService接口,添加了一下生命周期的管理方法。具体的可以查看ExecutorService。

    //平缓的关闭方式
    void shutdown();
    //粗暴的关闭方式
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();//轮训是否终止
    boolean awaitTermination(long timeout,TimeUnit unit);
    ....
    
2.4、周期任务

Timer类负责管理延迟任务以及周期任务,但是存在一些缺陷,应该考虑使用ScheduledThreadPoolExecutor来替代它。如果要构建自己的调度服务,那么可以使用DelayQueue。它实现了BlockingQueue,并为ScheduledThreadPoolExecutor提供调度功能。

3、优化实例

假设现在有一个页面需要我们去渲染,这个页面包含HTML标签,预定大小的图片和URL。

3.1、串行页面渲染

最简单的处理方法就是对HTMl页面进行串行处理。

    public class SingleThreadRenderer{
        void renderPage(CharSquence source){
            renderText(source);
            List<ImageData> imageData = new ArrayList<ImageData>();
            for(ImageInfo imageInfo : scanForImageInfo(source)){
                imageData.add(imageInfo.downloadImage());
            }
            for(ImageData data:imageData){
                renderImage(data);
            }
        }
    }

这种方式的大部分时间都是在等待I/O操作执行完成,CPU在这期间几乎不做任何工作。因此如果能够分开执行,将会获得更高的效率。

3.2、携带结果的Callable和Future

Executor框架中,已提交但未开始的任务可以取消,但是对于那些已经开始执行的任务,只有他们能响应中断时,才能取消。

为了使页面的渲染速度提高,首先将渲染过程分解为两个任务,一个是渲染所有的文本,另一个是下载所有的图像。(因为一个是CPU密集型,一个是I/O密集型)Callable和Future有助于表示这些协同人物之间的交互

    public class FutureRenderer{
        private final ExecutorService executor =...;
        
        void renderPage(CharSequence source){
            final List<ImageInfo> imageInfos = scanForImageInfo(source);
            Callable<List<ImageData>> task = 
                new Callable<List<ImageData>>() {
                    public List<ImageData> call(){
                        List<ImageData> result = new ArrayList<ImageData>();
                        for(ImageInfo imageInfo:imageInfos){
                            result.add(imageInfo.downloadImage())
                        }
                        return result;
                    }
                }
                
            Future<List<ImageData>> future = executor.submit(task)
            renderTask(source);
            
            try{
                //如果任务完成,get会抛出异常;如果没完成,get将阻塞。
                List<ImageData> imageData = future.get();
                for(ImageData:imageData)
                    renderImage(data);
            }catch(InterruptedException e){
                //重新设置线程中断状态
                Thread.currentThread().interrupt();
                //由于不需要结果,取消任务
                future.cancel(true);
            }catch(Exeception e){
            
            }
        }
    }
3.3、CompletionService

如果Executor提交一组任务,希望得到计算结果,可以保留与每个任务关联的Future,然后反复使用get,同时将timeout指定为0,通过轮询判断任务是否完成,但是这种凡是过于繁琐,这里哟更好的方法。

CompletionService将Executor和BlockingQueue融合在一起。你可以将Callablle任务提交给他,然后使用队列操作的take方法和poll等方法来获取已经完成的结果。

总结

Executor框架将任务提交与执行策略解耦开来,同时还支持多种不同类型的执行策略。当需要创建线程来执行任务时,可以考虑使用Executor。要想在将应用程序分解为不同的任务时获得最大好处,必须定义清晰的任务边界。

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

推荐阅读更多精彩内容

  • 第6章介绍了任务执行框架, 它不仅能简化任务与线程的生命周期管理, 而且还提供一 种简单灵活的方式将任务的提交与任...
    好好学习Sun阅读 1,169评论 0 2
  • 一.线程安全性 线程安全是建立在对于对象状态访问操作进行管理,特别是对共享的与可变的状态的访问 解释下上面的话: ...
    黄大大吃不胖阅读 836评论 0 3
  • Java线程池 [toc] 什么是线程池 线程池就是有N个子线程共同在运行的线程组合。 举个容易理解的例子:有个线...
    石家志远阅读 1,308评论 0 6
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,094评论 1 32
  • 以前经常使用 notepad++ ,据说通过插件可以支持代码跳转,我试过,并不好用。而且notepad++设置黑色...
    荒原狼的博客阅读 695评论 0 0