目录
概述
线程
线程被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程池
线程池是一种用于管理和复用线程的机制。它包含一个线程队列,用于保存需要执行的任务,并且能够动态地创建或销毁线程。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。通过线程池,可以减少线程创建和销毁的开销,提高系统性能和资源利用率。
线程池的好处
使用线程池管理多线程任务有以下几个好处:
- 降低资源消耗:线程池可以控制并发线程数量,防止无限制创建大量线程,从而降低系统资源消耗。
- 提高响应速度:由于线程池中的线程是预先创建好的,因此可以在任务到达时立即执行,从而提高系统的响应速度。
- 提高系统稳定性:合理配置线程池参数可以避免因为并发线程过多导致系统崩溃的情况,提高系统的稳定性。
- 统一管理:线程池可以统一管理线程的生命周期、状态和执行情况,为线程的监控和调优提供了便利。
详述
线程池原理
当一个任务需要执行时,线程池会按照预先设定的规则,从线程队列中取出一个空闲的线程来执行任务,如果没有空闲线程,则根据设定的参数来决定是否创建新的线程。任务执行完成后,线程不会被销毁,而是重新放入线程队列中,等待下一个任务的执行。这样就避免了频繁创建和销毁线程的开销。
流程图如下:
![](https://i-blog.csdnimg.cn/blog_migrate/19d3737710f7a3226639c0bd201c45ac.png)
说的简单一点就是如何实现线程的复用---回收再利用
池这一类其实是一种设计思想,对资源的充分利用,而线程池则是利用了生产者消费者这一模式将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。
线程池的运行过程可以分成任务管理和线程管理两部分:
-
任务管理:
任务管理阶段涉及到任务的提交、存储和执行。在这个阶段,应用程序将需要执行的任务提交给线程池,线程池会将这些任务保存起来,并安排线程在合适的时机执行这些任务。这包括任务队列的管理,决定哪些任务会被执行,哪些会被暂时存储等。 -
线程管理:
线程管理阶段涉及到线程的创建、销毁和调度。线程池需要根据任务的数量和状态来动态管理线程的数量,包括创建新的线程、回收空闲线程等。线程管理还包括对线程的监控和调度,以确保任务能够得到及时而有效的执行。
在线程池中,任务提交阶段可以看作是生产者,负责不断地向线程池提交任务。而任务执行阶段可以看作是消费者,负责从任务队列中取出任务并执行。
生产者-消费者问题是一个经典的多线程同步问题,涉及到生产者将数据放入共享缓冲区,而消费者从缓冲区中取出数据的过程。在线程池中,任务提交和任务执行的过程就类似于生产者将任务放入任务队列,而工作线程则类似于消费者从任务队列中取出并执行任务。
为了保证线程池的正确运行,需要使用合适的同步机制来处理生产者和消费者之间的关系。常见的方式包括使用锁、条件变量、信号量等来实现任务队列的同步和互斥访问,以确保任务能够按照正确的顺序被提交和执行。
这样的划分有助于更好地理解线程池的运行机制。任务管理和线程管理相互配合,共同组成了线程池的核心功能,使得线程池能够高效地管理任务并执行任务,从而提高系统的性能和资源利用率。
线程池参数
线程池的参数配置对于线程池的性能和行为具有重要影响,以下是一些常见的线程池参数及其作用:
-
核心线程数(corePoolSize):
- 作用:线程池中保持的最小线程数,即使线程处于空闲状态,也不会被回收。
- 配置建议:根据系统负载和并发任务量来设置,确保核心线程能够满足基本的任务处理需求。
-
最大线程数(maximumPoolSize):
- 作用:线程池中允许存在的最大线程数量,当任务量超过核心线程数时,线程池会创建新线程来处理任务,但不会超过最大线程数。
- 配置建议:根据系统资源情况和预期的最大负载来设置,避免无限制地创建大量线程。
-
空闲线程存活时间(keepAliveTime)和任务队列(workQueue):
- 作用:当线程池中的线程数量超过核心线程数,多余的空闲线程在经过一定时间后会被回收;同时,任务队列用于保存等待执行的任务。
- 配置建议:根据任务的特性和系统的响应需求来调整,如IO密集型任务可以适当增加存活时间,而CPU密集型任务可能需要更大的任务队列容量。
-
拒绝策略(RejectedExecutionHandler):
- 作用:当任务无法被线程池接收执行时的处理策略,比如当线程池已满并且任务队列已达到最大容量时该怎么处理。
- 配置建议:根据业务需求和系统特点选择合适的拒绝策略,常见的有抛出异常、丢弃任务、阻塞等待和调用者运行等。
通过合理配置这些线程池参数,可以有效地控制线程池的行为,使其适应不同的业务场景和系统环境,从而提高系统的性能、稳定性和可维护性。
线程池的工作原理和参数密切相关
线程池的参数配置会直接影响线程池的工作方式和行为。
-
核心线程数和最大线程数:核心线程数决定了线程池中保持的最小线程数量,而最大线程数决定了线程池允许存在的最大线程数量。当任务到达时,线程池会根据核心线程数和最大线程数来决定是创建新线程还是将任务放入任务队列中。如果线程池中的线程数已经达到了最大线程数,且任务队列也已满,则根据拒绝策略来处理无法执行的任务。
-
任务队列:任务队列用于保存等待执行的任务。线程池的任务队列可以是有界队列或无界队列。有界队列的容量限制了能够保存的任务数量,当任务到达时如果队列已满,则会创建新的线程来执行任务(不超过最大线程数)。而无界队列没有容量限制,当任务到达时如果线程数未达到最大线程数,则会将任务放入队列中等待执行。
-
空闲线程存活时间:空闲线程存活时间决定了当线程池中的线程数量超过核心线程数时,多余的空闲线程在经过一定时间后会被回收。这个参数的设置可以影响线程池的动态调整能力,当任务量下降时,空闲线程可以被回收以释放资源,而当任务量增加时,线程池可以根据需要重新创建新的线程来处理任务。
通过合理配置线程池的参数,可以根据不同的业务需求和系统环境来优化线程池的性能和资源利用率,提高系统的响应速度和稳定性。因此,了解线程池的工作原理并合理地配置参数是使用线程池的关键。
应用
import java.util.concurrent.CountDownLatch;
public class CalaulationStudentsData implements Runnable {
//学生数量
int studentNumber;
//锁存器
CountDownLatch latch;
public CalaulationStudentsData(int studentNumber, CountDownLatch latch){
this.studentNumber=studentNumber;
this.latch=latch;
}
@Override
public void run() {
try {
//计算学生学习数据的方法
this.CalculationStudentData();
//计数器减一
latch.countDown();
}catch (Exception e){
throw new RuntimeException("学生进入学校执行数据异常"+e);
}
}
private void CalculationStudentData() throws InterruptedException {
//输出学生编号和执行该任务的线程名称
Thread.sleep(100);
System.out.println("学生" + studentNumber+"进入学校" + "接待的老师id(用到的线程号)" + Thread.currentThread().getId());
}
}
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class newThread {
public static void test(){
//实例化一个固定大小为10个线程的newFixedThreadPool线程池
ExecutorService excutorService = Executors.newFixedThreadPool(10);
//构造CountDownLatch传入数量为100,初始化的计数器大小为100,与学生数量对应。
final CountDownLatch latch = new CountDownLatch(100);
//计算100个学生进入学校的情况
for (int i = 0; i <100 ; i++) {
//线程提交任务
excutorService.submit(new CalaulationStudentsData(i,latch));
}
try{
//使调用该方法的主线程处于等待状态,当倒数到0时主线程才执行。
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException("学生进入学校多线程处理异常",e);
}
//关闭线程池
excutorService.shutdown();
return;
}
public static void main(String[] args) {
long startTime=System.currentTimeMillis(); //获取开始时间
test();
long endTime=System.currentTimeMillis(); //获取结束时
System.out.println("程序运行时间: "+(endTime-startTime)+"ms");
}
}
以上示例演示了如何使用线程池(ExecutorService)和 CountDownLatch 来模拟学生进入学校的情况,并进行多线程处理。
CalaulationStudentsData 类
- 这个类实现了 Runnable 接口,表示它可以在单独的线程中运行。
- 它包含了学生数量和一个 CountDownLatch 对象作为成员变量。
- 在 run() 方法中,首先调用 CalculationStudentData() 方法模拟计算学生学习数据的过程,然后通过 countDown() 方法将 CountDownLatch 的计数器减一。
newThread 类
- 包含了 test() 方法和 main() 方法。
- test() 方法中首先创建了一个固定大小为 10 个线程的线程池(通过 Executors.newFixedThreadPool(10) 创建),然后实例化了一个 CountDownLatch,初始值为 100,代表有 100 个学生。
- 接着使用 for 循环提交了 100 个 CalaulationStudentsData 任务给线程池处理。
- 调用 latch.await() 使主线程处于等待状态,直到所有学生的任务都执行完毕(即 CountDownLatch 的计数器减到 0)。
- 最后关闭了线程池。
main 方法
- 在 main 方法中调用了 test() 方法,并在执行前后分别记录了时间,以便计算程序的运行时间。
注意事项
当使用线程池时,有一些注意事项需要考虑:
线程池的大小:线程池的大小需要根据任务的性质来决定。如果是计算密集型的任务,通常可以配置较大的线程池;如果是 I/O 密集型的任务,可以配置较小的线程池。过大的线程池可能会导致资源竞争和上下文切换的开销增加,而过小的线程池则可能导致任务排队时间过长。
线程池的类型选择:
newFixedThreadPool
:固定大小的线程池,适用于负载稳定的情况。newCachedThreadPool
:根据需要创建新线程的线程池,适用于负载较轻的情况。newSingleThreadExecutor
:只有一个工作线程的线程池,适用于需要保证顺序执行的场景。
任务队列:线程池中的任务队列可以是有界队列(如 ArrayBlockingQueue)或无界队列(如 LinkedBlockingQueue)。选择合适的任务队列类型可以避免因任务过多而导致的内存溢出等问题。
错误处理:需要考虑如何处理线程池中任务的异常。可以通过实现 UncaughtExceptionHandler 接口来处理未捕获的异常。
线程池的生命周期管理:需要在适当的时候关闭线程池,以释放资源。可以通过调用 shutdown()
或 shutdownNow()
方法来关闭线程池。
监控和调优:对线程池进行监控,包括线程池中线程的运行状态、任务执行情况等,并根据监控结果进行调优,提高线程池的性能和稳定性。
避免死锁:在使用线程池时,要注意避免死锁的发生。特别是当涉及到多个资源时,需要谨慎安排任务的执行顺序和资源的获取顺序,避免出现死锁情况。