线程池概念
线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 例如,线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。
以上是百度百科对概念的解释,下面从多线程开始来理解线程池,首先看看多线程的一些可能的面试问题
关于多线程的简单面试问题简单解析
问题1、用多线程的目的是什么?
答:充分利用cpu资源,并发做多件事
问题2、单核cpu机器上适不适合用多线程?
答: 适合,如果是单线程,线程中需要等待IO时,此时cpu就空闲出来了,利用多线程可以充分利用cpu资源。
问题3、线程什么时候会让出cpu?
答:1、阻塞时候 ,wait 、await 、等待IO
2、Sleep、yield、线程结束。
问题4、线程是什么?
答:1、一条代码执行流,完成一组代码的执行。
2、这是一组代码,往往称为一个任务。
问题5、cpu做的是什么工作?
答:1、执行代码。
任务(code)——>线程(code 线程)——> cpu(执行代码)。
问题6、线程是不是越多越好?
答:不是,
1、创建线程需要时间,用完线程需要销毁,创建和销毁都需要消耗时间。线程在java中是一个对象,每个java线程都需要操作系统线程支持。如果 线程创建时间+销毁时间 大于 执行任务时间,就失去意义了。
2、java对象占用堆内存,操作系统线程占用系统内存,根据jvm 规范,一个线程默认最大栈大小1M,这个栈空间需要从系统内存中分配,线程过多,会消耗内存。
3、操作系统需要频繁切换线程上下文,影响性能。
针对这个问题可以用一个图解更加形象表示:
问题7、该如何正确使用多线程?
1、多线程目的:充分利用cpu并发做事(多做任务)
2、线程的本质:将代码送给cpu执行
3、用合适数量的线程 不断运送代码即可
4、合适数量的线程就构成了一个池
问题8、如何确定合适数量的线程?
1、如果是计算型任务:就取cpu数量的1-2倍。
2、如果是IO型任务:则需要多一些线程,需根据具体的IO阻塞时长进行考量决定。tomcat中默认的最大线程数为200
也可考虑根据需要在一个最小和最大数量间自动增减线程数。
线程池工作原理
1、接收任务,放入任务仓库中
2、 工作线程从仓库取出任务,并执行取出的任务。
3、当没有任务时候,线程阻塞,当有任务时候唤醒线程执行。
这里也先用一张图解来形象表示:
图中的卡车表示线程池中工作的线程,任务仓库中存着需要执行的任务。下面再用提问讨论方式说明。
1、 那么工作中的线程(卡车)用什么表示?
1、Runnable 2、Callable
通过实现Runnable接口、Callable可以创建多线程,其中使用Callable有返回值,两种方式具体区别不细说。下面用代码写一下如何实现这个两个接口来建立线程。
Ruanable:
public class RunnableDemo2 {
//通过类实现接口
public static class TestRunnableDemo implements Runnable{
@Override
public void run() {
System.out.println("类实现:"+Thread.currentThread().getName()+"在运行");
}
}
public static void main(String[] args) {
//直接声明
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("直接声明:"+Thread.currentThread().getName()+"在运行");
}
};
//启动线程
new Thread(runnable).start();
new Thread(new TestRunnableDemo()).start();
}
}
运行结果:
类实现:Thread-1在运行
直接声明:Thread-0在运行
Callable:
public class CallableDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
Random random = new Random();
return "返回随机结果:"+random.nextInt(200);
}
};
FutureTask<String> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
String result = futureTask.get();
System.out.println(result);
}
}
Callable代码看起来比runnable要复杂一点,但是可以自己拿到执行的返回值,并且可以抛出已检查的异常。
2、 任务仓库用什么?
用 BlockingQueue 阻塞队列,就是线程安全的队列
BlockingQueue核心方法以四种形式出现:
抛出异常
- 当阻塞队列满时,再往队列里add插入元素会抛出 java.lang.IllegalStateException: Queue full;
- 当阻塞队列空时,再从队列里remove移除元素会抛出 java.util.NoSuchElementException
特殊值
- 插入方法,成功true失败false
- 移除方法,成功返回出队列的元素,队列里面没有就返回null。
一直阻塞
- 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产线程知道put数据或者响应中断退出
- 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程知道队列可用。
超时退出
- 当阻塞队列满时,队列会阻塞生产者现场一定时间,超过限时后生产者线程会退出。
我们的任务会在仓库中进进出出,当仓库为空时,获取(take)操作是阻塞的;当仓库为满时,添加(put)操作是阻塞的。
这里我们把上述操作进行分类
插入方法:
add(E e) :
- 添加成功返回true,失败抛IllegalStateException异常
offer(E e) :
- 成功返回 true,如果此队列已满,则返回 false。
put(E e) :
- 将元素插入此队列的尾部,如果该队列已满,则一直阻塞
删除方法:
remove(Object o) :
- 移除指定元素,成功返回true,失败返回false
poll() :
- 获取并移除此队列的头元素,若队列为空,则返回 null
take():
- 获取并移除此队列头元素,若没有元素则一直阻塞。
检查方法
element() :
- 获取但不移除此队列的头元素,没有元素则抛异常
peek() :
- 获取但不移除此队列的头;若队列为空,则返回 null。
3、简单实现自己的线程池
前面说了讨论了线程池的组成,现在来把前面的部分整合起来实现自己的线程池。
怎么写?
思路:
1、首先要有自己任务仓库
2、要有多个线程(线程集合)
3、每个线程要去做的事--->从任务仓库(队列)中拿到任务
4、初始化线程池
5、将任务放入任务仓库,任务本身也就是线程
6、线程池的关闭
6.1 关闭的时候,总共分几步?
a.仓库停止接受任务
b.仓库中剩下的任务要执行完
c.去仓库中拿任务时候就不应该阻塞了
d.若有线程被阻塞了,就中断线程
代码实现:
public class FixedSizeThreadPool {
//1.仓库(实际就是一个任务队列)
private BlockingQueue<Runnable> blockingQueue;
//2.线程集合
private List<Thread> workers;
//3.每一个线程要去干的事--->队列中拿任务
public static class Worker extends Thread{
private FixedSizeThreadPool pool;
public Worker(FixedSizeThreadPool pool) {
this.pool = pool;
}
@Override
public void run() {
//到队列中拿任务并执行
while (this.pool.blockingQueue.size()>0||this.pool.isWorking){
Runnable task = null;
try {
if(this.pool.isWorking) {
//阻塞的方式拿任务
task = this.pool.blockingQueue.take();
}else {
//不阻塞的方式拿
task = this.pool.blockingQueue.poll();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(task!=null) {
task.run();
System.out.println("线程:"+Thread.currentThread().getName()+"执行完毕!");
}
}
}
}
//4.初始化线程池
public FixedSizeThreadPool(int poolSize,int taskSize) {
if(poolSize<=0||taskSize<=0) {
throw new IllegalArgumentException("非法参数!");
}
this.blockingQueue = new LinkedBlockingDeque<>(taskSize);
//使得 ArrayList 变的安全
this.workers = Collections.synchronizedList(new ArrayList<Thread>());
for(int i=0;i<poolSize;i++) {
Worker worker = new Worker(this);
worker.start();
workers.add(worker);
}
}
//5.将任务放入队列,这里相当与提交的任务,提交的任务也是线程
public boolean submit(Runnable task) {
if(this.isWorking) {
return this.blockingQueue.offer(task);
}else {
return false;
}
}
//6.线程池的关闭
//a.仓库停止接受任务
//b.仓库中剩下的任务要执行完
//c.去仓库中拿任务时候就不应该阻塞了
//d.若有线程被阻塞了,就中断线程
private volatile boolean isWorking = true;
public void shutDown(){
this.isWorking = false;
for (Thread thread : workers) {
if(Thread.State.BLOCKED.equals(thread.getState())) {
//中断线程
thread.interrupt();
}
}
}
//调用线程池
public static void main(String[] args){
//实例化线程池
FixedSizeThreadPool pool=new FixedSizeThreadPool(3, 6);
int task= 6;
//放入6个任务
for(int i=0;i<task;i++) {
pool.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("任务执行");
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
//关闭线程池
pool.shutDown();
}
}
运行结果:
任务执行
任务执行
任务执行
线程:Thread-2执行完毕!
任务执行
线程:Thread-0执行完毕!
任务执行
线程:Thread-1执行完毕!
任务执行
线程:Thread-2执行完毕!
线程:Thread-0执行完毕!
线程:Thread-1执行完毕!
从打印顺序可以看出,3个线程执行6个任务,while循环了两次,第一次三个线程各自拿到了一个任务,执行任务后阻塞了2秒,进行第二次执行。这样就完成了一个简单的线程池的实现。
后面简单说一说下常用Java线程池
4、Java线程池API
Java并发包中提供了丰富的线程池实现!其中常用的有两类 ThreadPoolExecutor、Executors。后面直接用图片展示。
线程池实现方式:
ThreadPoolExecutor 的使用,代码示例:
针对 runnable
public class RunnableDemo {
//线程池
static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(50, 100, 1000, TimeUnit.MICROSECONDS,new LinkedBlockingQueue<Runnable>());
//任务1
public static class TestRunnableDemo implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"在运行");
}
}
public static void main(String[] args) {
//任务2
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"main中在运行");
}
};
//线程池执行任务
threadPoolExecutor.execute(runnable);
threadPoolExecutor.execute(runnable);
threadPoolExecutor.execute(runnable);
threadPoolExecutor.execute(runnable);
threadPoolExecutor.execute(runnable);
threadPoolExecutor.execute(new TestRunnableDemo());
//关闭线程池
threadPoolExecutor.shutdown();
}
}
运行结果:
pool-1-thread-2main中在运行
pool-1-thread-3main中在运行
pool-1-thread-4main中在运行
pool-1-thread-5main中在运行
pool-1-thread-1main中在运行
pool-1-thread-6在运行
针对Callable
public class CallableDemo2 {
//线程池
static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(50, 100, 1000, TimeUnit.MICROSECONDS,new LinkedBlockingQueue<Runnable>());
public static void main(String[] args) throws InterruptedException, ExecutionException {
//任务
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
Random random = new Random();
System.out.println(Thread.currentThread().getName()+"执行任务");
return "返回随机结果:"+random.nextInt(200);
}
};
//提交任务并执行
threadPoolExecutor.submit(callable);
Future<String> future = threadPoolExecutor.submit(callable);
String result = future.get();
System.out.println(result);
threadPoolExecutor.shutdown();
}
}
运行结果:
pool-1-thread-1执行任务
pool-1-thread-2执行任务
返回随机结果:21
以上两个针对callable 和runnable 使用线程池不同的是一提交任务方法不同,Submit()返回一个方法Future结果,Execute()无返回值。
注:本篇笔记是自己在学习公开知识内容过程中整理的,有不同见解的朋友可以指点指点。