热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java并发编程笔记4线程池

我们使用线程的时候就去创建一个线程,但是就会有一个问题:如果并发的线程数量非常多,而且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会导致大大降低系统的效率,因为频

我们使用线程的时候就去创建一个线程,但是就会有一个问题:

  如果并发的线程数量非常多,而且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会导致大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

 

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

线程池正好能解决这样的问题。正如名称所称的那样,线程池管理一个工作者线程的同构池。线程池是与工作队列紧密绑定的。所谓工作队列,其作用是持有所有等待执行的任务。

工作者线程的生活从此轻松起来:它从工作队列中获取下一个任务,执行它,然后回来等待另外一个线程。

这类似于企业应用程序中事务监听器(transaction monitor)的角色:它将课运行事务的数量控制在一个合理的水平中,不会因过渡滥用事务而耗尽有限资源。

  线程池中执行任务线程,这方法有很多“每任务每线程”无法笔记的优势。重用存在的线程,而不是创建新的线程,这可以在处理多请求时抵消线程创建,消亡产生的开销。还有一个好处就是,在请求到达时,工作者线程通常已经存在

,用于创建线程的等待时间并不会延迟任务的执行,因此提高响应性。通过适当地调整线程池的大小,你可以得到足够多的线程以保持处理器忙碌,同时可以还防止过多的线程互相竞争资源,导致应用程序耗尽内存或者失败。

 

每任务每线程例子如下:

public class ThreadPool {

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

    private static void handleRequest(Socket socket) {
    }

}

可以看到这个例子是一个粗制滥造的并发服务端,来一个用户就创建一个线程,你根本就不知道有多少用户来,要创建多少个线程。这样频繁创建线程就会导致大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,

过渡滥创建线程而耗尽有限资源。

 

由于这原因,java中给我们提供Executor框架。通过Executors中的某个静态工厂方法来创建一个线程池:

  1.newFixedThreadPool 创建一个定长的线程池,每当提交一个任务就创建一个线程,知道达到池的最大长度,这时线程池会保持长度不再变化(如果一个线程由于非预期的Exception而结束,线程池会补充一个新的线程)。

  下面用newFixedThreadPool 创建一个定长的线程池来改造上面的例子,如下:

public class ThreadPool {

    public static void main(String[] args) throws IOException {
        //newFixedThreadPool参数为线程池的大小
        Executor executor = Executors.newFixedThreadPool(100);
        
        ServerSocket serverSocket = new ServerSocket(80);
        while (true){
            final Socket socket = serverSocket.accept();
            Runnable task = new Runnable() {
                public void run() {
                    handleRequest(socket);
                }
            };
            //直接将任务丢进线程池来执行任务
            executor.execute(task);
        }
    }

    private static void handleRequest(Socket socket) {
    }

}

这样就不会发生过渡滥创建线程而耗尽有限资源。

 

  

  2.newSingleThreadExecutor创建一个单线程化的executor,他只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它。executor会保证任务依照任务队列所规定的顺序(FIFO,LIFO,优先级)执行。

 

 

  3.newCachedThreadPool创建一个可缓存的线程池,如果当前线程的长度超过了处理的需要,它可以灵活的回收空闲的线程,当需求增加时,它可以灵活的增加新的线程,并不会对池的长度做任何限制。但是认为改线程池的长度没有任何限制,有可能会把资源耗尽,

这需要自己很好的把控了。

 

  4.newScheduledThreadPool创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。

 

Executor的生命周期:

  Executor实现通常知识为执行任务而创建线程。但是JVM会在所有(非后台的,nondaemon)线程全部终止后才退出。因此,如果无法正确关闭Executor,将会阻止JVM的结束。

因为Executor是异步地执行任务,所以在任何时间里,所有之前提交的任务状态都不能立即可见。这些任务中,有些可能已经完成,有些可能正在运行,其他的还可能在队列中等待执行。关闭应用程序时,程序会出现很多中情况:从平缓关闭

到最突然的关闭,以及介于这两种阶段情况之间的各种可能。Executor是为应用服务提供服务的,他们理应可以关闭,无论是平缓还是突然。

注意:关闭操作还会影响到记录应用程序任务状态的反馈信息。

 

Executor就是一个接口,源码如下图:

 

我们可以进入Executors这个类的源码,如下:

可以看到newFixedThreadPool 创建一个定长的线程池,返回的是一个ExecutorService,但是我们上面例子接收的是Executor,为什么Executor也可以接收呢?我们继续进入ExecutorService源码如下:

可以看到原来ExecutorService继承了Executor。ExecutorService扩展了Executor,并且添加了一些用于声明周期管理的方法。

源码如下:

 

 

 

 ExecutorService暗示了生命周期有3种状态:运行、关闭、终止。ExecutorService最初创建后的初始状态是运行状态。

shutdown方法会启动一个平缓的关闭过程:停止接受新的任务,同时等待已经提交的任务完成,包括尚未开始执行的任务。

shutdownNow方法会启动要给强制的关闭过程:尝试取消所有运行中的任务和排在队列中尚未开始执行的任务is。

isShutdown方法:判断线程池(即ExecutorService)是否关闭。

isTerminated方法:是线程池(即ExecutorService)是否进入终止状态。

  在关闭后提交到ExecutorService中的任务,会被拒绝执行处理器(rejected execution handler)处理。拒绝执行处理器是ExecutorService的一种实现,ThreadPoolExecutor提供的,ExecutorService接口中的方法并不提供拒绝执行处理器。拒绝执行处理器可能只是

简单的放弃任务,也可能会引起execute抛出一个未检查的RejectedExecutionException。一旦所有任务全部完成后,ExecutorService会转入终止状态。

 

awaitTermination方法:等待ExecutorService到达终止状态。

 

通常shutdown会紧随awaitTermination之后,这样可以产生同步地关闭ExecutorService的效果。

 

上面的Executor的例子的程序是没办法关闭线程池,会一直跑下去,那么我们如何写一个支持关闭的webserver呢?

明显我们现在要用ExecutorService来改造上面的Executor的例子。伪代码如下:

public class ThreadPool {
    private static ExecutorService executorService = Executors.newCachedThreadPool();
    public static void main(String[] args) throws IOException {

        //newFixedThreadPool参数为线程池的大小

        ServerSocket serverSocket = new ServerSocket(80);
        //这里就不再像上面的例子一样无限的接受任务了,要根据我的线程池是否处于关闭状态来决定
        while (!executorService.isShutdown()){
            final Socket socket = serverSocket.accept();
            try{
                executorService.execute(new Runnable() {
                    public void run() {
                        handleRequest(socket);
                    }
                });
            }catch (RejectedExecutionException e){
                //如果拒绝服务不是因为我线程池关闭导致的,我们要在这里打印一下日志
                if (!executorService.isShutdown()){
                    System.out.println("接受任务被拒绝");
                    throw e;
                }
            }
        }
    }

    //用一个公共的方法去关闭线程池
    public void stop(){
        executorService.shutdown();
    }


    private static void handleRequest(Socket socket) {
        //获取请求
        Request req = readRequest(socket);
        //如果请求已经关闭
        if (isShutdownRequest(req)){
            //关闭线程池
            stop();
        }else {
            //请求转发
            dispatchRequest(req);
        }
    }

}

经过改造,这服务端变的优雅多了。

 

 

延时的,并具有周期性的任务

  在newScheduledThreadPool出来之前我们一般会用Timer和TimerTask来做。Timer在JDK里面,是很早的一个API了。

但是Timer存在一些缺陷,Timer只创建唯一的线程来执行所有Timer任务。如果一个timer任务的执行很耗时,会导致其他TimerTask的时效准确性出问题。例如一个TimerTask每10秒执行一次,

而另外一个TimerTask每40ms执行一次,重复出现的任务会在后市的任务完成后快速连续的被调用4次,要么完全“丢失”4次调用。

Timer的另外一个问题在于,如果TimerTask抛出未检查的异常会终止timer线程。这种情况下,Timer也不会重新回复线程的执行了;它错误的认为整个Timer都被取消了。此时

已经被安排但尚未执行的TimerTask永远不会再执行了,新的任务也不能被调度了。

 

现在我们看一下Timer的例子,如下:

public class Shedule {
    private static long start;

    public static void main(String[] args) {
        TimerTask task = new TimerTask() {
            public void run() {
                System.out.println(System.currentTimeMillis()-start);
                try{
                    Thread.sleep(3000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        };

        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                System.out.println(System.currentTimeMillis()-start);
            }
        };

        Timer timer = new Timer();
        start = System.currentTimeMillis();
        //启动一个调度任务,1S钟后执行
        timer.schedule(task,1000);
        //启动一个调度任务,3S钟后执行
        timer.schedule(task1,3000);


    }

}

上面程序我们预想是第一个任务执行后,第二个任务3S后执行的,即输出一个1000,一个3000.

实际运行结果如下:

 

 实际运行结果并不如我们所愿。世界结果,是过了4S后才输出第二个任务,即4001约等于4秒。那部分时间时间到哪里去了呢?那个时间是被我们第一个任务的sleep所占用了。

现在我们在第一个任务中去掉Thread.sleep();这一行代码,运行是否正确了呢?如下:

public class Shedule {
    private static long start;

    public static void main(String[] args) {
        TimerTask task = new TimerTask() {
            public void run() {
                System.out.println(System.currentTimeMillis()-start);
               
            }
        };

        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                System.out.println(System.currentTimeMillis()-start);
            }
        };

        Timer timer = new Timer();
        start = System.currentTimeMillis();
        //启动一个调度任务,1S钟后执行
        timer.schedule(task,1000);
        //启动一个调度任务,3S钟后执行
        timer.schedule(task1,3000);


    }

}

运行结果如下:

可以看到确实是第一个任务过了1S后执行,第二个任务在第一个任务执行完后过3S执行了。

这就说明了Timer只创建唯一的线程来执行所有Timer任务。如果一个timer任务的执行很耗时,会导致其他TimerTask的时效准确性出问题

 

 Timer存在一些缺陷,因此你应该考虑使用ScheduledThreadPoolExecutor作为替代品。你可以通过构造函数或者通过newScheduledThreadPool工厂方法创建一个ScheduledThreadPoolExecutor。

如下:

public class Shedule {
    private static long start;

    private static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);

    public static void main(String[] args) {
        TimerTask task = new TimerTask() {
            public void run() {
                System.out.println(System.currentTimeMillis()-start);
                try{
                    Thread.sleep(3000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        };

        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                System.out.println(System.currentTimeMillis()-start);
            }
        };

        Timer timer = new Timer();
        start = System.currentTimeMillis();

        //TimeUnit.MILLISECONDS指定毫秒为单位
        executorService.schedule(task,1000, TimeUnit.MILLISECONDS);
        executorService.schedule(task1,3000, TimeUnit.MILLISECONDS);

    }

}

运行结果如下:

可以看到运行结果符合预期。

可以看到如果一个timer任务的执行很耗时(例如Thread.sleep),ScheduledThreadPoolExecutor并不会导致其他TimerTask的时效准确性出问题。

还可以看到,这两个TimerTask互不干扰。

 

互相干扰还有一个反面:

    如果TimerTask抛出未检查的异常会终止timer线程。这种情况下,Timer也不会重新回复线程的执行了;它错误的认为整个Timer都被取消了。此时

    已经被安排但尚未执行的TimerTask永远不会再执行了,新的任务也不能被调度了。

例子如下:

public class Shedule {
    private static long start;

    private static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);

    public static void main(String[] args) {
        TimerTask task = new TimerTask() {
            public void run() {
                System.out.println(System.currentTimeMillis()-start);
               throw new RuntimeException();
            }
        };

        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                System.out.println(System.currentTimeMillis()-start);
            }
        };

        Timer timer = new Timer();
        start = System.currentTimeMillis();

        timer.schedule(task,1000);
        timer.schedule(task1,3000);
        

    }

}

如果第一TimerTask出现未知异常,第二个TimerTask还能运行起来吗?

结果如下:

明显第一TimerTask出现未知异常,第二个TimerTask不能运行起来了。这就说明

如果TimerTask抛出未检查的异常会终止timer线程。这种情况下,Timer也不会重新回复线程的执行了;它错误的认为整个Timer都被取消了。此时

已经被安排但尚未执行的TimerTask永远不会再执行了,新的任务也不能被调度了。

 

ScheduledThreadPoolExecutor可以解决此问题,例子如下:

public class Shedule {
    private static long start;

    private static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);

    public static void main(String[] args) {
        TimerTask task = new TimerTask() {
            public void run() {throw new RuntimeException();
            }
        };

        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                System.out.println(System.currentTimeMillis()-start);
            }
        };

        Timer timer = new Timer();
        start = System.currentTimeMillis();

        //TimeUnit.MILLISECONDS指定毫秒为单位
        executorService.schedule(task,1000, TimeUnit.MILLISECONDS);
        executorService.schedule(task1,3000, TimeUnit.MILLISECONDS);


    }

}

运行结果如下:

可以看到第一个线程挂了,第二个线程并没有受到影响。这就说明了ScheduledThreadPoolExecutor可以解决了

如果TimerTask抛出未检查的异常会终止timer线程。这种情况下,Timer也不会重新回复线程的执行了;它错误的认为整个Timer都被取消了。此时

    已经被安排但尚未执行的TimerTask永远不会再执行了,新的任务也不能被调度了的问题

 

还要注意一点,Timer是和系统时间挂钩的,如果当前服务器的时间一改,Timer就不那么靠谱了。

 

 

还要注意的是ThreadPoolExecutor。

源码如下图:

可以看到new一个FixedThreadPool/newSingleThreadExecutor/newCachedThreadPool/newScheduledThreadPool实际上返回的都是是new ThreadPoolExecutor()。

我们再看一下ThreadPoolExecutor源码如下:

可以看到ThreadPoolExecutor配置的非常灵活,如果我们用普通的一个FixedThreadPool/newSingleThreadExecutor/newCachedThreadPool/newScheduledThreadPool没办法满足你的需求了,你可以用

ThreadPoolExecutor灵活的指定参数来完成你的需求。这适合精确的任务执行。还不如说我们的任务被拒绝(RejecedExecutionHandler)后,我们可以用ThreadPoolExecutor灵活处理

 


推荐阅读
  • Oracle优化新常态的五大禁止及其性能隐患
    本文介绍了Oracle优化新常态中的五大禁止措施,包括禁止外键、禁止视图、禁止触发器、禁止存储过程和禁止JOB,并分析了这些禁止措施可能带来的性能隐患。文章还讨论了这些禁止措施在C/S架构和B/S架构中的不同应用情况,并提出了解决方案。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 本文介绍了在Windows环境下如何配置php+apache环境,包括下载php7和apache2.4、安装vc2015运行时环境、启动php7和apache2.4等步骤。希望对需要搭建php7环境的读者有一定的参考价值。摘要长度为169字。 ... [详细]
  • 本文比较了eBPF和WebAssembly作为云原生VM的特点和应用领域。eBPF作为运行在Linux内核中的轻量级代码执行沙箱,适用于网络或安全相关的任务;而WebAssembly作为图灵完备的语言,在商业应用中具有优势。同时,介绍了WebAssembly在Linux内核中运行的尝试以及基于LLVM的云原生WebAssembly编译器WasmEdge Runtime的案例,展示了WebAssembly作为原生应用程序的潜力。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • JavaScript设计模式之策略模式(Strategy Pattern)的优势及应用
    本文介绍了JavaScript设计模式之策略模式(Strategy Pattern)的定义和优势,策略模式可以避免代码中的多重判断条件,体现了开放-封闭原则。同时,策略模式的应用可以使系统的算法重复利用,避免复制粘贴。然而,策略模式也会增加策略类的数量,违反最少知识原则,需要了解各种策略类才能更好地应用于业务中。本文还以员工年终奖的计算为例,说明了策略模式的应用场景和实现方式。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
author-avatar
资深化妆师May
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有