「Java 并发编程」详解线程池实现原理及常见问题(核心线程=正式工,非核心线程=合同工)

1、 为什么要用线程池

1、提高性能:线程的创建和销毁需要消耗一定的系统资源,使用线程池可以重复利用已经创建好的线程,避免频繁地创建和销毁线程,从而提高程序的性能。

2、提高响应速度:当任务到达时,如果线程池中有空闲的线程,则可以立即执行任务,提高程序的响应速度。

3、控制并发线程数:线程池可以控制同时运行的线程数,避免由于过多的线程导致系统资源占用过多或者系统崩溃等问题。

4、提供更好的任务管理:线程池提供了更好的任务管理方式,可以方便地对任务进行管理和监控,避免任务执行出现异常或者无法处理的情况。

2、线程池工作原理

2.1 线程池任务提交过程

一个线程提交到线程池的处理流程如下图

  • 核心线程数(正式工

  • 等待队列(合同工

  • 最大线程数(所有人员

1)初始化线程池,线程池初始化时并没有创建corePoolSize数目的核心线程,而是惰性加载的方式。等有任务后才创建核心线程。

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

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

4)如果线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的非核心线程来处理被添加的任务。

5)如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。

2.2 ThreadPoolExecutor线程池(推荐)

import java.util.concurrent.*;

public class ThreadPoolExecutorDemo {
    public static void main(String[] args) {
        // 创建一个线程池对象
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, 5, 10, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        // 提交多个任务到线程池中
        for (int i = 1; i <= 10; i++) {
            executor.execute(() -> {
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " is running");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}
复制代码

该示例创建了一个核心线程数为 2,最大线程数为 5,等待队列大小为 5 的线程池对象,然后提交了 10 个任务到线程池中。每个任务会休眠 1 秒钟,然后输出当前线程的名称。最后,调用 shutdown() 方法关闭线程池。

当线程池任务处理不过来的时候,可以通过handler指定的策略进行处理,ThreadPoolExecutor提供了四种策略:

1)ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常;也是默认的处理方式。

2)ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。

3)ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)

4)ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

可以通过实现RejectedExecutionHandler接口自定义处理方式。

2.3 Executors线程池(不推荐)

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorsDemo {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池对象
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // 提交多个任务到线程池中
        for (int i = 1; i <= 10; i++) {
            executor.execute(() -> {
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " is running");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}
复制代码

该示例使用 Executors 工厂类创建了一个固定大小为 3 的线程池对象,然后提交了 10 个任务到线程池中。每个任务会休眠 1 秒钟,然后输出当前线程的名称。最后,调用 shutdown() 方法关闭线程池。

需要注意的是,虽然 Executors 提供了许多快速创建线程池对象的方法,但是这些方法并不能满足所有的需求和场景,因此在实际应用中,需要根据具体情况和性能需求选择合适的线程池实现,并进行适当的参数设置和优化等操作。以下是几种创建方式:

1)Executors.newCachedThreadPool();
说明: 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.
内部实现:new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue());

2)Executors.newFixedThreadPool(int);
说明: 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
内部实现:new ThreadPoolExecutor(nThreads, nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue());

3)Executors.newSingleThreadExecutor();
说明:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照顺序执行。
内部实现:new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue())

4)Executors.newScheduledThreadPool(int);
说明:创建一个定长线程池,支持定时及周期性任务执行。
内部实现:new ScheduledThreadPoolExecutor(corePoolSize)

【附】阿里巴巴Java开发手册中对线程池的使用规范

3、线程池的等待队列

1)ArrayBlockingQueue: 这是一个由数组实现的容量固定的有界阻塞队列.

2)SynchronousQueue: 没有容量,不能缓存数据;每个put必须等待一个take; offer()的时候如果没有另一个线程在poll()或者take()的话返回false。

3)LinkedBlockingQueue: 这是一个由单链表实现的默认无界的阻塞队列。LinkedBlockingQueue提供了一个可选有界的构造函数,而在未指明容量时,容量默认为Integer.MAX_VALUE。

  队列操作:

注意:

1)当workQueue使用的是无界限队列时,maximumPoolSize参数就变的无意义了,比如new LinkedBlockingQueue(),或者new ArrayBlockingQueue(Integer.MAX_VALUE);

2)使用SynchronousQueue队列时由于该队列没有容量的特性,所以不会对任务进行排队,如果线程池中没有空闲线程,会立即创建一个新线程来接收这个任务。maximumPoolSize要设置大一点。

3)核心线程和最大线程数量相等时keepAliveTime无作用.

4、常用的线程类及用法

1)主线程判断所有子线程是否执行完成

  • CountDownLatch

    import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;

    public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { int nThreads = 10; CountDownLatch latch = new CountDownLatch(nThreads); ExecutorService executor = Executors.newFixedThreadPool(nThreads);

        for (int i = 0; i < nThreads; i++) {
            executor.execute(() -> {
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " is running");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            });
        }
    
        // 等待所有线程执行完毕
        latch.await();
    
        System.out.println("All threads are finished.");
    
        // 关闭线程池
        executor.shutdown();
    }
    复制代码

    }

该示例使用 CountDownLatch 对象来统计所有子线程的完成情况。在每个子线程结束时,调用 countDown() 方法将计数器减一。主线程在调用 await() 方法时会被阻塞,直到计数器值为 0,即所有子线程都已完成。

  • CompletableFuture

jdk8增加的类,可以代替CountDownLatch

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        int nThreads = 10;
        ExecutorService executor = Executors.newFixedThreadPool(nThreads);

        CompletableFuture<?>[] futures = new CompletableFuture[nThreads];

        for (int i = 0; i < nThreads; i++) {
            CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " is running");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, executor);
            futures[i] = future;
        }

        // 等待所有线程执行完毕
        CompletableFuture.allOf(futures).join();

        System.out.println("All threads are finished.");

        // 关闭线程池
        executor.shutdown();
    }
}
复制代码

使用 CompletableFuture 可以非常方便地实现主线程判断所有子线程是否执行完成。该示例使用 CompletableFuture 对象来异步执行任务,并将每个对象添加到一个数组中。在所有子线程执行结束后,调用 CompletableFuture.allOf() 方法等待所有任务完成。由于 allOf() 方法返回的是一个新的 CompletableFuture 对象,需要调用 join() 方法等待其完成。需要注意的是,CompletableFuture 对象支持链式调用和组合操作,可以非常方便地处理多个异步任务之间的依赖关系和结果处理,因此在实际应用中非常灵活和强大。但是在使用时需要注意其线程池的管理和资源消耗等问题。

2)线程之前变量共享

zhuanlan.zhihu.com/p/622506562

5、相关问题

1)什么是线程池?为什么需要线程池?

线程池是一种管理和复用线程的机制,它可以避免频繁地创建和销毁线程,从而提高系统性能和稳定性。在多线程编程中,通常会需要处理大量的并发任务或请求,如果每个任务都创建一个线程来执行,可能会导致系统资源的浪费和性能的下降,同时还会使得线程管理变得复杂和困难。使用线程池可以有效地解决这些问题,提高代码的可维护性和可扩展性。

2)线程池的核心参数有哪些?如何设置线程池的参数?

线程池的核心参数包括核心线程数、最大线程数、等待任务队列大小、线程超时时间和拒绝策略等。这些参数可以通过 ThreadPoolExecutor 类的构造器或者 setter 方法进行设置和调整。其中,核心线程数表示线程池中的基本线程数量,当任务数量超过了核心线程数,但小于等于最大线程数时,会创建非核心线程来执行任务;最大线程数表示线程池中最多能够同时运行的线程数量;任务队列大小表示任务缓冲区的容量,当没有空闲线程时,新的任务会被放入任务队列中等待执行;线程超时时间表示非核心线程的最大空闲时间,当超过该时间时,非核心线程将被回收;拒绝策略用于处理无法处理的任务,例如抛出异常或者丢弃任务等。

3)线程池的工作原理是什么?

线程池的工作原理可以简单地概括为如下几个步骤:首先,当有新的任务到达时,线程池会检查当前线程数量是否小于核心线程数,如果是,则创建一个新的核心线程来处理任务;如果否,则将任务放入任务队列中等待执行。当任务数量超过了任务队列的容量,线程池会创建一定数量的非核心线程来处理任务。同时,当非核心线程空闲时间超过了指定的超时时间时,线程池可能会将其回收以释放资源。另外,当线程池中的线程数量已达到最大值,并且任务队列也已满时,线程池会触发拒绝策略来处理无法处理的任务。

4)如何优化线程池的性能?有哪些注意事项?

优化线程池的性能可以从以下几个方面入手:首先,合理设置线程池的核心参数,以充分利用系统资源并避免过度消耗资源;其次,及时关闭不需要的线程池,并避免创建过多的线程池实例;另外,尽量使用轻量级任务或者异步任务来替代重量级任务或者同步任务,以提高任务处理的效率和吞吐量;同时,需要注意线程安全问题和共享状态的管理,避免出现数据竞争和死锁等问题。另外,在实际应用中还需要进行性能测试和监控,以及根据具体情况进行调整和优化。

5)线程池拒绝策略有哪些?如何选择合适的拒绝策略?

线程池的拒绝策略是用于处理无法处理的任务的一种机制,通常有以下几种选项:

  • AbortPolicy:默认的拒绝策略,当任务无法处理时,抛出 RejectedExecutionException 异常。
  • CallerRunsPolicy:将任务返回给调用者线程执行,如果线程池已关闭,则直接丢弃任务。
  • DiscardPolicy:直接丢弃任务,不做任何处理。
  • DiscardOldestPolicy:丢弃队列中最老的一个任务,并尝试重新提交当前任务。

选择合适的拒绝策略取决于具体应用场景和业务需求。例如,对于实时性较高的任务,可能需要使用比较严格的拒绝策略,如 AbortPolicy 或 CallerRunsPolicy,以避免任务队列过载和响应时间过长;而对于一些非关键性的任务,可以使用较为宽松的拒绝策略,如 DiscardPolicy,以减轻系统负担并保证线程池的稳定性。

另外,在实际应用中,还可以根据具体情况进行自定义拒绝策略的实现,以满足特殊的业务需求。例如,可以通过记录、统计和报警等方式来处理被拒绝的任务,或者通过异步方式来将任务提交到其他线程池中执行等。

最后


如果文章对你有帮忙,不要忘记加个关注,点个赞!!!必回关!!!

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

栈江湖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值