多线程之旅:线程池与定时器
ok,今天小编接着来介绍下线程池与定时器。
那么,同样的请出我们今天的第一位主角
线程池
那么之前小编分享过了如何创建线程和启动线程了,那么为什么还需要个线程池呢?
回到最初讲到线程的时候,线程为什么要引入呢?
这是因为进程太“重”,频繁的进行创建销毁,需要消耗资源较大。
此时就引入了线程。
同样,随着业务量对性能要求的提高,此时线程不断的频繁创建销毁,那么此时这笔开销,就显的不能忽略不计了。
那么这就引入了一个线程池。。
线程池,就是提前从系统那边申请好线程资源,放到一个地方,此时,后面谁需要线程就从这里取
而不是从系统那边申请,当用完线程后,就放回到原处,从而减少开销
那为什么从线程池取线程,会比从系统申请,来的高效些呢。
这就涉及到了操作系统中的一些内容
用户态和内核态。
那什么又是用户态和内核态呢?
用户态和内核态是操作系统用来管理权限和资源访问的两种运行模式。
用户态:
定义:当一个进程在用户态时,它只能访问那些分配给它的内存空间和其他有限的资源,用户态下代码执行会受到严格限制,不能访问物理硬件和系统内核的数据结构。
内核态:
内核态具有完全的访问权限,可以访问所有的系统资源,包括CPU、网络、硬盘等等,以及整个文件系统和进程列表高级功能。
举个例子
在银行中

此时A、B、C代表的是工作人员
甲代表的是客户
此时甲进入银行要求打印些东西,那么此时,我们的工作人员就会可能对其打印的资料进行审查啊
然后甚至要去经理那进行审批签字,可能整个过程显得较为繁琐耗时。
如若此时我们的甲客户,去用这个打印机,进行打印资料,那么此时自己就可以搞定,无需工作人员了,同时整个过程显得简单便捷。
那么对应着来,此时我们创建销毁线程就像是在内核态中工作
创建线程池,从线程池取线程就像是在用户态中操作。
那么此时,我们一般认为,纯用户态操作会比内核态操作更为高效。
这就是为什么从线程池取线程,会比从系统申请,来的高效。
那么java标准库中,也提供了线程的线程池,让我们直接使用。
那么这个java提供的线程类是怎么样的呢?

这里的图片来自于javaSE的使用手册
我们直接看到最后一个即可
那么这些个参数是什么意思?
1.corePoolSize、maximumPoolSize
corePoolSize:核心线程数
maximumPoolSize:线程池中最大线程数
最大线程数=核心线程数+非核心线程数
即实习生和正式员工的区别。
2.keepAliveTime、TimeUint unit
keepAliveTime:当线程池中的线程数量超过核心线程数时,多余的空闲线程在终止前等待新任务的最长时间
即实习生“摸鱼”时间,也是代表非核心线程可空余的时间
TimeUint unit:即空余时间的,时间单位
库中提供的有:
NANOSECONDSMICROSECONDSMILLISECONDSSECONDSMINUTESHOURSDAYS
3.BlockingQueue<Runnable> workqueue
传递任务的阻塞队列
4.threadFactory
创建的线程工厂,参与线程具体的创建工作,通过不同线程工厂创建出的线程相当于对一些属性进行了不同的初始设置。
5.handler
拒绝策略
库中的拒绝策略:
1.AbortPolicy()
超出负荷,抛出异常
2.CalledRunsPolicy()
调用者负责处理多出来的任务
3.DiscardOldestPolicy()
剔除最老的任务,在队列中
4.DIscardPolicy()
丢弃新来的任务。
所以会发现,提高的参数挺复杂,所以Java中又对此进行了封装
提供了ExecutorService类型来接收Executors的一些工厂方法
比如:

创建固定数目的线程池

创建可扩容的线程池

创建单个使用工作线程的线程池

创建一个支持定时及周期性任务执行的线程池。
创建一个工作窃取的线程池
那么此时,由这个submit方法传入任务,进行处理
这里有个代码例子进行演示
public class Demo26 {
public static void main(String[] args) throws InterruptedException {
ExecutorService service= Executors.newFixedThreadPool(4);
for(int i=0;i<100;i++){
int id=i;
service.submit(()->{
String currentID=Thread.currentThread().getName();
System.out.println("线程个数:"+id+" "+"线程名字:"+currentID);
});
}
Thread.sleep(1000);
service.shutdown();
System.out.println("线程结束");
}
}ok,为什么这里我添加个shutdown方法呢?
这是因为,Thread创建线程默认为前台线程,此时
当for循环里的任务执行完,还没有东西去阻止它执行的时候,会是一个阻塞状态,
所以调用shutdown方法,会是这个前台线程结束
代码效果:

ok,那么接下来,小编来分享下,是如何简单实现这个线程池的。
其实思路是挺简单。
首先我们要知道的是,线程池创建好后,里面的线程就会准备就绪,准备处理新的任务。
有点和我们的阻塞队列类似,那么此时我们可以使用这个阻塞队列了
有它的好处
比如,可以全部保存其执行任务,而且还是线程安全的。
那么这个submit方法,传入任务到队列即可,构造方法中,执行任务即可
模拟线程池实现
class MyThreadPool{
//使用一个阻塞队列来存储任务
BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>(1000);
public MyThreadPool(int n) throws InterruptedException {
//n代表线程池中,线程的个数//由构造方法执行线程任务 for(int i=0;i<n;i++){
Thread t=new Thread(()->{
//不断获取任务while (true){
try {
Runnable ret=queue.take();
ret.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
public void submit(Runnable runnable) throws InterruptedException {
//提交任务到队列中
queue.put(runnable);
}
}那么介绍完这个线程池后,接着介绍另一位主角吧
定时器
那么这个定时器在我们软件开发中,也是个重要的组件。
因为某些时候某些程序重启的时候,不希望启动其他资源那么快,此时定时器的作用就来了。
那么这个定时器就可以设定时间,什么时候才开始启动其他资源。
那么这个java的标准库中,也是提供了相关的类,叫做Timer。
这个Timer类称为调度器类,用于调度任务是什么时候执行。
一般来说,这个Timer搭配TimerTask来使用。
TimerTask这个类是实现了Runnable接口。
代码示例:
public static void main(String[] args) {
Timer timer=new Timer();
TimerTask task=new TimerTask(){
@Overridepublic void run() {
System.out.println("A");
}
};
TimerTask task1=new TimerTask(){
@Overridepublic void run() {
System.out.println("B");
}
};
TimerTask task2=new TimerTask(){
@Overridepublic void run() {
System.out.println("C");
}
};
timer.schedule(task,1000);
timer.schedule(task1,2000);
timer.schedule(task2,3000);
}运行结果:

不过,通常也是可以通过其核心方法schedule来提交任务。
代码示例:
public static void main(String[] args) {
Timer timer=new Timer();
timer.schedule(new TimerTask() {
@Overridepublic void run() {
System.out.println("A");
}
},1000);
timer.schedule(new TimerTask() {
@Overridepublic void run() {
System.out.println("B");
}
},2000);
timer.schedule(new TimerTask() {
@Overridepublic void run() {
System.out.println("C");
}
},3000);
}运行结果:

ok,那么接着小编来分享下,这个定时器如何模拟实现一个定时器吧。
那么首先我们刚刚了解到,这个定时器是先执行时间短的先,后执行时间长的。
那么我们提交的任务给定的时间中,如何知道呢?
此时,我们可以使用优先级队列,因为它默认为小堆,此时呢,时间最短的,那就会放到堆顶。
接着,我们可以定义一个任务类,来存储提交的任务和保存执行的时间。
当前这个任务类得实现下一个比较接口,比如Comparable,来重写compareto方法。
任务类初始化:
//实现定时器class MyTimer implements Comparable<MyTimer>{
public Runnable runnable;
public long time;
public MyTimer(Runnable runnable,long time){
this.runnable=runnable;
//当前系统时间加上延迟时间this.time=System.currentTimeMillis()+time;
}
public void run(){
runnable.run();
}
public long getTime(){
return time;
}
@Overridepublic int compareTo(MyTimer o) {
return (int) (this.time-o.time);
}
}接着定义一个执行类,用来执行这个任务里的内容。
那么这个执行方法,通过一个线程去构造内容,并且还是在执行类的构造方法中
在这个构造方法中。
什么时候执行这个run方法呢?
当然是当前的系统时间 > 被执行任务的时间。
那么此时如若是小于,那么我们可以等待一下,等待时间为被执行任务的时间 — 当前的系统时间。
ok,要值得注意的是,这里要加锁。
这是为什么呢?
因为在多个线程中执行任务,那么此时poll()方法 和 实现schedule方法中的put()方法,可能会交错执行,出现线程安全。
所以这个执行类中,我们可以这样写
执行类:
class MyTimeTask{
public PriorityQueue<MyTimer> queue=new PriorityQueue<>();
public static Object locker=new Object();
public MyTimeTask(){
Thread t=new Thread(()->{
try {
while (true) {
synchronized (locker){
while (queue.isEmpty()){
locker.wait();
}
MyTimer current=queue.peek();
if(current.getTime() <= System.currentTimeMillis()){
//执行任务
current.run();
queue.poll();
}else {
//先不执行任务,等待时间为:任务时间-当前时间
locker.wait(current.getTime()-System.currentTimeMillis());
}
}
}
}catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.start();
}
public void schedule(Runnable runnable,long time){
synchronized (locker){
MyTimer task=new MyTimer(runnable,time);
queue.offer(task);
locker.notify();
}
}
}当队列为空时,我们等待,添加完任务后,去通知唤醒。
那么小编就分享到这里吧。
完!