Java并发(三) 线程池详解

  • 为什么要有线程池?

如果没有线程池,我们执行100个任务是这样的:

for (int i = 0; i < 100; i++) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("执行任务");
        }
    }).start();
}

每执行一个任务我们就需要创建一个线程,线程频繁的创建和销毁非常消耗性能,那能不能线程执行完先找个地方存着,等下次任务再出来执行呢?这样就可以解决线程频繁创建销毁而浪费性能的问题了。

  • 线程池怎么用?

Java内部已经为我们提供好了线程池,但是很多人一看到要传这么多参数直接放弃了…

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

没关系,Java早已经为我们封住好了4个线程池工厂,我们直接用就行,终于不用传一堆参数了:

创建带缓存的线程池
ExecutorService executorService = Executors.newCachedThreadPool();

创建固定数量的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);

创建单一线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();

创建定时调度线程池
ExecutorService executorService = Executors.newScheduledThreadPool(5);

可是… 有4个我们应该使用哪个呢?哪个又更适合我们使用呢?我们随便点进去一个看看源码:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

内部还是调用了ThreadPoolExecutor并且传了很多参数,看来我们还是得搞懂这几个参数的含义才行,不然乱用线程池可能会导致效率更低。

注意:阿里巴巴Java开发手册明确禁止使用Executors来创建线程池。

原文如下:
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 返回的线程池对象的弊端如下:
FixedThreadPoolSingleThreadPool : 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。CachedThreadPoolScheduledThreadPool : 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
  • ThreadPoolExecutor参数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
参数用途
corePoolSize核心线程数
maximumPoolSize最大线程数
keepAliveTime非核心线程数没有任务时的存活时间,超出时间自动销毁
unit存活时间单位
workQueue任务阻塞队列
threadFactory线程工厂
handler拒绝策略

怎么样?看完是不是还是很懵逼?😏

  • 举个栗子

我们要去银行取钱,银行最多只有5个窗口,但是正常只开3个,所以有2个总是暂停服务,如果办理业务的人多了,就需要在等候区等着。
在这里插入图片描述
假如我们来了5个用户办理业务,只有3个窗口可以同时办理,所以剩余的两个人需要在等候区等待
在这里插入图片描述

假如我们来了7个用户办理业务,只有3个窗口可以同时办理,等候区最多只能容纳3个人,还多1个人,这个时候银行经理看到今天人有点多,只能打电话让窗口4的休假员工喊回来上班了
在这里插入图片描述

假如我们来了9个用户办理业务,就算窗口5也同时打开了,但是还是无法容纳所有用户,银行经理只能看情况解决这部分多出来的用户了,可能把多出来的用户赶出去,也可能自己帮这个用户办理业务也可能是别的情况
在这里插入图片描述

现在业务都办理的差不多了,窗口4和窗口5也闲了一会了,银行经理又让窗口4和窗口5下班了,毕竟本来他们今天就是休假。
在这里插入图片描述

  • 银行代入线程池

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

核心线程:corePoolSize

窗口1、窗口2和窗口3就是核心线程,不管有没有任务都不会被销毁

最大线程:maximumPoolSize

窗口4和窗口5就是最大线程和核心线程的差值,如果阻塞队列满了,就会查看当前是否达到了最大线程数,如果没有达到就继续开启新的线程来处理任务,直到达到最大线程数

阻塞队列:workQueue

等候区就是阻塞队列,如果核心线程比较忙,任务会先进入阻塞队列等待

保持可用时间:keepAliveTime

窗口4和窗口5如果一段时间没有处理任务了就会下班,因为他们今天本来就休假,也就是说如果非核心线程一段时间没有处理任务了,就会自动销毁,而keepAliveTime就是用来设置这个时间的,超过这个时间线程就会自动销毁了

时间单位:unit

keepAliveTime的时间单位。

线程工厂:threadFactory

用户创建线程的工厂,通常使用默认的就可以,如果不满足要求也可以自定义。

拒绝策略:handler

如果所有窗口都正在办理业务,等候区也满了,这个时候只能看银行经理怎么处理了,也就是任务把阻塞队列占满了,线程也已经达到最大线程数了,这个时候就会走拒绝策略,系统已经给我们提供好了几个拒绝策略,我们也可以自定义。

  • 代码实战

现在我们按照上面的银行的图创建一个银行线程池,核心线程3个,最大线程5个,阻塞队列最大容量3,然后我们添加5个任务来看一下:

public class ThreadDemo {

    private static int corePoolSize = 3; //核心线程数
    private static int maximumPoolSize =  5; //最大线程数
    private static long keepAliveTime = 10; //非核心线程数没有任务时的存活时间,超出时间自动销毁
    private static TimeUnit unit = TimeUnit.SECONDS; //存活时间单位
    private static BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(3); //任务阻塞队列
    private static ThreadFactory threadFactory = Executors.defaultThreadFactory(); //线程工厂
    private static RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); //拒绝策略
    private static ArrayList<String> mList = new ArrayList<>(); //任务列表

    public static void main(String[] args) {
        //创建5个任务
        for (int i = 0; i < 5; i++) {
            mList.add("任务" + i);
        }

        ThreadPoolExecutor executor =
                new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
                    unit, workQueue, threadFactory, handler);

        for (String s : mList) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " 执行了-> " + s);
                }
            });
        }
        executor.shutdown();
    }
}

执行结果:
pool-1-thread-1 执行了-> 任务0
pool-1-thread-3 执行了-> 任务2
pool-1-thread-1 执行了-> 任务3
pool-1-thread-3 执行了-> 任务4
pool-1-thread-2 执行了-> 任务1

执行结果刚好对应了我们上面的图,窗口1,2,3把5个任务全部执行了。

现在我们换成7个任务看一下:

//创建7个任务
for (int i = 0; i < 7; i++) {
    mList.add("任务" + i);
}
        
pool-1-thread-1 执行了-> 任务0
pool-1-thread-3 执行了-> 任务2
pool-1-thread-2 执行了-> 任务1
pool-1-thread-3 执行了-> 任务4
pool-1-thread-2 执行了-> 任务5
pool-1-thread-1 执行了-> 任务3
pool-1-thread-4 执行了-> 任务6

我们可以看到,窗口4也开始执行任务了

那我们把任务变成20个看一下:

//创建20个任务
for (int i = 0; i < 20; i++) {
    mList.add("任务" + i);
}
        
pool-1-thread-1 执行了-> 任务0
pool-1-thread-3 执行了-> 任务2
pool-1-thread-2 执行了-> 任务1
pool-1-thread-3 执行了-> 任务4
pool-1-thread-1 执行了-> 任务3
pool-1-thread-4 执行了-> 任务6
pool-1-thread-1 执行了-> 任务9
pool-1-thread-5 执行了-> 任务7
pool-1-thread-3 执行了-> 任务8
pool-1-thread-2 执行了-> 任务5
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task senior.thread.ThreadDemo$1@266474c2 rejected from java.util.concurrent.ThreadPoolExecutor@6f94fa3e[Running, pool size = 5, active threads = 4, queued tasks = 0, completed tasks = 6]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
	at senior.thread.ThreadDemo.main(ThreadDemo.java:37)

任务变成20个直接抛异常了,因为我们代码拒绝策略设置的就是如果任务处理不过来就直接抛异常

private static RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); //拒绝策略
  • 拒绝策略

ThreadPoolExecutor.AbortPolicy()

当任务添加到线程池中被拒绝时,它将抛出 RejectedExecutionException 异常

ThreadPoolExecutor.CallerRunsPolicy()

当任务被拒绝添加后,会在调用execute方法的的线程来执行被拒绝的任务,除非executor被关闭,否则任务不会被丢弃。

ThreadPoolExecutor.DiscardOldestPolicy()

当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中

ThreadPoolExecutor.DiscardPolicy()

当任务添加到线程池中被拒绝时,线程池将丢弃被拒绝 的任务。

  • 关闭线程池

shutdown()

shutdown并不是直接关闭线程池,只是将线程池的状态设置为SHUTWDOWN状态,不再接受新的任务,正在执行的任务会继续执行下去,没有被执行的则中断。

shutdownNow()

shutdownNow是将线程池的状态设置为STOP,正在执行的任务被停止,没被执行的任务返回。

  • Executors

虽然阿里巴巴不建议我们使用官方提供的工厂,但是我们还是要学习一下

  • 创建带缓存的线程池:Executors.newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

核心线程是0,最大线程是Integer的最大值,超时等待60秒,在执行新的任务时,当线程池中有之前创建的可用线程就重用可用线程,否则就新建一条线程。

  • 创建固定数量的线程池:Executors.newFixedThreadPool(5)
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

核心线程和最大线程相等,都是我们通过nThreads传过来的,可控制线程最大并发数,超出的线程会在队列中等待。

  • 创建单一线程池:Executors.newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

核心线程和最大线程数都是1,如果该线程因为异常而结束就新建一条线程来继续执行后续的任务。

  • 创建定时调度线程池:newScheduledThreadPool(5)
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

创建一个支持定时及周期性的任务执行的线程池。

  • 总结

  1. 线程池可以降低资源的消耗,避免频繁的创建销毁线程。
  2. 提高执行任务的响应速度,任务执行时不用等线程创建可以直接取线程池中已经创建好的线程使用。
  3. 提高线程的管理性,可以统一管理线程。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值