JUC 线程池

线程池

普通线程池的设计思想

在完成某一个功能实现的时候,需要产生多个任务协作来完成,如果每个任务都使用一个线程来解决,每次创建线程以及线程的上下文切换都是需要消耗资源的;尤其是不可能永无止境的创建新线程,因为cpu 的核心数目也是有限的;使用享元设计模式,使得创建出来有限的线程构成线程池,使得多个任务共享使用线程池,可以减少系统的开销;

在这里插入图片描述

基于线程池的运行流程大致为:
1、相关方法产生任务需要运行,将任务放置到任务队列中;
2、线程池中的有限线程拿到任务并且执行;
可以使用消费者模式对于任务的添加以及消耗进行理解;

Blocking Queue 是阻塞队列,可以平衡产生的任务(可以设置阻塞队列的长度);
在没有任务的时候,线程池里面的线程可以在 Blocking Queue 里面进行等待;当任务太多的时候,线程池里面的线程处理不了,此时也是可以放在 Blocking Queue 里面进行等待的;

JDK 里面的线程池实现

JDK 线程池状态

在这里插入图片描述
RUNNABLE 代表的是一个负数,所以在数字大小的比较中是最小的;

为什么线程状态信息,以及线程数量信息要保存在一个 int 中?

因为这样可以保证原子性,使用一次 cas 操作就可以操作赋值,不需要两次的 cas 操作,所以使用一个 int 更好一些;
在这里插入图片描述

ThreadPoolExecutor 构造方法 十分重要


    /**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @param handler the handler to use when execution is blocked
     *        because the thread bounds and queue capacities are reached
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue}
     *         or {@code threadFactory} or {@code handler} is null
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

关于上面的ThreadPoolExecutor 构造方法的七个参数的注释,方便理解(来自参数的英语解释的翻译)

参数含义
corePoolSize保留在池中的线​​程数,即使它们是空闲的,除非设allowCoreThreadTimeOut
maximumPoolSize线程池中的最大线程数
keepAliveTime救急线程在终止前等待新任务的最长时间
unitkeepAliveTime 参数的时间单位
workQueue保存没有执行的任务的队列 用于在执行任务之前保存任务的队列。此队列将仅保存由execute方法提交的Runnable任务
threadFactory执行器创建新线程时使用的工厂
handler由于达到线程边界和队列容量而阻塞执行时使用的处理程序

模拟线程池的运行步骤

下面的图示中 corePoolSize = 2;
maxmuismPoolSize = 3;
救急线程 = 3 - 2 = 1;(救急线程在任务阻塞队列满了之后才会启动使用,生命周期可以自己设计)
在这里插入图片描述

存在救急线程的执行流程:
1、线程池刚开始是没有线程的,当第一个任务提交给线程池之后,线程池会创建出来一个新线程来执行任务;
2、当线程数组达到 corePoolSize 并且没有线程空闲的时候,此时新的任务会进入 workQueue 里面进行排队等待,直到出现有空闲的线程;
3、队列选择了有界线程,任务超过了队列大小的时候,会创建 maximumPoolSize - corePoolSize 数目的线程来救急;
4、救急线程也使用结束之后,此时来了新任务,会执行拒绝策略;具体的拒绝策略 JDK 提供了四种选项:

5、当线程需要被使用的高峰过去之后,此时的超过 corePoolSize 的救急线程有一段时间没有任务可以做的时候,需要节省资源,使得救急线程释放,这个由 keepAliveTime 以及 unit 来控制实现;
在这里插入图片描述
对于不同的策略的解释查看上图中注解的部分;

Executors 工厂方法创建线程池

实际上就是调用不同的构造方法,进行参数的组合,实现自己的线程池的创建需要;
用来各种用途的线程池

newFixedThreadPool

由于任务队列是没有界限的,所以大量的任务来的时候,可能导致内存溢出的情况,禁止使用;

创建出来固定大小的线程池
在这里插入图片描述

救急线程数目 = 0 (nThread - nThread)

适合任务量是已知的,任务相对比较耗时的;

newCachedThreadPool

任务队列是没有界限的,所以禁止使用,可能造成内存溢出的情况;

带有缓冲功能的线程池;

没有核心线程,全部的线程都是救急线程,救急线程的生存实现是 60s

救急线程在 60s 的时候被回收;
救急线程可以被无限的创建;

队列没有容量,只有取的时候,才能放进去,不然放不进去任务;
没有取的线程是不会放进去任务的,全员外包;
在这里插入图片描述

newSingleThreadExecutor

只有一个线程,核心线程是 1 ;总线程是 1 ,没有救急线程;
在这里插入图片描述

应用场景:多个线程之间,排队执行;

创建的一个线程,每次执行一个任务,任务执行结束自己去无界队列中取任务即可;

单线程和单线程线程池之间的区别:
1、保证始终都能有一个线程是可以用的,自己创建出来的单个线程,这个线程奔溃之后,没有补救措施了;

2、单线程的线程池与其他的线程池定义的初始线程数量为 1 的区别遇到再仔细甄别;

3、newFixedThreadPool 里面参数设置为 1 和这个 newSingleThreadExecutor 单线程线程池之间的区别是:返回的内容不一致的;newSingleThreadExecutor 使用到了装饰器模式;

线程池中一组与提交任务相关的方法

在这里插入图片描述

execute(Runnable command)

@Slf4j(topic = "c.Test")
public class ThreadPoolTest {
    public static void main(String[] args) throws InterruptedException, ExecutionException {


        ExecutorService pool =  Executors.newFixedThreadPool(2);
        
        pool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
}

submit(Callable task)

与 execute 不同的是,Callable 是存在返回结果的;

@Slf4j(topic = "c.Test")
public class ThreadPoolTest {
    public static void main(String[] args) throws InterruptedException, ExecutionException {


        ExecutorService pool =  Executors.newFixedThreadPool(2);

        // 将线程的执行结果返回到线程池中
        // 使用 future 进行返回参数的接收
        Future<String> future =  pool.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(1000);

                return "ok";
            }
        });

        // 主线程中获取线程池中的线程执行的返回结果
        System.out.println("返回的参数 future: " + future.get());
    }
}

上面的代码实现了 submit() 方法与 Callable 结合使用,来实现这种有返回结果的执行任务;

返回结果的接收使用 future ;

future 使用的是保护性暂停的模式接收另一个线程的返回结果;上面的代码表现为在主线程接收线程池中的线程的返回结果;

Callable 是一个单方法的接口;可以使用 lambda 进行替换;

invoke()

上面的invoke() 方法是接收任务的集合,执行所有的任务,返回一个 future 的集合;
下面的invoke() 在上面的执行基础上,添加了超时时间,在一定的时间中,传递进来的任务集合是没有完成掉的话,会将后面的任务取消执行;
在这里插入图片描述

    public static void main(String[] args) throws InterruptedException, ExecutionException {
    	// 上面创建了线程池,线程池里面存在固定个数的线程,可以用线程池对象调用执行任务的相关方法,目的就是让任务完成;
        ExecutorService pool = Executors.newFixedThreadPool(2);

        List<Future<String>> futures = pool.invokeAll(Arrays.asList(
                () -> {
                    log.debug("begin");
                    Thread.sleep(1000);
                    return "1";
                }, () -> {
                    log.debug("begin");
                    Thread.sleep(1000);
                    return "2";
                }, () -> {
                    log.debug("begin");
                    Thread.sleep(1000);
                    return "3";
                }, () -> {
                    log.debug("begin");
                    Thread.sleep(1000);
                    return "4";
                }, () -> {
                    log.debug("begin");
                    Thread.sleep(1000);
                    return "5";
                }));
    }

invokeAny()

返回最先得到的结果,有一个任务执行结束之后,其他的任务都不执行了;

传递进去一个任务集合,什么任务可以优先获取到线程的执行?按照集合的顺序得到?
后序探讨;

在这里插入图片描述

shutdown()

将线程池中的状态改变为:SHUTDOWN
不会接收新的任务;
已经提交的任务会执行结束;
此方法不会阻塞调用线程的执行,不会阻塞主线程的执行,想让主线程等可以执行其他的方法 awaitTermination()

比如主线程调用了 线程池的 shutsown ,不会等待线程池中的所有线程执行结束之后,主线程才会执行,主线程会直接执行自己下面的逻辑代码;

源代码如下:

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        // 修改线程池状态
        advanceRunState(SHUTDOWN);
        // 只会打断空闲线程
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
	// 尝试终结(没有运行的线程可以立即终结,存在运行的线程不会等待,正在运行的线程自己结束,自己等待即可,线程池的状态和那些正在运行的线程的状态是不一样的,线程池先结束了)
    tryTerminate();
}

异步模式 - 工作线程

1、有限的工作线程,异步的处理无限多的任务,一个线程负责多个任务;
2、不同的任务类型使用不同的线程池,可以有效的避免饥饿,提升系统的效率;
在这里插入图片描述

饥饿现象的产生 - 线程池中的线程不足

在这里插入图片描述

饥饿现象的问题解决 - 使用不同类型的线程池

@Slf4j(topic = "c.TestDeadLock")
public class TestStarvation {

    static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");
    static Random RANDOM = new Random();
    static String cooking() {
        return MENU.get(RANDOM.nextInt(MENU.size()));
    }
    public static void main(String[] args) {
        ExecutorService waiterPool = Executors.newFixedThreadPool(1);
        ExecutorService cookPool = Executors.newFixedThreadPool(1);

        waiterPool.execute(() -> {
            log.debug("处理点餐...");
            Future<String> f = cookPool.submit(() -> {
                log.debug("做菜");
                return cooking();
            });
            try {
                log.debug("上菜: {}", f.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
        waiterPool.execute(() -> {
            log.debug("处理点餐...");
            Future<String> f = cookPool.submit(() -> {
                log.debug("做菜");
                return cooking();
            });
            try {
                log.debug("上菜: {}", f.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });

    }
}

创建合理的线程数量

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

任务调度线程池

有时候,希望任务是反复的执行,或者任务是延缓执行,可以使用 java.util.Timer 可以用来调度线程;但是这个线程只能是一个线程进行调度的,前一个任务的延迟执行是会影响到后面的任务的执行的;

Time 实现线程的延缓执行是比较脆弱的,使用起来也是不方便的;

使用任务调用功能的线程池

newScheduledThreadPool

        ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);

        pool.scheduleAtFixedRate(() ->{
            System.out.println("running...");
        },1,1,TimeUnit.SECONDS);

        pool.schedule(()->{
            System.out.println("延迟执行 1 秒");
        },1, TimeUnit.SECONDS);

        pool.schedule(()->{
            System.out.println("延迟执行 2 秒");
        },2, TimeUnit.SECONDS);

        pool.schedule(()->{
            System.out.println("延迟执行 5 秒");
        },5, TimeUnit.SECONDS);
    }

在线程池中捕捉异常

1、自己手写 try catch 进行日志的打印
2、使用 callable 创建线程,里面可以存在输出错误日志信息;

一个定时线程的应用场景

固定在每一周的特定时间中执行特定的线程;

public class ThreadFinate {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);

        // 当前的时间
        LocalDateTime now = LocalDateTime.now();
        System.out.println(now);

        // 计算当前下次一定的线程执行的时间
        LocalDateTime time = now.withHour(12).minusMinutes(0).withSecond(0).withNano(0).with(DayOfWeek.MONDAY);

        // 下一次的执行时间在本周可能已经过去了,在此处重新计算下一次的执行时间,是不是将时间加一周
        if (now.compareTo(time) > 0) {
            time = time.plusWeeks(1);
        }
        
        //  传递到 pool.scheduleAtFixedRate 方法里面的时间差
         long innitialTime =  Duration.between(now, time).toMillis();

        pool.scheduleAtFixedRate(()->{
            System.out.println("按照特定的时间执行这个线程");
        }, innitialTime,1, TimeUnit.MILLISECONDS);
    }
}

Tomcat 里面的线程池

Tomcat Connector 里面使用到的线程池 Executor 是线程池;
在这里插入图片描述
Tomcat 的线程池扩展了 Java 自带的 ThreadPoolExecutor ,处理的策略是稍微的有一些不同的;
在这里插入图片描述
在这里插入图片描述

一个更加高级的线程池 Fork/Join

使用

在这里插入图片描述

1、创建任务对象
2、让任务使用 Fork/Join 执行

public class TestForkJoin {

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
//        System.out.println(pool.invoke(new AddTask1(5)));
        System.out.println(pool.invoke(new AddTask3(1, 1000000)));
    }
}

@Slf4j(topic = "c.AddTask")
class AddTask1 extends RecursiveTask<Integer> {

    int n;

    public AddTask1(int n) {
        this.n = n;
    }

    @Override
    public String toString() {
        return "{" + n + '}';
    }

    @Override
    protected Integer compute() {
        if (n == 1) {
            log.debug("join() {}", n);
            return n;
        }
        AddTask1 t1 = new AddTask1(n - 1);

        t1.fork();
        log.debug("fork() {} + {}", n, t1);
        int result = n + t1.join();
        log.debug("join() {} + {} = {}", n, t1, result);
        return result;
    }
}

@Slf4j(topic = "c.AddTask")
class AddTask2 extends RecursiveTask<Integer> {

    int begin;
    int end;

    public AddTask2(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    @Override
    public String toString() {
        return "{" + begin + "," + end + '}';
    }

    @Override
    protected Integer compute() {
        if (begin == end) {
            log.debug("join() {}", begin);
            return begin;
        }
        if (end - begin == 1) {
            log.debug("join() {} + {} = {}", begin, end, end + begin);
            return end + begin;
        }
        int mid = (end + begin) / 2;

        AddTask2 t1 = new AddTask2(begin, mid - 1);
        t1.fork();
        AddTask2 t2 = new AddTask2(mid + 1, end);
        t2.fork();
        log.debug("fork() {} + {} + {} = ?", mid, t1, t2);

        int result = mid + t1.join() + t2.join();
        log.debug("join() {} + {} + {} = {}", mid, t1, t2, result);
        return result;
    }
}

@Slf4j(topic = "c.AddTask")
class AddTask3 extends RecursiveTask<Integer> {

    int begin;
    int end;

    public AddTask3(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    @Override
    public String toString() {
        return "{" + begin + "," + end + '}';
    }

    @Override
    protected Integer compute() {
        if (begin == end) {
            log.debug("join() {}", begin);
            return begin;
        }
        if (end - begin == 1) {
            log.debug("join() {} + {} = {}", begin, end, end + begin);
            return end + begin;
        }
        int mid = (end + begin) / 2;

        AddTask3 t1 = new AddTask3(begin, mid);
        t1.fork();
        AddTask3 t2 = new AddTask3(mid + 1, end);
        t2.fork();
        log.debug("fork() {} + {} = ?", t1, t2);

        int result = t1.join() + t2.join();
        log.debug("join() {} + {} = {}", t1, t2, result);
        return result;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值