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,以减轻系统负担并保证线程池的稳定性。
另外,在实际应用中,还可以根据具体情况进行自定义拒绝策略的实现,以满足特殊的业务需求。例如,可以通过记录、统计和报警等方式来处理被拒绝的任务,或者通过异步方式来将任务提交到其他线程池中执行等。
最后
如果文章对你有帮忙,不要忘记加个关注,点个赞!!!必回关!!!