JUC线程池的实战问题引出的一系列原理问题

1 我们为什么需要使用线程池

线程过多会带来额外的开销,其中包括创建销毁线程的开销调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待、监督、管理、分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。线程池是一种通过“池化”思想,帮助我们管理线程而获取并发性的工具,在Java中的体现是ThreadPoolExecutor类。

2 Executors [ɪgˈzɛkjətərz] 创建四种常见线程池

2.1 newFixedThreadPool

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

从构造方法可以看出,它创建了一个固定大小的线程池,每次提交一个任务就创建一个线程,直到线程数达到线程池的最大值nThreads。线程池的大小一旦达到最大值后,再有新的任务提交时则放入阻塞队列中,等到有线程空闲时,再从队列中取出任务继续执行。FixedThreadPool提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

package com.zs.thread;
public class TestVolatile {
    public static void main(String[] args) {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 5; i++) {
            final int index = i;
            fixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                        System.out.println("运行时间: " + sdf.format(new Date()) + " " + index);
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        fixedThreadPool.shutdown();
    }
}

例中创建了一个固定大小为3的线程池,然后在线程池提交了5个任务。在提交第4个任务时,因为线程池的大小已经达到了3并且前3个任务在运行中,所以第4个任务被放入了队列,等待有空闲的线程时再被运行。运行结果如下(注意前3个任务和后2个任务的运行时间):
在这里插入图片描述

2.2 newCachedThreadPool

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

从构造方法可以看出,它创建了一个可缓存的线程池。当有新的任务提交时,有空闲线程则直接处理任务,没有空闲线程则创建新的线程处理任务,队列中不储存任务。线程池不对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。如果线程空闲时间超过了60秒就会被回收。在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统OOM。

package com.zs.thread;

public class TestVolatile {
    public static void main(String[] args) {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

        for (int i = 0; i < 5; i++) {
            final int index = i;
            cachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        SimpleDateFormat sdf = new SimpleDateFormat(
                                "HH:mm:ss");
                        System.out.println("运行时间: " +
                                sdf.format(new Date()) + " " + index);
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        cachedThreadPool.shutdown();
    }
}

因为这种线程有新的任务提交,就会创建新的线程(线程池中没有空闲线程时),不需要等待,所以提交的5个任务的运行时间是一样的。
在这里插入图片描述

2.3 newSingleThreadExecutor

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

从构造方法可以看出,它创建了一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

package com.zs.thread;

public class TestVolatile {
    public static void main(String[] args) {
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 5; i++) {
            final int index = i;
            singleThreadExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        SimpleDateFormat sdf = new SimpleDateFormat(
                                "HH:mm:ss");
                        System.out.println("运行时间: " +
                                sdf.format(new Date()) + " " + index);
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        singleThreadExecutor.shutdown();
    }
}

因为该线程池类似于单线程执行,所以先执行完前一个任务后,再顺序执行下一个任务:
在这里插入图片描述
既然类似于单线程执行,那么这种线程池还有存在的必要吗?这里的单线程执行指的是线程池内部,从线程池外的角度看,主线程在提交任务到线程池时并没有阻塞,仍然是异步的。

2.4 newScheduledThreadPool

这个方法创建了一个固定大小的线程池,支持定时及周期性任务执行。

package com.zs.thread;

public class TestVolatile {
    public static void main(String[] args) {
        final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
        System.out.println("提交时间: " + sdf.format(new Date()));
        scheduledThreadPool.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("运行时间: " + sdf.format(new Date()));
            }
        }, 3, TimeUnit.SECONDS);
        scheduledThreadPool.shutdown();
    }
}

使用该线程池的schedule方法,延迟3秒钟后执行任务,运行结果如下:
在这里插入图片描述
再看一下周期执行的例子:

package com.zs.thread;

public class TestVolatile {
    public static void main(String[] args) throws InterruptedException {
        final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
        System.out.println("提交时间: " + sdf.format(new Date()));
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("运行时间: " + sdf.format(new Date()));
            }
        }, 1, 3, TimeUnit.SECONDS);
        Thread.sleep(10000);
        scheduledThreadPool.shutdown();
    }
}

使用该线程池的scheduleAtFixedRate方法,延迟1秒钟后每隔3秒执行一次任务,运行结果如下:
在这里插入图片描述

2. 5 Executors 各个方法的弊端:

在这里插入图片描述
阿里巴巴的Java操作手册中明确说明:对于Integer.MAX_VALUE初始值较大,所以一般情况我们要使用底层的ThreadPoolExecutor来创建线程池。

3 生命周期管理,线程池都有哪些状态?

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

任务调度

首先,所有任务的调度都是由execute方法完成的:(workerCount:前线程池的线程数,corePoolSize:基本大小线程数,maximumPoolSize:线程池中允许的最大线程数)

1 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。

2 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。

3 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。

4 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。

5 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

在这里插入图片描述

任务缓冲

线程池的本质是对任务和线程的管理:将任务和线程两者解耦,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列:在队列为空时,获取任务的线程会等待队列变为非空;当队列满时,存储任务的线程会等待队列可用。

使用不同的队列可以实现不一样的任务存取策略。阻塞队列的成员有以下:

在这里插入图片描述
任务申请

一种是:任务直接由新创建的线程执行。另一种是:线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。

任务拒绝

任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。

拒绝策略策略
AbortPolicy丢弃任务,并抛出异常:最大承载=maximumPoolSize + BlockingQueue
CallerRunsPolicy由提交任务的线程处理该任务
DiscardPolicy不处理新任务,直接丢弃掉
DiscardOldestPolicy丢弃队列最前面的任务,然后重新提交被拒绝的任务。

4 创建线程池的七大参数

参数特点
corePoolSize核心线程池大小:线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁。当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。
maximumPoolSize一个任务被提交到线程池以后,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
keepAliveTime一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁;如果allowCoreThreadTimeOut被设置为true时,无论线程数多少,线程处于空闲状态超过一定时间就会被销毁掉
TimeUnit unit超时单位
BlockingQueue workQueue用来保存等待被执行的任务的阻塞队列
ThreadFactory threadFactory为线程池提供创建新线程的线程工厂
RejectedExecutionHandler handler当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,会执行拒绝策略

5 你知道怎么创建线程池吗?

ThreadPoolExecutor() 是最原始的线程池创建,也是阿里巴巴 Java 开发手册中明确规范的创建线程池的方式。
在这里插入图片描述

public class Test {
    public static void main(String[] args) {
        // 获取cpu 的核数
        int max = Runtime.getRuntime().availableProcessors();
        ExecutorService service =new ThreadPoolExecutor(
                2,
                max,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
        try {
            for (int i = 1; i <= 10; i++) {
                service.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "ok");
                });
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            service.shutdown();
        }
    }
}

6 ThreadPoolExecutor

Java中的线程池核心实现类是ThreadPoolExecutor。ThreadPoolExecutor是如何运行,如何同时维护线程和执行任务的呢?
在这里插入图片描述
线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。
任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:
1 直接申请线程执行该任务;
2 缓冲到队列中等待线程执行;
3 拒绝该任务。

线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

7 如何优雅设置线程池的大小

线程池需要设置合适的大小,假如设置的太大,线程上线文切换过于频繁,造成大量资源开销,反而会使性能降低。假如设置的太小,存在很多可用的处理器资源却未在工作,会造成资源的浪费和对吞吐量造成损失。

为了充分利用处理器资源,创建的线程数至少要等于处理器核心数。如果所有的任务都是计算密集型的,那么线程数等于可用的处理器核心数就可以了。不过,如果所有的任务都是IO密集型,那么处理器大部分时间是空闲的,所以要适当的增加线程数。线程等待时间所占比例越高(IO密集型),需要越多线程。线程运算时间所占比例越高(计算密集型),需要越少线程。 于是可以使用下面的公式进行估算:

最佳线程数 =1 + 线程等待时间/线程计算时间)* 目标CPU的使用率 * 处理器核心数

例如:平均每个线程计算运行时间为0.5s,而线程等待时间(非计算时间,比如IO)为1.5s,目标CPU的使用率是90%,CPU核心数为8,那么根据上面这个公式估算得到:(1 + 1.5/0.5) * 90% * 8 = 28.8。

即使有上面的简单估算方法,也许看似合理,但实际上也未必合理,都需要结合系统真实情况(比如是IO密集型或者是CPU密集型或者是纯内存操作)和硬件环境(CPU、内存、硬盘读写速度、网络状况等)来不断尝试达到一个符合实际的合理估算值,也可以尝试Dark Magic的估算方法。

8 线程池为什么要使用阻塞队列而不使用非阻塞队列?

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。使得在线程不至于一直占用cpu资源。

9 线程池是如何保持核心线程不被摧毁呢?

1、客户端创建线程池对象后,调用execute() 提交一个Runnable任务;
2、execute()会调用addWorker() 创建一个Worker对象;
3、addWorker()内部会调用Worker.thread.start() 这时候实际调用的就是Worker对象内部的run方法;
4、Worker中的run方法委托给runWorker()执行;
5、runWorker()中有while循环体,不断地调用getTask()获取新任务;
6、在getTask()方法里它就是调用阻塞队列的poll()take()等待获取其中的任务,getTask()通过调用blockQueuetake()获取队列中的任务,如果队列为空,就一直阻塞当前线程,利用了阻塞队列的特性,实现核心线程空闲时间,也保持live;

package com.thread.pool;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

import static java.lang.Thread.sleep;

public class TestFixedThreadPool {
    public static void main(String[] args) {
        ExecutorService fixedThreadPool = getThreadPoolExecutorService_bk();
        fixedThreadPool.shutdown();
    }

    private static ExecutorService getFixedExecutorService() {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        IntStream.rangeClosed(1, 5).forEach(
                item -> fixedThreadPool.execute(() -> {
                        try {
                            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                            System.out.println("运行时间 - " + sdf.format(new Date()) + " " + item);
                            sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }));
        return fixedThreadPool;
    }

    private static ExecutorService getCachedExecutorService() {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        IntStream.rangeClosed(1, 10).forEach(
                item -> cachedThreadPool.execute(() -> {
                        try {
                            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                            System.out.println("运行时间 - " + sdf.format(new Date()) + " " + item);
                            sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }}));
        return cachedThreadPool;
    }

    private static ExecutorService getSingleExecutorService() {
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        IntStream.rangeClosed(1, 8).forEach(
                item -> singleThreadExecutor.execute(() -> {
                    try {
                        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                        System.out.println("运行时间 - " + sdf.format(new Date()) + " " + item);
                        sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }}));
        return singleThreadExecutor;
    }

    private static ExecutorService getScheduledExecutorService() {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        System.out.println("提交时间: " + sdf.format(new Date()));
        scheduledThreadPool.schedule(() ->
                System.out.println("运行时间: " + sdf.format(new Date())), 3, TimeUnit.SECONDS);

        scheduledThreadPool.scheduleAtFixedRate(() ->
                System.out.println("运行时间: " + sdf.format(new Date())), 1, 3, TimeUnit.SECONDS);

        return scheduledThreadPool;
    }

    private static ExecutorService getThreadPoolExecutorService() {
        int max = Runtime.getRuntime().availableProcessors();
        System.out.println(max);
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,
                max,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        IntStream.rangeClosed(1, 10).forEach(
                item -> threadPoolExecutor.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " ok " + sdf.format(new Date()));
                    try {
                        sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                })
        );
        threadPoolExecutor.shutdown();
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(threadPoolExecutor.getPoolSize());
        return threadPoolExecutor;
    }

    /**
     * 由提交任务的线程处理该任务 不会丢弃任务
     * @return
     */
    private static ExecutorService getThreadPoolExecutorService_bk() {
        int max = Runtime.getRuntime().availableProcessors();
        System.out.println(max);
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,
                2,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());
        IntStream.rangeClosed(1, 10).forEach(
                item -> CompletableFuture.supplyAsync(() -> {
                    System.out.println(Thread.currentThread().getName() + " ok " + sdf.format(new Date()));
                    try {
                        sleep(3000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    return null;
                }, threadPoolExecutor)
        );
        threadPoolExecutor.shutdown();
        return threadPoolExecutor;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值