现在学到的池有很多:在字符串里学到的字符串常量池,MySQL里学到的数据库连接池,以及进程池,线程池,内存池,我们所学的这些池本质上是为了更节省资源,不论是内存资源还是时间资源,我们今天所提到的线程池就是如此,我们引入线程池一定不是为了更麻烦而引入
资源节省的两种方式:协程和线程池
那么线程池主要在哪里有资源节省的情况呢?主要是为了创建销毁线程更轻松,快捷,为了在资源节省方面有所进展,主要有两种方面可以进行优化,第一种是协程(纤程),后续jdk高版本中引入虚拟线程本质上就是协程;那么第二种就是我们今天要介绍的线程池了
介绍字符串常量池
在聊线程池之前,我们可以先谈论类比一下字符串所在的字符串常量池,字符串常量池是为了使字符串对象创建的时候现在字符串常量池中寻找是否有该字符串,如果有就直接指向该引用,如果没有该字符串,直接创建一个该字符串的引用,在下一次相同字符串对象创建的时候直接在字符串常量池中进行查找,大大节省了时间的开销
private final char value[];
//我们在String类中找到其成员,发现两个主要属性
/** Cache the hash code for the string */
private int hash; // Default to 0
假设str1先创建“Hello”,此时在字符串常量池中创建“Hello”,str2在创建时直接在字符串常量池中进行寻找,如果找到,此时就直接让value这个引用直接指向字符串常量池中的“Hello”
线程池的原理
线程池是怎样的呢?创建销毁线程是需要用户态和内核态一起配合完成的工作,但是线程池不这样做,直接使用用户态就可以创建销毁线程,不需要内核态的配合,用户态是程序员,而内核态可以理解为操作系统的内核,也就是底层的一些东西
直接创建线程,需要调用api,销毁线程需要需要内核完成,内核完成的工作很多时候是不太可控的,如果使用线程池,提前把线程创建好,放到用户态中写的数据结构里面,后面用的时候,随时从线程池中取,用完了放回池子里去,这个过程完全是用户态代码,不需要和内核进行交互
这个数据结构也不会太复杂,我们写一个伪代码
List<Thread> list = ...
for(...) {
Thread t = new Thread();
list.add(t);
{
//这也就是把创建好的线程用数据结构管理起来
我们举个栗子来理解这一操作:
我们从银行取钱,需要大堂和柜台还有你来配合,柜台说需要身份证复印件,现在有两种办法,是银行工作人员帮你复印,另一种办法是自己去复印。如果是工作人员帮你复印,那么中途中工作人员可能会做别的事,例如给老公打个电话了,上个卫生间了等等一系列我们看不到的操作,此时效率就比自己去复印的效率低很多,所以我们把自己去复印相当于是使用线程池,银行工作人员帮我们复印就是内核态加用户态一起来操作,是不可控的
协程的简洁原理
[协程本质上也是纯用户态操作,规避内核操作,不过它并不是在内核里把线程创建好,而是用一个内核的线程,来表示多个协程(纯用户态,进行协程之间的调度)]
介绍线程池的构造方法
在标准库中线程池是ThreadPoolExecutor
//第一种构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
//第二种构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
//第三种构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
//第四种构造方法-最终构造方法,也是参数最全的构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
我们直接介绍最后一种构造方法:
public ThreadPoolExecutor(int corePoolSize,//核心线程数
int maximumPoolSize,//最大线程数 线程有两种 核心线程和非核心线程
//最大线程数=核心线程数+非核心线程数
//核心线程和非核心线程的区别相当于正式员工和实习员工的区别
//非核心线程在不使用的时候会被回收掉
//我们应该设置多少核心线程数呢?
//如果有人给你一个准确的答案,假设你的CPU核心数有N个,答案是N,2N,N+1,等答案 会有一定的错误
//核心数的设定不仅与电脑的CPU核心数有关,更有关系的是程序的类型,我们极端一点,把程序分为两种类型
//1.CPU密集型程序,这种程序类似许多逻辑判断,数值计算
//这种程序所在的线程与cpu有关,程序一旦运行就会沾满你的cpu,与你的cpu核心数有关,也就是线程数目不应该超过CPU核心数的
//2.io密集型程序,这种程序与硬盘有许多io操作
//这种程序的线程占用cpu很小的一部分,很可能进行网络操作,可能对网卡会有要求,例如你的网卡是1gbps,你的线程数目读写速度是100mbps,此时你的线程数不应该超过10个
//这两种划分程序的方式过于极端,一般的程序会包含网络操作和cpu计算等操作,所以实际的线程数应该通过实验来获得,自己动手来看最佳的核心数是多少
long keepAliveTime,//保持线程存活的时间,也就是线程不执行任务时,非核心线程存活的时间,一旦过了这个时间,线程就会结束,也就是非核心线程闲时,不执行操作存活的最大时间
TimeUnit unit,
//这是时间的单位,是一个枚举类型,包含NANOSECONDS纳秒 MICROSECONDS微秒 MILLISECONDS毫秒 SECONDS秒 MINUTES分 HOURS时 DAYS天 下图中有各进制之间的换算
BlockingQueue<Runnable> workQueue,
//我们在阻塞队列中介绍过这个类,里面包含各线程,其中可以包含各个线程之间各自的任务,也就是线程池中部署任务等待线程池执行,线程池会提供一个submit方法,其他线程可以提交到线程池,线程池里需要一个数据结构来管理各个线程的任务,阻塞队列就出现了,执行的逻辑就是各个thread里的run方法
ThreadFactory threadFactory,
//线程工厂,什么意思? 我们先介绍一种新的设计模式:工厂模式,看下文
//解释完工厂模式我们再次回到线程工厂,线程工厂主要是为了给创建的线程设置一些属性,在工厂的方法内就把属性初始化好了,平时一般也不会使用ThreadFactory,主要是搭配线程池来使用,如果我们不写这个参数,就会调用默认的线程工厂,下图中写的是default..
RejectedExecutionHandler handler) {
//我们看到后面的handler可能会误认为是句柄,实则和句柄没有关系,句柄是资源的标示
//这里指的是当任务队列满了,但是还要继续添加任务,你的策略是什么?
//这里有四种策略 有四种策略的代码 均在线程池中 下文中有四种策略的详细代码
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
时间单位 换算
工厂模式:把构造方法封装起来,如果我们创建一个Point类,在构造时,既想使用极坐标的方法来构造,又想使用直角坐标的方式来构造,此时怎么做,有一种比较多余的操作,就是多加一个参数来判断是哪个构造方法
class Point {
private int x;
private int y;
private int r;
private int E;
public Point(int x,int y) {
this.x=x;
this.y=y;
}
public Point(int r,int E,int Mode) {
this.r=r;
this.E = E;
}//此时我们使用多一个参数就可以使用极坐标参数,但是看起来还是很别扭
//如果我们使用E的类型是double,把成员变量改成double似乎也可以,但是只适用这个例子,我们可以使用工厂模式,来使用更好的代码,提高代码下限
}
工厂模式代码
class Point {
private int x;
private int y;
private int r;
private int E;
public void setR(int r) {
this.r = r;
}
public void setE(int e) {
E = e;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
}
class PointBuilder {//使用另一个构造类,也可以使用一个类,不用加一个类,这样做是为了做进一步封装
//还有一个优化就是可以把xy,re一起设计一个set,更简洁一些
public static Point makeRectangular(int x,int y) {
Point point = new Point();
point.setX(x);
point.setY(y);
return point;
}
public static Point makePolar(int r,int E) {
Point point = new Point();
point.setR(r);
point.setE(E);
return point;
}
}
线程池的四种策略
第一种策略:直接抛出异常
public static class AbortPolicy implements RejectedExecutionHandler {
/**
* Creates an {@code AbortPolicy}.
*/
public AbortPolicy() { }
/**
* Always throws RejectedExecutionException.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
* @throws RejectedExecutionException always
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
第二种策略:你给我这个线程池添加任务?不好意思,我这满了,你自己来执行,谁负责添加这个任务,谁负责执行任务,线程池本身不管了
public static class CallerRunsPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code CallerRunsPolicy}.
*/
public CallerRunsPolicy() { }
/**
* Executes task r in the caller's thread, unless the executor
* has been shut down, in which case the task is discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
第三种策略:丢弃掉最古老的任务,让新的任务去队列中排队
public static class DiscardPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardPolicy}.
*/
public DiscardPolicy() { }
/**
* Does nothing, which has the effect of discarding task r.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
第四种策略:丢弃最新的任务,还是按照原有的节奏来执行
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardOldestPolicy} for the given executor.
*/
public DiscardOldestPolicy() { }
/**
* Obtains and ignores the next task that the executor
* would otherwise execute, if one is immediately available,
* and then retries execution of task r, unless the executor
* is shut down, in which case task r is instead discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
以上就是当任务队列满的时候,还要往队列中添加任务的四种策略
这一篇博客我们介绍了线程池的一些大致的细节,未来将会介绍线程池的实现,敬请期待!!!