概述
在各大公司的面试中,线程池的题目都是比较多且比较难的,并且,线程相关的对象和其他的业务API是不相同的,区别在于一个直接操纵了操作系统,使用的是操作系统相关的API,一个单纯只占用内存。
从Java核心专栏线程相关的知识中我们也可以知道,线程的产生与销毁都会消耗一定的性能,所以要避免频繁的创建与销毁。
那么解决相关问题的方法就是,创建线程池。
概述
线程池的需求很普遍,从一般使用的池化角度去说:当你需要资源的时候就用acquire()方法来申请资源,用完之后就调用release()释放资源。但是在线程池中,是完全不同的,Java没有提供申请线程和释放线程的方法。
线程池模型:生产-消费
为什么线程池的模型和普通的池化资源不同呢,如果采用了一般模型的线程池设计,那应该是是如下:
class ThreadPool{
//获取空闲线程
Thread acquire() {
}
//释放空闲线程
void release(Thread t) {
}
}
ThreadPool pool;
Thread t1 = pool.acquire();
//传入Runnable对象
t1.execute(()->{
//业务代码
});
过程:假设我们获取到一个空闲线程T1,然后使用t1完成我们的业务。
- 创建线程t1
- 调用t1.execute()
- 然后传入Runnable执行基本逻辑,就像通过构造函数Thread(Runnable target)创建线程。
但是并没有execute(Runnable target)这个方法。所以线程池并不是这么设计的。
线程池最终采用的设计方式为 生产-消费
线程池的使用方式生产者,线程池本身是消费者。
可以看以下的示例代码:
package jike_Time;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
//简化的线程池,仅仅用来说明工作原理
public class MyThreadPool {
//利用阻塞队列来实现生产者-消费者模型
BlockingQueue<Runnable> workQueue;
//保存内部工作线程
List<WorkerThread> threads = new ArrayList<>();
//构造方法
MyThreadPool(int poolSize, BlockingQueue<Runnable> workQueue) {
this.workQueue = workQueue;
//创建工作线程
for (int idx = 0; idx < poolSize; idx++) {
WorkerThread workerThread = new WorkerThread();
workerThread.start();
threads.add(workerThread);
}
}
//工作线程负责消费任务,并且执行任务
class WorkerThread extends Thread {
@Override
public void run() {
//循环取任务并且执行
while (true) {
Runnable task = null;
try {
task = workQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
task.run();
}
}
}
//提交任务
void execute(Runnable command) throws InterruptedException {
workQueue.put(command);
}
}
/*使用实例*/
//创建有界阻塞队列
class User {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(2);
//创建线程池
MyThreadPool pool = new MyThreadPool(10, workQueue);
//提交任务
pool.execute(() ->{
System.out.println("hello");
});
}
}
//output : hello
接下来看一下程序,在MyThreadPool内部,我们维护了一个阻塞队列workQueue和一组工作线程,工作线程的个数由构造函数的poolSize决定。用户通过execute()方法提交Runnable任务,execute()方法内部仅仅是添加任务到任务队列。而MyThreadPool会消费任务队列执行任务,相关的代码就是while循环。
多线程编程无法throws异常
多说一嘴,多线程编程之所以无法throws抛出异常,用最简单的话来说,多线程中一个线程中止,不会影响 其他线程继续运行,所以Exception就会逃逸,在Java线程被设计出来的时候,有这样一个理念,线程自己的异常由线程自己解决,而线程的实现是在run方法内的,如果想捕获到单线程的异常,就需要在run方法内部catch,而不是在大类里面throws。
但,也并不是完全没有方法在不catch的情况下获得到线程内部的异常,java5有一个方法Thread.UncaughtExceptionHandler.uncaughtException(),可以帮助我们获取到线程内部的异常,但是很麻烦,简化之后,我们就参照正常的线程设计,去设计我们的架构就好。
如何使用java的线程池
Java的线程池中,最核心的就是ThreadPoolExecutor,此工具的构造函数十分复杂,如以下代码所示,共7个参数:
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
然后来一一介绍一下里面的参数:
- corePoolSize :表示线程池保有的最小线程数。一般给不太重要的业务使用。
- maximumPoolSize :表示线程池创建的最大线程数。给最繁忙的业务用,但是当业务清净下来,也会降低线程使用,但不会低于corePoolSize。
- keepAliveTime & unit :用来定义业务的空闲与繁忙,当超过keepAliveTime & unit(ms)都没有响应,就会定义为空闲,降低线程的占用。
- workQueue :工作队列,和上面示例代码的工作队列同义。
- threadFactory :通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字
- handler::通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队 列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略, 你可以通过handler这个参数来指定。以下是四种拒绝方法:
- CallerRunsPolicy:提交任务的线程自己去执行该任务。
- AbortPolicy:默认的拒绝策略,会throws RejectedExecutionException。
- DiscardPolicy:直接丢弃任务,没有任何异常抛出。
- DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入 到工作队列。
使用线程池要注意什么
因为上面提到的这些参数确实使用起来比较复杂,于是并发包就提供了一个静态并发工厂类Executors快速创建线程池,不过不建议。
原因是:Executors很多方法都是无界的LinkedBlockingQueue,无界队列想当然的会造成oom(Out of Memory),而oom会耽误所有事情,所以建议用有界队列。
当采用了有界队列,当并发量超载的时候,就会触发RejectedExecutionException拒绝策略,此异常不会被强制catch,容易中断 项目,所以要慎用,包括锁的降级同理,因此最好自定义降级策略和拒绝策略。
当使用线程池进行异常处理的时候,建议根据不同的异常,写出不同的策略。
最后
以上就是精明抽屉为你收集整理的Executor与线程池:如何创建正确的线程池?的全部内容,希望文章能够帮你解决Executor与线程池:如何创建正确的线程池?所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复