JUC核心之线程池

1. 线程池的优势

线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
主要特点:线程复用; 控制最大并发数; 管理线程。

优势:

  • 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
  • 第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
  • 第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

2. Executor框架

Java中的线程池是通过Executor框架实现的,该框架中用到了下图标出的几个重要接口和类
在这里插入图片描述

创建线程池一:Executors.newSingleThreadExecutor()

通过工具类Executors的newSingleThreadExecutor()方法创建一个单线程(Single)的线程池

public static void main(String[] args) throws Exception {
        // 一池一线程
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        try {
            for (int i = 0; i < 10; i++) {
                final int temp = i + 1;
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "执行业务" + temp);
                });

            }
        } finally {
            // 启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务。 如果已经关闭,调用没有额外的作用。
            threadPool.shutdown();
        }
    }

执行结果:线程池中只有一个线程可以用,只能一个任务一个任务的执行
在这里插入图片描述

创建线程池二:Executors.newFixedThreadPool(nThreads)

传参为线程池中预创建固定(Fixed)的线程个数,执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有固定线程数的线程

public class Test {
    public static void main(String[] args) throws Exception {
        // 一池固定个数线程
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            final int temp = i + 1;
            threadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "执行业务" + temp);
            });
        }
    }
}

执行结果:同时有5个线程可以用,且线程得到了复用,比如线程pool-1-thread-3在执行完业务3之后又被获取出来执行业务6。
在这里插入图片描述

创建线程池三:Executors.Executors.newCachedThreadPool();

穿件缓存()线程池执行很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重用它们。可扩容,遇强则强

public class Test {
    public static void main(String[] args) throws Exception {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        try {
            for (int i = 0; i < 10; i++) {
                final int temp = i + 1;
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t执行业务" + temp);
                });
            }
        } finally {
            threadPool.shutdown();
        }
    }
}

执行结果:根据需要调整池中的线程数
在这里插入图片描述

创建线程池四:newScheduledThreadPool(int corePoolSize)

创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

Executors源码

在这里插入图片描述
底层都是使用ThreadPoolExecutor类创建线程池。进入ThreadPoolExecutor类的构造方法可以看到线程池的7个重要参数
在这里插入图片描述
在这里插入图片描述

ThreadPoolExecutor七大参数解读

  1. corePoolSize:线程池中的常驻核心线程数
  2. maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
  3. keepAliveTime:多余的空闲线程的存活时间,当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为止
  4. unit:keepAliveTime的单位
  5. workQueue:任务队列,被提交但尚未被执行的任务,这是一个阻塞队列,关于其的介绍移步JUC之阻塞队列BlockingQueue
  6. threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可
  7. handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数maximumPoolSize)时如何来拒绝请求执行的runnable的策略

3. 线程池原理

在这里插入图片描述
在这里插入图片描述

  1. 在创建了线程池后,开始等待请求。
  2. 当调用了execute()方法添加一个请求任务时,线程池会做出如下判断:
    1)如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
    2)如果正在运行的线程数量大于或者等于corePoolSize,那么这个任务将会放入队列;
    3)如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    4)如果队列满了且正在运行的线程数量大于或者等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行;
  4. 当一个线程无事可做超过一定时间(keepAliveTime)时,线程会判断:
    如果当前运行线程数大于corePoolSize,那么这个线程将会被停掉;
    所以线程池的所有任务完成后,它将会收缩到corePoolSize的大小;

4. Executors存在什么问题

实际生产中以上几种线程池的方式都不用,原因引用阿里的java
开发手册

在这里插入图片描述
在阿里巴巴Java开发手册中提到,使用Executors创建线程池可能会导致OOM(OutOfMemory ,内存溢出),但是并没有说明为什么,那么接下来我们就来看一下到底为什么不允许使用Executors?

我们先来一个简单的例子,模拟一下使用Executors导致OOM的情况

public class ExecutorsDemo {
    private static ExecutorService executor = Executors.newFixedThreadPool(15);
    public static void main(String[] args) {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            executor.execute(new SubThread());
        }
    }
}
 
class SubThread implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            //do nothing
        }
    }
}

通过指定JVM参数:-Xmx8m -Xms8m 运行以上代码,会抛出OOM:

Exception in thread “main” java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:16)

以上代码指出,ExecutorsDemo.java的第16行,就是代码中的executor.execute(new SubThread());。

Executors为什么存在缺陷
通过上面的例子,我们知道了Executors创建的线程池存在OOM的风险,那么到底是什么原因导致的呢?我们需要深入Executors的源码来分析一下。

其实,在上面的报错信息中,我们是可以看出蛛丝马迹的,在以上的代码中其实已经说了,真正的导致OOM的其实是LinkedBlockingQueue.offer方法。

Exception in thread “main” java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:16)

翻看代码的话,也可以发现,其实底层确实是通过LinkedBlockingQueue实现的:

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());

如果读者对Java中的阻塞队列有所了解的话,看到这里或许就能够明白原因了。

Java中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueue 和 LinkedBlockingQueue。

ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。

LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。

这里的问题就出在:**不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。**也就是说,如果我们不设置LinkedBlockingQueue的容量的话,其默认容量将会是Integer.MAX_VALUE。

而newFixedThreadPool中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。

上面提到的问题主要体现在newFixedThreadPool和newSingleThreadExecutor两个工厂方法上,并不是说newCachedThreadPool和newScheduledThreadPool这两个方法就安全了,这两种方式创建的最大线程数可能是Integer.MAX_VALUE,而创建这么多线程,必然就有可能导致OOM。

5. 创建线程池的正确姿势

避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。

private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
        60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue(10));

这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。

除了自己定义ThreadPoolExecutor外。还有其他方法。这个时候第一时间就应该想到开源类库,如apache和guava等。

推荐使用guava提供的ThreadFactoryBuilder来创建线程池。

public class ExecutorsDemo {
 
    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("demo-pool-%d").build();
 
    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
 
    public static void main(String[] args) {
 
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            pool.execute(new SubThread());
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值