线程诞生的意义是因为进程的创建与销毁太重量了,也耗时,与进程相比,线程是更快了,但是如果进一步提高创建销毁的频率,线程的开销也不能忽略。
两种典型的解决办法:第一种是使用协程(轻量级线程),相比于线程,把系统调度的过程给省略了。第二种就是要讲的线程池。
池:池这个词,在计算机中是一种重要的思想,在很多地方都能用到。比如进程池,内存池,常量池,线程池等。
线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象;
通俗的来讲,就是提前把线程创建好,放在池子里,后续用的时候直接从池子里来取,不必重新创建,这样创建线程的开销就被降低了。那么为啥从池子里取的效率比新创建的线程效率更高?因为从池子里取,这个动作是纯用户态的操作。而创建新的线程这个动作,则是需要用户态+内核态相互配合完成的操作。
内核
操作系统是由内核+配套的应用程序组成。内核是系统最核心的部分,创建线程操作就需要调用系统API,进入到内核中,按照内核态方式完成一系列操作。
内核态和用户态
如果一段程序,是在系统内核中执行,此时就被称为"内核态",如果不是,则称为"用户态"。
内核态操作:是要给所有的进程提供服务的,当你要创建进程的时候,内核会帮我们做,但是在做的过程中难免也要做一些其他的事。过程不可控。
用户态操作:过程可控。
第一种:
注意:线程池对象不是直接new的,而是通过一个专门的方法,返回了一个线程池对象。这种方式我们称为工厂模式,是设计模式中的一中。
代码中 Executors 称为工厂类,newCachedThreadPool() 称为工厂方法。cache翻译为缓存。即用过之后不急着释放,以备随时使用。
特点:
第二种:
特点:
第三种:
特点:
是用工厂方法代替new操作的一种模式,是为了弥补构造方法的局限性。
在我们学习Java语言中,一般都见过当创建实例对象时,有的就会提供多种构造方法(带参的,不带参的等等)供我们使用,这些方法显示是重载关系,要求是方法名相同,构造方法的话还必须跟类名一样,并且要求参数类型和个数不能相同。但对于下面这种情况。
假设有各类,我们期望通过这个类,构造平面上的点。有两种方式。一种是利用直角坐标系,另一种是利用极坐标。
class Point {
public Point(double x,double y) {}public Point(double r,double a) {}
}显然这两方法没有构成重载,就会编译失败。而如果使用工厂模式,可以单独搞一个
类,给这个类写一些静态方法,由这些静态方法负责构造出对象:
class PointFactory {
public static Point makePointByXY() {
Point p = new Point();
p.setX(x);
p.setY(y);
return p;
}
public static Point makePointByRA() {
....
}
}
调用:Point p = PointFactory.makePointByXY(12,16);
上述这几个工厂方法生成的线程池,本质上都是对一个类的封装:ThreadPoolExecutor。相当于下面例子中的Point。
构造方法:
public ThreadPoolExecutor(int corePoolSize, //核心线程数量 相当于正式员工
int maximumPoolSize,// 最大线程数 相当于正式员工+实习生
long keepAliveTime, // 最大空闲时间
TimeUnit unit, // 时间单位
BlockingQueue workQueue, // 任务队列 阻塞队列
ThreadFactory threadFactory, // 线程工厂 工厂模式的体现
RejectedExecutionHandler handler // 饱和处理机制 拒绝策略
)
{ ... }
上面源码中前两个参数的设置。
拒绝策略指的是上面展示源码的最后一个参数 RejectedExecutionHandler handler 的设置。
上面源码中第五个参数 BlockingQueue
我们可以通过下面的场景理解ThreadPoolExecutor中的各个参数;
a客户(任务)去银行(线程池)办理业务,但银行刚开始营业,窗口服务员还未就位(相当于线程池中初始线程数量为0),
于是经理(线程池管理者)就安排1号工作人员(创建1号线程执行任务)接待a客户(创建线程);
在a客户业务还没办完时,b客户(任务)又来了,于是经理(线程池管理者)就安排2号工作人员(创建2号线程执行任务)接待b客户(又创建了一个新的线程);假设该银行总共就2个窗口(核心线程数量是2);
紧接着在a,b客户都没有结束的情况下c客户来了,于是经理(线程池管理者)就安排c客户先坐到银行大厅的座位上(空位相当于是任务队列)等候,
并告知他: 如果1、2号工作人员空出,c就可以前去办理业务;
此时d客户又到了银行,(工作人员都在忙,大厅座位也满了)于是经理赶紧安排临时工(新创建的线程)在大堂站着,手持pad设备给d客户办理业务;
假如前面的业务都没有结束的时候e客户又来了,此时正式工作人员都上了,临时工也上了,座位也满了(临时工加正式员工的总数量就是最大线程数),
于是经理只能按《超出银行最大接待能力处理办法》(饱和处理机制)拒接接待e客户;
最后,进来办业务的人少了,大厅的临时工空闲时间也超过了1个小时(最大空闲时间),经理就会让这部分空闲的员工人下班.(销毁线程)
但是为了保证银行银行正常工作(有一个allowCoreThreadTimeout变量控制是否允许销毁核心线程,默认false),即使正式工闲着,也不得提前下班,所以1、2号工作人员继续待着(池内保持核心线程数量);
使用线程池,若要设置其数目,设置多少合适?
其实没有一个具体的数字,要根据场景确定。总结起来就是两种情况。
一个线程执行的代码,主要有两类(设CPU核心数 (逻辑核心数) 为N):
1.CPU密集型:
2.IO密集型:
而代码不同,线程池的线程数目设置就不同。由于无法知道一个代码,具体多少内容是CPU密集型,多少内容是IO密集型。
正确的做法:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
//线程池 ThreadPool
class MyThreadPool {
//任务队列 阻塞队列实现
private BlockingQueue queue = new ArrayBlockingQueue<>(5); // 队列的容量5 队列中存的是任务
//将任务添加到队列中
public void submit(Runnable runnable) throws InterruptedException {
//拒绝策略 等待阻塞 和 那四种有所不同 但这也是一种策略
queue.put(runnable);
}
//构造方法
public MyThreadPool(int n) {
//n 即创建出n个线程
for(int i=0;i {
try {
//获取队列头任务
Runnable runnable = queue.take();
//执行队头任务
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//启动线程
t.start();
}
}
}
//测试
public class test {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool(10);
for (int i = 0; i < 1000; i++) {
int tmp = i; //内部类访问外部类变量的变量捕获 该变量得是final或是有效的final
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("执行任务: "+tmp);
}
});
}
}
}
1.非核心线程的创建时机
1.1) 核心线程的数量是 corePoolSize 的值,非核心线程的数量是 maxinumPoolSize - corePoolSize ;
1.2) 非核心线程创建的触发时机是:当前线程池中核心线程已满,且没有空闲的线程,还有任务等待队列已满,
满足上面的所有条件,才会去创建线程去执行新提交的任务;
1.3) 如果线程池中的线程数量达到 maxinumPoolSize 的值,此时还有任务进来,就会执行拒绝策略,抛弃任务或者其他
如果拒绝策略是抛弃任务的话,有一种场景,就会造成大量任务的丢弃,就是瞬时冲高的情况下。
2.排队任务调度策略
当线程池中核心线程数量已达标,且没有空闲线的情况下,在产生的任务,会加入到等待队列中去,这样一直持续下去,
等到等待队列已满,在来的任务,会创建非核心线程去执行新提交的任务,那么就产生一种结果,在等待队列中的任务是先提
交的任务,反而没有在此时提交的任务先执行。
任务的执行顺序和任务的提交顺序不一致,如果业务需求的任务是有先后依赖关系的,就会降低线程的调度效率