定时器
定时器像是一个闹钟,在一定时间之后,被唤醒并执行某个之前设定好的任务。
之前学习的 join(指定超时时间)
sleep(休眠指定时间)
都是基于系统内部的定时器,来实现的。
标准库中的定时器
标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule 。
schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒)
public class Demo23 {
public static void main(String[] args) {
Timer timer =new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello timer");
}
},3000);
System.out.println("main");
}
}
执行效果
实现定时器
Timer内部需要什么东西?
1)描述任务
创建一个专门的类来表示一个定时器中的任务(TimerTask)
//创建一个类,表示一个任务
class MyTask{
//任务具体要干什么
private Runnable runnable;
//任务具体什么时候干,保存任务要执行的毫秒级时间戳
private long time;
public MyTask(Runnable runnable, long after) {
this.runnable = runnable;
this.time = System.currentTimeMillis()+after;
}
public void run(){
runnable.run();
}
}
2)组织任务(使用一定的数据结构把一些任务给放到一起)
class MyTimer{
//定时器内部要能存放多个任务
private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
public void schedule(Runnable runnable,long delay){
MyTask task=new MyTask(runnable, delay);
queue.put(task);
}
}
3)执行时间到了的任务
需要先执行时间在靠前的任务
就需要一个线程,不停的去检查当前优先级队列的队首元素,看看说当前最靠前的这个任务是不是时间到了。
//提供一个MyTimer的构造方法
public MyTimer(){
Thread t=new Thread(() ->{
while(true){
try {
//先取出队首元素
MyTask task=queue.take();
//在比较一下看看当前这个任务时间到了没
long curTime=System.currentTimeMillis();
if(curTime < task.getTime()){
//时间没到,把任务在放到队列中
queue.put(task);
}else{
//时间到了,执行任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
上述代码存在严重缺陷!!!
第一个缺陷:MyTask没有指定比较规则
MyTask这个类的比较规则,并不是默认就存在的,这个需要手动指定,按照时间大小来比较的。
Java标准库中的集合类,很多都有一定的约束限制的,不是随便拿个类都能放到这些集合类里面去的
第二个缺陷
可以基于wait这样的机制来实现
wait有个版本,指定等待时间(不需要notify,时间到了自然唤醒)。
计算出当前时间和任务目标之间 的时间差,就等待这么长时间。
那么既然是指定一个等待时间,,为什么不直接使用sleep呢?而是要用wait?
sleep不能被中途唤醒
wait能够被中途唤醒
在等待过程中,可能要插入新的任务,新的任务是可能出现在之前所有任务的最前面的,在schedule操作中,就需要加上一个notify操作。
因此最终的代码是:
//创建一个类,表示一个任务
class MyTask implements Comparable<MyTask>{
//任务具体要干什么
private Runnable runnable;
//任务具体什么时候干,保存任务要执行的毫秒级时间戳
private long time;
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis()+delay;
}
public void run(){
runnable.run();
}
public long getTime(){
return time;
}
@Override
public int compareTo(MyTask o) {
return (int) (this.time-o.time);
}
}
class MyTimer{
//定时器内部要能存放多个任务
private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
public void schedule(Runnable runnable,long delay){
MyTask task=new MyTask(runnable, delay);
queue.put(task);
//每次任务插入成功之后,都唤醒一下扫描线程,让线程重新检查一下队首元素的任务是否时间到要执行
synchronized (locker){
locker.notify();
}
}
private Object locker=new Object();
//提供一个MyTimer的构造方法
public MyTimer(){
Thread t=new Thread(() ->{
while(true){
try {
//先取出队首元素
MyTask task=queue.take();
//在比较一下看看当前这个任务时间到了没
long curTime=System.currentTimeMillis();
if(curTime < task.getTime()){
//时间没到,把任务在放到队列中
//指定一个等待时间
synchronized (locker){
locker.wait(task.getTime()-curTime);
}
queue.put(task);
}else{
//时间到了,执行任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
public class Demo24 {
public static void main(String[] args) {
MyTimer timer=new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello timer");
}
},3000);
System.out.println("hello main");
}
}
线程池
进程,比较重,频繁创建销毁,开销大,解决方案:进程池或线程。
线程,虽然比进程轻了,但是如果创建销毁的频率进一步增加,仍然会发现开销还是有的,解决方案:线程池或协程。
把线程提前创建好,放到池子里,后面需要使用线程,直接从池子里取,就不必从系统这边申请了。线程用完,也不是还给系统,而是放回池子里,以备下次再用。
这回创建销毁过程,速度就更快了。
为什么认为线程放到池子里,就比从系统这边申请释放更快呢??
Java标准库的线程池
有一个程序,这个程序要并发的 / 多线程的来完成一些任务,如果使用线程池的话,这里的线程数设为多少合适?
正确的做法:要通过性能测试的方式,找到合适的值
例如:写一个服务器程序,服务器里通过线程池,多线程的处理用户请求。
就可以对这个服务器进行性能测试,比如构造一些请求,发送给服务器,要测试性能,这里的请求就需要构造很多,比如每秒发送500 / 1000/2000…,根据实际的业务场景,构造一个合适的值。
根据这里不同的线程池的线程数,来观察,程序处理任务的速度,程序持有的CPU的占用率。
当线程数多了,整体的速度会快,CPU占用率也会高
当线程数少了,整体的速度会慢,CPU占用率也会下降。
需要找到一个让程序速度能接受,并且CPU占用率也合理这样的平衡点。
不同类型的程序,因为单个任务,里面的CPU上计算的时间和阻塞的时间分布是不同的。因此,只说一个数字是不靠谱的。
搞多线程,就是为了让程序跑的更快,那为什么考虑不让CPU占用率太高?
对于线上服务器来说,要留有一定的冗余,随时应对一些可能的突发情况(例如,请求突然暴涨)
如果本身已经把CPU快占完了,这时候突然来一波请求的峰值,此时服务器可能直接就挂了。
标准库中还提供了一个简化版本的线程池
Executors
本质是针对ThreadPollExecutor进行封装,提供了一些默认参数。
public class Demo25 {
public static void main(String[] args) {
//创建一个固定线程数目的线程池,参数指定了线程个数
ExecutorService pool=Executors.newFixedThreadPool(10);
//创建一个自动扩容的线程池,会根据任务自动进行扩容
//Executors.newCachedThreadPool();
//创建一个只有一个线程的线程池
// Executors.newSingleThreadExecutor();
//创建一个带有定时器功能的线程池,类似于Timer
// Executors.newScheduledThreadPool();
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello threadpool");
}
});
}
}
实现线程池
线程池中有什么?
1.先能够描述任务(直接使用Runnable)
2.需要组织任务(直接使用BlockingQueue)
3.能够描述工作线程
4.还需要组织这些线程
5.需要往线程池里添加任务
class MyThreadPool{
//1.描述一个任务,直接使用Runnable,不需要额外创建类了
//2.使用一个数据结构来组织若干个任务
private BlockingDeque<Runnable> queue=new LinkedBlockingDeque<>();
//3.描述一个线程,工作线程的功能就是从任务队列中取任务并执行
static class Worker extends Thread{
//当前线程池中有若干个worker线程,这些线程内部,都持有了上述的任务队列
private BlockingDeque<Runnable> queue=null;
public Worker(BlockingDeque<Runnable> queue){
this.queue=queue;
}
@Override
public void run() {
while(true){
//就需要能够拿到上面的队列
try {
//循环的获取任务队列中的任务
//这里如果队列为空,就直接阻塞,如果队列不为空,就获取到里面的内容
Runnable runnable=queue.take();
//获取到之后,就执行任务
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//创建哪一个数据结构来组织若干个线程
private List<Thread> workers=new ArrayList<>();
public MyThreadPool(int n){
//在构造方法中,创建若干个线程,放到上述的数组中
for(int i=0;i<n;i++){
Worker worker=new Worker(queue);
worker.start();
workers.add(worker);
}
}
//创建一个方法,能够让程序员来放任务到线程池
public void submit(Runnable runnable){
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Demo26 {
public static void main(String[] args) {
MyThreadPool pool=new MyThreadPool(10);
for(int i=0;i<100;i++){
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello thraedpool");
}
});
}
}
}