• 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{
    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 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 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
    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. }

  • 相关阅读:
    Jvm.分析工具(jconsole,jvisualvm,arthas,jprofiler,mat)
    Amazon EC2的出现,是时代的选择了它,还是它选择了时代
    iOS“超级签名”绕过App Store作弊解决方案
    Javascript中简单数据类型的转换
    【LeetCode】生命游戏
    8 年 Java 开发含泪刷题,架构岗现在好难进,有点崩溃
    【MySQL】索引特性
    多层感知器(神经网络)与激活函数
    linux的审计功能(audit)
    git revert 撤销之前的提交
  • 原文地址:https://blog.csdn.net/weixin_60778429/article/details/126063456