文章目录
定时器&线程池
一、定时器
- 类似一个"闹钟",约定一个时间,时间到达之后,执行某个代码逻辑
- 在进行网络通信中很常见
客户端在发出请求后,就要等待响应。但是如果服务器迟迟没有响应,客户端不能无限的等下去,需要有一个最大的期限,等时间到了之后再次进行判断。而“等待的最大时间”就可以通过定时器的方式来实现。
1.标准库中的定时器
- import java.util.Timer 。 Timer这个类是在util里的
public static void main(String[] args) {Timer timer = new Timer();//给定时器安排了一个任务,预订在2秒后执行。timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("执行定时器");}},2000);System.out.println("程序启动");}程序启动
执行定时器//两秒后
- schedule()方法的第一个参数:使用匿名内部类,创建一个TimerTask()实例,重写当中的run方法,通过run方法来描述任务的详细情况
TimerTask类本身就实现了Runnable接口。从而重写run方法
-
schedule方法的第二个参数:填写的时间,表示当前这个任务以此时此刻为基准,往后推X时间后再执行该任务。
当主线程在执行schedule方法的时候,就会把任务放进timer对象中。同时,timer当中,存在一个扫描线程。一旦时间到了,扫描线程就会执行刚才安排的任务。换句话说,timer当中的任务,是有当中的扫描线程来执行的。当任务结束时,扫描线程并未结束,还在等待执行后续可能安排的任务
public static void main(String[] args) {Timer timer = new Timer();//给定时器安排了一个任务,预订在2秒后执行。timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("执行定时器2");}},2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("执行定时器3");}},3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("执行定时器1");}},1000);System.out.println("程序启动");}程序启动
执行定时器1
执行定时器2
执行定时器3
2.实现定时器
要有一个扫描线程,扫描任务是否到达时间执行
要有一个优先级对列来保存任务:o(1),优先取时间最小的任务执行。
创建一个类,通过类的对象来描述任务(任务内容、任务时间)
class MyTimerTask implements Comparable<MyTimerTask> {//描述一个任务private Runnable runnable;//要执行的任务private long time;MyTimerTask(Runnable runnable, long delay) {this.runnable = runnable;this.time = System.currentTimeMillis() + delay;//当前的时间戳+要延迟的时间}public long getTime() {return time;}public Runnable getRunnable() {return runnable;}@Overridepublic int compareTo(MyTimerTask o) {//保证队首的任务是是最小的时间return (int) (this.time - o.time);}
}
- 如果队列为空了,就需要 调用wait来进行阻塞,同时需要搭配synchronized来使用。因为wait的操作有三个:1在前提有锁的情况下,释放锁。2.等待通知。3.通知到来之后,进行唤醒,同时重新拿到锁。
- 对应的,也需要在调用schedule方法添加任务时,对之前因为队列为空而等待的wait进行唤醒(notify)
同时:由于schedule方法和扫描线程都会操作队列。存在线程安全问题。因此要加锁。
//自己实现的定时器
class MyTimer {private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();private Object locker = new Object();//锁对象//优先级队列存任务public void schedule(Runnable runnable, long delay) {//把要完成的任务和延迟的时间构造成一个任务对象,存进优先级队列synchronized (locker) {queue.offer(new MyTimerTask(runnable, delay));locker.notify();//唤醒空对列的wait}}public MyTimer() {//创建一个扫描线程Thread t = new Thread(() -> {while (true) {//不停扫描队首元素try {synchronized (locker) {while (queue.isEmpty()) {//使用wait进行等待locker.wait();//需要由添加任务的时候唤醒}MyTimerTask task = queue.peek();//比较一下当前时间是否可以执行任务long curTime = System.currentTimeMillis();if (curTime >= task.getTime()) {task.getRunnable().run();//执行任务queue.poll();}else {locker.wait(task.getTime()-curTime);}}} catch (InterruptedException e) {e.printStackTrace();}}});t.start(); }
}
由于扫描线程是while(true)循环,在队列不为空的情况下,不会进入等待。会一直持续循环,直到当前时间到达设定的时间为止。在此期间并没有进行任何操作,只是不断的对表忙等,但是消耗了很多cpu资源。所以在第一次判断时间时,在else中,当任务时间还没到的时候,进行wait阻塞,此时线程不会在CPU上调度,避免了忙等。
- 同时:如果schedule方法添加了一个比当前待任务要早执行的任务时,schedule方法内部的notify就会唤醒这个带参数的wait,让循环再执行一次,重新拿到新的队首元素,跟新wait的时间。
也就是说:当队列为空时进行阻塞等待,调用一次schedule方法,其中的notify可以唤醒wait。而在等待要执行的任务时,调用schedule方法存入一个任务。其中的notify会唤醒带参数的wait,再次循环,重新获取队首任务,更新等待时间。
二、线程池
1.线程池的概念
由于进程创建/销毁,太重量了(比较慢)才引进了线程的概念,但是如果进一步提高创建销毁的频率,线程的开销就不容忽视。
有两种办法来提高线程的效率:
-
1.协程(轻量级线程)
线程省略的是资源创建的过程。先比与线程,协程省略了系统调度的过程。(程序员手动调度)
Java虽然标准库中没有协程,但是一些第三方库实现了协程。
-
2.线程池 同样可以提高线程的效率,同时避免了全面的修改。减少了每次创建、销毁线程的损耗
线程池:
在使用第一个线程的时候,提前把后续多个线程创建好,放进池中。后续如果想使用多个线程,不必重新创建,直接从池中拿过来用,降低创建线程的开销。
而从池中取这个操作,是用户态的操作。而创建一个线程,则是用户态+内核态 相互配合来完成的操作。
如果一段程序在系统内核中执行,就被称为内核态。否则就是用户态。而一个操作系统则是由操作系统内核和配套的应用程序构成。创建一个线程,就会需要调用系统API,进入到内核中,按照内核态的方式来完成一系列操作。操作系统内核是要对所有的进程提供服务的,当要创建一个线程的时候,内核难免会被干扰做其他事情,不可控,不可预期。而用户态的操作不涉及系统内核,可控可预期。因此线程池的操作要比创建线程更高效。
2.标准库当中的线程池
ExecutorService service = Executors.newCachedThreadPool()//可执行的服务
- 线程池对象不是直接new出来的,而是通过专门的方法,返回了一个线程池对象。这种写法叫做工厂模式
工厂模式
工厂模式是一种常见的设计模式
通常在使用new关键字创建对象时,会触发类的构造方法来实例对象。但是构造方法存在一定的局限性,工厂模式就可以解决构造方法的不足
class Point{public Point(double x,double y){//通过笛卡尔坐标系构造点}public Point(double r,double a){//使用极坐标构造点}
}
- 此时,在这个类中,两个构造方法采用的是两种截然不同的方式,但是构造方法的方法名必须是类名,不同的构造方法只能通过重载来进行区分(重载要求参数类型/个数不同)。但是此时,两个构造方法的参数列表相同,没有完成重载,编译失败。
- 而采用工程模式,使用普通的方法,代替构造方法来完成初始化工作,普通方法就可以使用不同方法名来进行区分。
class PointFactory{public static Point makePointByXY(double x,double y){Point p =new Point();p.setX(x){}p.setY(y){}return p;}public static Point makePointByRA(double x,double y){return p;}
}Point p = PointFactory.makePointByXY(15,20);
//就类似于线程池的创建
ExecutorService service = Executors.newCachedThreadPool()//工厂类 //工厂方法
- 通过方法名来完成区分
Executors 创建线程池
1.自适应线程池
newCachedThreadPool,可以动态适应。随着往线程池中添加任务,这个线程池中的线程会根据需要自动被创建出来,并且使用后不会立即销毁,会在池中保留一定的时间,以备后续再次使用
ExecutorService service = Executors.newCachedThreadPool();//可以动态适应//cached:缓存,用过之后不着急释放,先保留,
2.固定数量线程池
ExecutorService service1 = Executors.newFixedThreadPool(10);//固定的
3.只有单个线程的线程池
ExecutorService service2 = Executors.newSingleThreadExecutor();
4.设定延迟时间后执行命令的线程池
ExecutorService service3 = Executors.newScheduledThreadPool(5);
//类似于定时器,但是不在是一个扫面线程在执行任务,而是变成了多个线程来执行任务。
ThreadPoolExecutor 类
Executors 本质上是 ThreadPoolExecutor 类的封装。ThreadPoolExecutor 类的功能非常丰富,提供了很多参数。标准库当中的几个工厂方法,其实就是给这个类填写了不同的参数来构造线程池
ThreadPoolExecutor 类的核心方法有两个:
1.注册任务
ExecutorService service1 = Executors.newFixedThreadPool(10);service1.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");}});
2.构造
ThreadPoolExecutor当中的构造参数有很多(面试题)
JUC这个包就是和并发编程相关的内容(多线程)
ThreadPoolExecutor的构造方法有四个版本,其中最后一个版本参数最多,可以涵盖其余方法的参数
int corePoolSize(核心线程数)int maximumPoolSize(最大线程数), 描述线程的数目
这个线程池中,线程的数目是可以动态变化的,线程数变化的范围就是 [ corePoolSize, maximumPoolSize ]
核心线程数(正式员工数量) ;最大线程数(正式员工数量 + 实习生的数量)
实习生不允许摸鱼,活多了招人,少了裁人。但是不会动正式员工
在满足效率的同时,又可以避免过多的系统开销
BlockingQueue 阻塞队列:可以根据需要灵活选择队列,需要有优先级,设置PriorityBlockingQueue
如果不需要优先级,并且任务数量相对恒定,使用ArrayBlockingQueue。如果任务数量变动较大,
使用LinkedBlockingQueue.
使用工厂类来创建线程,主要是为了在创建的过程中,对线程的属性做一些设置。如果手动创建线程,就需要手动设置这些属性,所以用工厂方法进行封装。
RejectedExecutionHandler handler 线程池的拒绝策略,一个线程池的容量是有限的,达到上限后,采用不同的拒绝策略会有不同的效果。(4种)
使用线程池,需要设置线程的数目
设置多少线程合适?
在接触到实际的项目代码之前,是无法确定的。
一个线程,要执行的代码主要有两大类:
1.CPU密集型:代码中主要的逻辑是进行算数运算/逻辑判断
2.IO密集型:代码里主要进行IO操作。(网络通信、写硬盘、读硬盘)
假设一个线程的所有代码都是CPU密集型代码,线程池中的线程数量不应该超过N(CPU核心数),设置的比N大,cpu吃满了,无法提高效率,此时添加更多的线程反而增加调度的开销。
假设一个线程的所有代码都是IO密集型的,这个时候不吃CPU,此时设置的线程数,就可以超过N.一个核心可以通过调度的方式,来并发执行。
代码不同,线程池的线程数目设置就不同。正确的设置方法:使用实验的方式,对程序进行性能测试。在测试的过程中,尝试修改不同的线程池的线程数目。看哪种情况最符合需求。
3.实现线程池
class MyThreadPool{//任务队列private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);//通过submit方法,将任务添加到队列中public void submit(Runnable runnable) throws InterruptedException {//次处的拒绝策略相当于第5种策略:阻塞等待queue.put(runnable);}public MyThreadPool(int n){//创建出n个线程,负责执行上述队列中的任务for (int i = 0; i < n; i++) {Thread t = new Thread(()->{//让线程,从队列中消费任务,并进行执行try {Runnable runnable = queue.take();runnable.run();} catch (InterruptedException e) {e.printStackTrace();}});t.start();}}
}
public class MakeMyThreadPoll {public static void main(String[] args) throws InterruptedException {MyThreadPool myThreadPool = new MyThreadPool(4);for (int i = 0; i < 1000; i++) {int id = i;myThreadPool.submit(new Runnable() {@Overridepublic void run() {System.out.println("执行任务 " + id);//防止匿名内部类的变量捕获//此时捕获的是id,id没有人进行修改。每次循环都创建了新的id}});}}
点击移步博客主页,欢迎光临~