当前位置:   article > 正文

java多线程基础——定时器与线程池_java 定时线程池

java 定时线程池

目录

1.定时器

2.线程池


1.定时器

1.1 概念

定时器是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定 好的代码。

定时器是一种实际开发中非常常用的组件

比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.

比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除).

类似于这样的场景就需要用到定时器.

在Java标准库中也提供了一个Timer类,核心方法为 schedule 。schedu方法可以理解为为定时器分配任务。schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后 执行 (单位为毫秒).。我们下面简单看一下他的用法:

  1. Timer timer = new Timer();
  2. timer.schedule(new TimerTask() {
  3. @Override
  4. public void run() {
  5. System.out.println("hello");
  6. }
  7. }, 3000);

TimeTask实际上是实现了Runnable接口的一个类,所以要重写run方法。 

1.2 Timer和sleep的区别

大家看到Timer能够在指定的一段时间后执行某个任务,很自然会想到Thread.sleep。虽然sleep方法的确也能在一段时间后再去执行某个任务,但是在sleep期间线程处于阻塞状态,其他任务也无法进行,而Timer等待期间线程可以完成其他工作,提高cpu利用率。

1.3 手动实现一个简单的Timer 

首先我们明确一下设计思路

首先定时器内是可以加入多个任务等待执行的,所以我们需要一个容器储存,既然我们需要按照时间顺序来执行任务我们很自然会想到PriorityQueue(优先级队列)这个东西,但是我们是处于多线程的状态,还需考虑线程安全问题,结合我们上次讲的BlockingQueue(阻塞队列),其实我们java内是自带了PriorityBlockingQueue(优先级阻塞队列)供我们使用。

其次我们的每个任务是需要通过一定的方式来描述出来,这里我们自定义一个类来描述

同时我们需要一个线程,通过这个线程来扫描定时器内部任务,执行其中时间到了的任务。这里我们在构造方法里面来实现。实现Timer类一被创建就能够即使扫描将要执行的任务。 

当然由于多线程的线程安全问题,我们还需要使用synchronized将一些关键操作锁住,我们下面结合代码再分析一下。

  1. import java.util.Timer;
  2. import java.util.concurrent.PriorityBlockingQueue;
  3. class MyTask implements Comparable<MyTask>{
  4. //任务要干什么
  5. private Runnable command;
  6. //等待时长
  7. private long time;
  8. public MyTask(Runnable command,long after){
  9. this.command=command;
  10. this.time=System.currentTimeMillis()+after;
  11. }
  12. public void run(){
  13. command.run();
  14. }
  15. public long getTime(){
  16. return time;
  17. }
  18. @Override
  19. public int compareTo(MyTask o) {
  20. //时间小的在前面
  21. return (int)(this.time-o.time);
  22. }
  23. }
  24. class MyTimer {
  25. private Object locker=new Object();
  26. private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
  27. public void schedule(Runnable command,long after){
  28. MyTask myTask=new MyTask(command,after);
  29. synchronized (locker) {
  30. queue.put(myTask);
  31. locker.notify();
  32. }
  33. }
  34. public MyTimer(){
  35. Thread t=new Thread(()->{
  36. while(true){//不断扫描
  37. try{
  38. synchronized (locker) {
  39. //队列为空则wait
  40. if(queue.isEmpty()){
  41. locker.wait();
  42. }
  43. MyTask myTask = queue.take();
  44. long curTime = System.currentTimeMillis();
  45. if (myTask.getTime() > curTime) {
  46. // 时间还没到, 塞回到队列中
  47. queue.put(myTask);
  48. locker.wait(myTask.getTime()-curTime);
  49. } else {
  50. // 时间到了~~, 直接执行任务
  51. myTask.run();
  52. }
  53. }
  54. } catch (InterruptedException e) {
  55. e.printStackTrace();
  56. }
  57. }
  58. });
  59. t.start();
  60. }
  61. }
  62. public class MyTimerDemo {
  63. public static void main(String[] args) {
  64. MyTimer myTimer=new MyTimer();
  65. myTimer.schedule(new Runnable() {
  66. @Override
  67. public void run() {
  68. System.out.println("test");
  69. }
  70. },3000);
  71. }
  72. }

首先我们需要注意的是,由于PriorityBlockingQueue涉及到比较操作,所以我们需要在MyTask类内部实现Comparable接口。

之后我们需要对入队和出队等一系列操作加锁,否则可能会出现下面的情况:

假设现在9点,我们有一个任务需要在10点执行,此时我们的线程准备进入等待1h状态,但是此时突然有一个9点30需要执行的任务插入,执行notify操作,但由于操作没有加锁,可能导致notify比wait早一点执行导致实际上并没有唤醒线程。

最后,为什么队列为空时需要进行wait操作?

当刚创建队列还未添加任务时,此时队列为空,假如不进行等待操作,此时就会直接进入加锁操作,开始执行take,由于队列为空进入阻塞状态,且并没有释放锁。此时就会来到schedule方法准备执行put方法,但是在进行加锁操作时发现锁已经被占用,而另一边也因为队列为空一直处于阻塞状态无法释放锁,就形成了死锁

2.线程池 

2.1 概念

之前我们学习过字符串常量池,它的存在是为了减少系统资源的开销。而线程池同样如此,虽然创建线程 / 销毁线程的开销已经比较小了,但是在面对非常多线程的情况下计算机资源还是略显捉急,所以就有了线程池。线程池最大的好处就是减少每次启动、销毁线程的损耗。

那为啥把线程放进池子就比从系统这里创建线程要来的快呢?

实际上,从池子里去涉及的是纯用户态操作

而通过系统来创建,设计内核态操作

通常我们认为,牵扯到内核态的操作要比纯用户态更加低效 

2.2 标准库中的线程池 

使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.

返回值类型为 ExecutorService

通过 ExecutorService.submit 可以注册一个任务到线程池中.

  1. ExecutorService pool = Executors.newFixedThreadPool(10);
  2. pool.submit(new Runnable() {
  3. @Override
  4. public void run() {
  5. System.out.println("hello");
  6. }
  7. });

 Executors 创建线程池的几种方式:

1.newFixedThreadPool: 创建固定线程数的线程池

2.newCachedThreadPool: 创建线程数目动态增长的线程池.

3.newSingleThreadExecutor: 创建只包含单个线程的线程池.

4.newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

2.3 工厂模式 

不知道大家有没有注意到我们ExecutorService的初始化方式好像和之前的其他类不太一样,是使用Executors来初始化的,而不是原本的构造方法。Executors 本质上是 ThreadPoolExecutor 类的封装。这种在原本的构造方法外进行包装作用的方法就叫做工厂方法,这种模式也被称作工厂模式。而工厂模式的意义实际上是为了突破一些构造方法的限制。比如下面的例子。

假如我们想用笛卡尔坐标系和极坐标系两种方式来表示一个点,可能会写出这样的代码

  1. class point{
  2. public Point(double x,double y){};//笛卡尔坐标系
  3. public Point(double x,double y){};//极坐标系
  4. }

我们想要重载构造方法,但是由于参数都是一样的,并且构造方法要求同名,所以很明显上面的方法会编译错误。

  1. public static PointMakeByXY(double x,double y){
  2. Point p=new Point();
  3. p.setX(x);
  4. p.setY(y);
  5. return p;
  6. }
  7. public static PointMakeByRA(double r,double a){
  8. Point p=new Point();
  9. p.setR(r);
  10. p.setA(a);
  11. return p;
  12. }

但是我们可以把它们包装成一个静态方法,这样就可以通过类名调用。

2.4 简单实现一个线程池 

这里我们只是为了帮助大家理解线程池概念,所以自己动手实现一个非常简单的线程池,实际使用我们还是使用java库自带的。

由于插入的任务一下子可能很多,所以我们采用阻塞队列存储

  1. import java.util.concurrent.BlockingQueue;
  2. import java.util.concurrent.LinkedBlockingQueue;
  3. class MyThreadPool{
  4. private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();
  5. // 核心方法, 往线程池里插入任务
  6. public void submit(Runnable runnable){
  7. try {
  8. queue.put(runnable);
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. }
  13. // 设定线程池里有几个线程
  14. public MyThreadPool(int n){
  15. for(int i=0;i<n;i++){
  16. Thread t=new Thread(()->{
  17. while(!Thread.currentThread().isInterrupted()){
  18. try {
  19. Runnable runnable=queue.take();//获取任务
  20. runnable.run();
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. break;
  24. }
  25. }
  26. });
  27. t.start();
  28. }
  29. }
  30. }
  31. public class MyThreadPoolDemo {
  32. public static void main(String[] args) {
  33. MyThreadPool myThreadPool = new MyThreadPool(10);//初始化时所有线程处于WAITING
  34. for (int i = 0; i < 100; i++) {
  35. myThreadPool.submit(new Runnable() {
  36. @Override
  37. public void run() {
  38. System.out.println("hello");
  39. }
  40. });
  41. }
  42. }
  43. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/495556
推荐阅读
相关标签
  

闽ICP备14008679号