Java线程池的四种用法与使用场景

目录

 

一、如下方式存在的问题

二、使用线程池有什么优点

三、线程池的四种使用方式

四、ThreadPoolExecutor线程池类简介

五、线程池的作用


一、如下方式存在的问题

new Thread() {
    @Override
    public void run() {
        // 业务逻辑
    }
}.start();
复制代码

  • 首先频繁的创建、销毁对象是一个很消耗性能的事情;
  • 如果用户量比较大,导致占用过多的资源,可能会导致我们的服务由于资源不足而宕机;
  • 综上所述,在实际的开发中,这种操作其实是不可取的一种方式。

二、使用线程池有什么优点

  • 线程池中线程的使用率提升,减少对象的创建、销毁;
  • 线程池可以控制线程数,有效的提升服务器的使用资源,避免由于资源不足而发生宕机等问题;

三、线程池的四种使用方式

1、newCachedThreadPool

  • 创建一个线程池,如果线程池中的线程数量过大,它可以有效的回收多余的线程,如果线程数不足,那么它可以创建新的线程。
public static void method() throws Exception {

    ExecutorService executor = Executors.newCachedThreadPool();

    for (int i = 0; i < 5; i++) {

        final int index = i;

        Thread.sleep(1000);

        executor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "  " + index);
            }
        });
    }
}
复制代码

执行结果

image

通过分析我看可以看到,至始至终都由一个线程执行,实现了线程的复用,并没有创建多余的线程。

如果当我们的业务需要一定的时间进行处理,那么将会出现什么结果。我们来模拟一下。

image

可以明显的看出,现在就需要几条线程来交替执行。

  • 不足:这种方式虽然可以根据业务场景自动的扩展线程数来处理我们的业务,但是最多需要多少个线程同时处理缺是我们无法控制的;
  • 优点:如果当第二个任务开始,第一个任务已经执行结束,那么第二个任务会复用第一个任务创建的线程,并不会重新创建新的线程,提高了线程的复用率;

2、newFixedThreadPool

这种方式可以指定线程池中的线程数。举个栗子,如果一间澡堂子最大只能容纳20个人同时洗澡,那么后面来的人只能在外面排队等待。如果硬往里冲,那么只会出现一种情景,摩擦摩擦...

首先测试一下最大容量为一个线程,那么会不会是我们预测的结果。

public static void method_01() throws InterruptedException {

    ExecutorService executor = Executors.newFixedThreadPool(1);

    for (int i = 0; i < 10; i++) {

        Thread.sleep(1000);
        final int index = i;

        executor.execute(() -> {
            try {
                Thread.sleep(2 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "  " + index);
        });
    }
    executor.shutdown();
}
复制代码

执行结果

image

我们改为3条线程再来看下结果

image

优点:两个结果综合说明,newFixedThreadPool的线程数是可以进行控制的,因此我们可以通过控制最大线程来使我们的服务器打到最大的使用率,同事又可以保证及时流量突然增大也不会占用服务器过多的资源。

3、newScheduledThreadPool

该线程池支持定时,以及周期性的任务执行,我们可以延迟任务的执行时间,也可以设置一个周期性的时间让任务重复执行。 该线程池中有以下两种延迟的方法。

  • scheduleAtFixedRate

测试一

public static void method_02() {
    ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);

    executor.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            long start = new Date().getTime();
            System.out.println("scheduleAtFixedRate 开始执行时间:" +
                    DateFormat.getTimeInstance().format(new Date()));
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long end = new Date().getTime();
            System.out.println("scheduleAtFixedRate 执行花费时间=" + (end - start) / 1000 + "m");
            System.out.println("scheduleAtFixedRate 执行完成时间:" + DateFormat.getTimeInstance().format(new Date()));
            System.out.println("======================================");
        }
    }, 1, 5, TimeUnit.SECONDS);
}
复制代码

执行结果

image

测试二

image

总结:以上两种方式不同的地方是任务的执行时间,如果间隔时间大于任务的执行时间,任务不受执行时间的影响。如果间隔时间小于任务的执行时间,那么任务执行结束之后,会立马执行,至此间隔时间就会被打乱。

  • scheduleWithFixedDelay

测试一

public static void method_03() {
    ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);

    executor.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            long start = new Date().getTime();
            System.out.println("scheduleWithFixedDelay 开始执行时间:" +
                    DateFormat.getTimeInstance().format(new Date()));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long end = new Date().getTime();
            System.out.println("scheduleWithFixedDelay执行花费时间=" + (end - start) / 1000 + "m");
            System.out.println("scheduleWithFixedDelay执行完成时间:"
                    + DateFormat.getTimeInstance().format(new Date()));
            System.out.println("======================================");
        }
    }, 1, 2, TimeUnit.SECONDS);
}
复制代码

执行结果

image

测试二

public static void method_03() {
    ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);

    executor.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            long start = new Date().getTime();
            System.out.println("scheduleWithFixedDelay 开始执行时间:" +
                    DateFormat.getTimeInstance().format(new Date()));
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long end = new Date().getTime();
            System.out.println("scheduleWithFixedDelay执行花费时间=" + (end - start) / 1000 + "m");
            System.out.println("scheduleWithFixedDelay执行完成时间:"
                    + DateFormat.getTimeInstance().format(new Date()));
            System.out.println("======================================");
        }
    }, 1, 2, TimeUnit.SECONDS);
}
复制代码

执行结果

image

总结:同样的,跟scheduleWithFixedDelay测试方法一样,可以测出scheduleWithFixedDelay的间隔时间不会受任务执行时间长短的影响。

4、newSingleThreadExecutor

这是一个单线程池,至始至终都由一个线程来执行。

public static void method_04() {

    ExecutorService executor = Executors.newSingleThreadExecutor();

    for (int i = 0; i < 5; i++) {
        final int index = i;
        executor.execute(() -> {
            try {
                Thread.sleep(2 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "   " + index);
        });
    }
    executor.shutdown();
}
复制代码

执行结果

image

四、ThreadPoolExecutor线程池类简介

线程池类为 java.util.concurrent.ThreadPoolExecutor,常用构造方法为:

Java代码 :

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler)  

corePoolSize:        线程池维护线程的最少数量 (core : 核心)
maximumPoolSize:线程池维护线程的最大数量
keepAliveTime:     线程池维护线程所允许的空闲时间
unit:           线程池维护线程所允许的空闲时间的单位
workQueue: 线程池所使用的缓冲队列
handler:      线程池对拒绝任务的处理策略

一个任务通过 execute(Runnable)方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是 Runnable类型对象的run()方法。

当一个任务通过execute(Runnable)方法欲添加到线程池时:

如果线程池中运行的线程 小于corePoolSize ,即使线程池中的线程都处于空闲状态,也要 创建新的线程 来处理被添加的任务。

如果线程池中运行的线程大于等于corePoolSize,但是缓冲队列 workQueue未满 ,那么任务被放入缓冲队列 。

如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满(即无法将请求加入队列 ),并且线程池中的数量小于maximumPoolSize,建新的线程 来处理被添加的任务。

如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize ,那么通过 handler 所指定的策略来处理此任务。
当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止 。这样,线程池可以动态的调整池中的线程数。

 

也就是:处理任务的优先级为:
corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。

Java代码 :

unit可选的参数为java.util.concurrent.TimeUnit中的几个静态属性:  
NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。  
  
workQueue常用的是:  
       java.util.concurrent.ArrayBlockingQueue  
  
handler有四个选择:  
ThreadPoolExecutor.AbortPolicy()  
      //抛出java.util.concurrent.RejectedExecutionException异常  
ThreadPoolExecutor.CallerRunsPolicy()  
     //重试添加当前的任务,他会自动重复调用execute()方法  
ThreadPoolExecutor.DiscardOldestPolicy()  
     //抛弃旧的任务  
ThreadPoolExecutor.DiscardPolicy()  
    // 抛弃当前的任务  

五、线程池的作用

线程池的作用主要是为了提升系统的性能以及使用率。文章刚开始就提到,如果我们使用最简单的方式创建线程,如果用户量比较大,那么就会产生很多创建和销毁线程的动作,这会导致服务器在创建和销毁线程上消耗的性能可能要比处理实际业务花费的时间和性能更多。线程池就是为了解决这种这种问题而出现的。

同样思想的设计还有很多,比如数据库连接池,由于频繁的连接数据库,然而创建连接是一个很消耗性能的事情,所有数据库连接池就出现了。

 

以下为重要分析:

到此如果有很多疑问,那是必然了(除非你也很了解了)

 

先从BlockingQueue <Runnable > workQueue这个入参开始说起。在JDK中,其实已经说得很清楚了,一共有三种类型的queue。以下为引用:(我会稍微修改一下,并用红色突出显示)

  

所有 BlockingQueue 都可用于传输和保持提交的任务。可以使用此队列与池大小进行交互:

  • 如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。(什么意思?如果当前运行的线程小于corePoolSize ,则任务根本不会存放,添加到queue中 ,而是直接 抄家伙(thread)开始运行 )
  • 如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列 ,而不添加新的线程 。
  • 如果无法将请求加入队列,则创建新的线程 ,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。

先不着急举例子,因为首先需要知道队列的三种类型 。

 

排队有三种通用策略:

  1. 直接提交。 工作队列的默认选项是 SynchronousQueue ,它将任务直接提交给线程而不保持它们 。在此,如果不存在可用于立即运行任务的线程 ,则试图把任务加入队列将失败,因此会构造一个新的线程 。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务 。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
  2. 无界队列。 使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue )将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize 。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
  3. 有界队列。 当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue )有助于防止资源耗尽 ,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。  

 

到这里,该了解的理论已经够多了,可以调节的就是corePoolSize和maximumPoolSizes 这对参数还有就是BlockingQueue的选择。

 

例子一:使用直接提交策略,也即SynchronousQueue。

 

首先SynchronousQueue是无界的,也就是说他存数任务的能力是没有限制的,但是由于该Queue本身的特性 ,在某次添加元素后必须等待其他线程取走后才能继续添加 。在这里不是核心线程便是新创建的线程,但是我们试想一样下,下面的场景。

 

我们使用一下参数构造ThreadPoolExecutor:

Java代码 

new ThreadPoolExecutor(    
            2, 3, 30, TimeUnit.SECONDS,     
            new <span style="white-space: normal;">SynchronousQueue</span><Runnable>(),     
            new RecorderThreadFactory("CookieRecorderPool"),     
            new ThreadPoolExecutor.CallerRunsPolicy());    

 Java代码 

 收藏代码

new ThreadPoolExecutor(  
            2, 3, 30, TimeUnit.SECONDS,   
            new <span style="white-space: normal;">SynchronousQueue</span>  
  
  
  
  
  
<Runnable>(),   
            new RecorderThreadFactory("CookieRecorderPool"),   
            new ThreadPoolExecutor.CallerRunsPolicy());  

 当核心线程已经有2个正在运行.

 

  1. 此时继续来了一个任务(A),根据前面介绍的“如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列 ,而不添加新的线程 。”,所以A被添加到queue中。
  2. 又来了一个任务(B),且核心2个线程还没有忙完,OK,接下来首先尝试1中描述,但是由于使用的SynchronousQueue,所以一定无法加入进去。
  3. 此时便满足了上面提到的“如果无法将请求加入队列,则创建新的线程 ,除非创建此线程超出maximumPoolSize,在这种情况下,任务将被拒绝。”,所以必然会新建一个线程来运行这个任务。
  4. 暂时还可以,但是如果这三个任务都还没完成,连续来了两个任务,第一个添加入queue中,后一个呢?queue中无法插入,而线程数达到了maximumPoolSize,所以只好执行异常策略了。

所以在使用SynchronousQueue通常要求maximumPoolSize是无界的,这样就可以避免上述情况发生(如果希望限制就直接使用有界队列)。对于使用SynchronousQueue的作用jdk中写的很清楚:此策略可以避免在处理可能具有内部依赖性的请求集时出现锁 。

 

什么意思?如果你的任务A1,A2有内部关联,A1需要先运行,那么先提交A1,再提交A2,当使用SynchronousQueue我们可以保证,A1必定先被执行,在A1么有被执行前,A2不可能添加入queue中

 

例子二:使用无界队列策略,即LinkedBlockingQueue 

这个就拿newFixedThreadPool 来说,根据前文提到的规则:

 写道

如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。

 那么当任务继续增加,会发生什么呢? 

如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。

 OK,此时任务变加入队列之中了,那什么时候才会添加新线程呢? 

 写道

如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。

这里就很有意思了,可能会出现无法加入队列吗?不像SynchronousQueue那样有其自身的特点,对于无界队列来说,总是可以加入的(资源耗尽,当然另当别论)。换句说,永远也不会触发产生新的线程! corePoolSize大小的线程数会一直运行,忙完当前的,就从队列中拿任务开始运行。所以要防止任务疯长,比如任务运行的实行比较长,而添加任务的速度远远超过处理任务的时间,而且还不断增加,如果任务内存大一些,不一会儿就爆了,呵呵。

 

可以仔细想想哈。

 

例子三:有界队列,使用ArrayBlockingQueue。

 

这个是最为复杂的使用,所以JDK不推荐使用也有些道理。与上面的相比,最大的特点便是可以防止资源耗尽的情况发生。 

举例来说,请看如下构造方法:

 

Java代码 

 收藏代码

new ThreadPoolExecutor(    
            2, 4, 30, TimeUnit.SECONDS,     
            new ArrayBlockingQueue<Runnable>(2),     
            new RecorderThreadFactory("CookieRecorderPool"),     
            new ThreadPoolExecutor.CallerRunsPolicy());   

 Java代码 

 收藏代码

new ThreadPoolExecutor(  
            2, 4, 30, TimeUnit.SECONDS,   
            new ArrayBlockingQueue<Runnable>(2),   
            new RecorderThreadFactory("CookieRecorderPool"),   
            new ThreadPoolExecutor.CallerRunsPolicy());  

假设,所有的任务都永远无法执行完。

 

对于首先来的A,B来说直接运行,接下来,如果来了C,D,他们会被放到queu中,如果接下来再来E,F,则增加线程运行E,F。但是如果再来任务,队列无法再接受了,线程数也到达最大的限制了,所以就会使用拒绝策略来处理。

 

总结:

  1. ThreadPoolExecutor的使用还是很有技巧的。
  2. 使用无界queue可能会耗尽系统资源。
  3. 使用有界queue可能不能很好的满足性能,需要调节线程数和queue大小
  4. 线程数自然也有开销,所以需要根据不同应用进行调节。

通常来说对于静态任务可以归为:

  1. 数量大,但是执行时间很短
  2. 数量小,但是执行时间较长
  3. 数量又大执行时间又长
  4. 除了以上特点外,任务间还有些内在关系

看完这篇问文章后,希望能够可以选择合适的类型了

 

参考:http://dongxuan.iteye.com/blog/901689

http://www.cnblogs.com/jersey/archive/2011/03/30/2000231.html

https://juejin.im/post/5df5a44e6fb9a0161711b55a?utm_source=gold_browser_extension#heading-2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java线程池在实际应用中非常常见,以下是一些常见的线程池应用实例: 1. Web服务器:在Web服务器中,可以使用线程池来处理客户端请求。每当有一个请求到达时,可以将其封装成一个任务提交给线程池线程池会自动分配线程来处理请求,从而提高服务器的并发性能。 2. 文件下载器:在文件下载器中,可以使用线程池来同时下载多个文件。每个文件可以作为一个独立的任务提交给线程池线程池会自动创建并管理多个线程来并发下载文件,加快下载速度。 3. 数据库连接池:在使用数据库连接时,可以使用线程池来管理数据库连接。通过将每个数据库操作封装成一个任务提交给线程池线程池可以管理连接的创建和释放,避免频繁地创建和关闭数据库连接,提高数据库操作的效率。 4. 定时任务调度:在定时任务调度中,可以使用线程池来执行定时任务。可以将每个定时任务封装成一个任务提交给线程池线程池会根据设定的时间间隔自动执行任务,实现定时任务的调度功能。 5. 并行计算:在需要进行大量计算的场景下,可以使用线程池来进行并行计算。将计算任务分解成多个子任务,每个子任务作为一个独立的任务提交给线程池线程池会自动创建并管理多个线程来并行执行计算任务,提高计算速度。 这些只是一些常见的应用实例,实际上线程池Java开发中的应用非常广泛,可以根据实际需求灵活地运用线程池来提高程序的性能和并发处理能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值