【Java|多线程与高并发】定时器(Timer)详解

简介: 在Java中,定时器Timer类是用于执行定时任务的工具类。它允许你安排一个任务在未来的某个时间点执行,或者以固定的时间间隔重复执行。

1. 前言

在Java中,定时器Timer类是用于执行定时任务的工具类。它允许你安排一个任务在未来的某个时间点执行,或者以固定的时间间隔重复执行。


在服务器开发中,客户端向服务器发送请求,然后等待服务器响应. 但服务器什么时候返回响应,并不确定. 但也不能让客户端一直等下去, 如果一直死等,就没有意义了. 因此通常客户端会通过定时器设置一个"等待的最长时间".


604b0f608bfd431ba76d4549f3e4b96e.gif


2. 定时器的基本使用

Java的标准库库中就给我们提供了一个定时器Timer类


可以看到Timer这个类在很多包里面都有,注意要选择java.util里的

b1a8e797499f47ae91ce9f129c93c747.png



其中在Timer类中有一个十分重要的方法- schedule()方法


e512a9d29c2e416e868a1ab6c1b2e5ac.png

形参:


task:要执行的任务,必须是TimerTask的子类,可以通过继承TimerTask类并重写run()方法来定义具体的任务逻辑。

time:指定任务执行的时间,类型为java.util.Date。

当然一个Timer类中也可以执行设置多个任务.


示例:

public class Demo17 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("1s!");
            }
        },1000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("2s!");
            }
        },2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("3s!");
            }
        },3000);
    }
}

运行结果:

28470399c42a468599c76d5debe72c68.png


仔细观察运行结果,会发现这个程序有些问题,为什么程序执行完了,进程没有退出呢?


是因为Timer内部需要一组线程来执行注册任务,这里的线程是前台线程,会影响进程的退出


3. 实现定时器

实现定时器,最主要的就是实现里面的schedule方法

class MyTask{
    // 要执行的任务
    private Runnable runnable;
    // 时间
    private long time;
    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + time;
    }
}
public class MyTimer {
    public void schedule(Runnable runnable, long time){
        MyTask myTask = new MyTask(runnable,time);
    }
}

System.currentTimeMillis()是Java中的一个静态方法,用于获取当前时间的毫秒数。


描述一个任务,以及多久后执行定时器的第一步完成了


接下来就是如何让这个定时器能够管理多个任务,例如上述示例中输出1s,2s,3s的那个示例一样


关于如何管理这些任务,我们肯定是想让设置时间短的任务先执行,但是在设置任务时,不一定会按照时间从小到大的顺序去进行放入. 这时候就要使用到 优先级队列(PriorityQueue)


但是优先级队列并不是线程安全的, 在多线程环境下使用优先级队列可能会出现问题,我们可以使用阻塞队列

不要忘了,我们可以创建一个带有优先级的阻塞队列


09a865d1bd5f41e79ebcf054582efcb2.png


将任务添加到阻塞队列中即可.


但是优先级队列的对象的类型必须是可比较的. 我们可以让Mytask实现Comparable接口,实现里面的compareTo方法. 比较的规则就是时间,时间小的优先级高.


接下来就要检查队首任务的时间是否到了,时间到了就要执行任务. 可以单独创建一个扫描线程来进行检查.


完整代码:


import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
class MyTask implements Comparable<MyTask>{
    // 要执行的任务
    private Runnable runnable;
    // 时间
    private long time;
    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + time;
    }
    public Runnable getRunnable() {
        return runnable;
    }
    public long getTime() {
        return time;
    }
    @Override
    public int compareTo(MyTask o) {
        return (int) (this.time - o.time);
    }
}
public class MyTimer {
    private BlockingQueue<MyTask> blockingQueue = new PriorityBlockingQueue<>();
    public MyTimer() {
        Thread t = new Thread(()->{
           while(true){
               try {
                   MyTask myTask = blockingQueue.take();
                   // 当前时间是否大于等于要执行任务的时间
                   if (System.currentTimeMillis() >= myTask.getTime()){
                       // 时间到了 执行任务
                       myTask.getRunnable().run();
                   }else {
                       // 时间没到,再把任务放回阻塞队列中
                       blockingQueue.put(myTask);
                   }
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
        t.start();
    }
    public void schedule(Runnable runnable, long time) throws InterruptedException {
        MyTask myTask = new MyTask(runnable,time);
        blockingQueue.put(myTask);
    }
}

测试代码:


public class Demo18 {
    public static void main(String[] args) throws InterruptedException {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("2s");
            }
        },2000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("1s");
            }
        },1000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("3s");
            }
        },3000);
    }
}

运行结果:

200a1945404e46d2a8ba348ec4546276.png



结果没有问题.


4. 优化上述的定时器代码

但仔细思考上述代码中还存在一个问题:

d9b489a59e8347f5ba4a1457cef39029.png



这里的条件是while(true),说明程序会一直进行这里的循环, 这也是"忙等"


"忙等"是指一个线程在等待某个条件满足时,不断地进行无效的循环检查,而不释放CPU资源给其他线程执行。这种方式会浪费CPU资源,并且可能导致性能下降。


针对这个问题,我们可以使用 wait和notify来解决这个问题


通过使用wait和notify,对MyTimer这个类进行优化:


public class MyTimer {
    private BlockingQueue<MyTask> blockingQueue = new PriorityBlockingQueue<>();
    private Object locker = new Object();
    public MyTimer() {
        Thread t = new Thread(()->{
           while(true){
               try {
                   MyTask myTask = blockingQueue.take();
                   // 当前时间是否大于等于要执行任务的时间
                   if (System.currentTimeMillis() >= myTask.getTime()){
                       // 时间到了 执行任务
                       myTask.getRunnable().run();
                   }else {
                       // 时间没到,再把任务放回阻塞队列中
                       blockingQueue.put(myTask);
                       // 进行等待
                       synchronized (locker) {
                           locker.wait(myTask.getTime()-System.currentTimeMillis());
                       }
                   }
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
        t.start();
    }
    public void schedule(Runnable runnable, long time) throws InterruptedException {
        MyTask myTask = new MyTask(runnable,time);
        blockingQueue.put(myTask);
        synchronized (locker) {
            locker.notify();
        }
    }
}

虽然解决了"忙等"问题,但是又带来了新的问题.


如果扫描线程再取出队首任务(10分钟后要执行)时,线程切换,执行schedule方法,新增任务(5分钟后执行)然后执行notify,但此时并没有通知线程并没有意义,因为扫描线程刚执行完take,并没有执行到wait,然后扫描线程继续执行,进行wait,等待10分钟. 这样就会把刚才新增的5分钟后执行的任务给错过了.


对于上述问题 产生的原因还是因为"锁"的粒度不够大, 这些操作不是原子的,只需放大锁的粒度即可


d32551cca00b4b31941cdb1e952d06fe.png

public class MyTimer {
    private BlockingQueue<MyTask> blockingQueue = new PriorityBlockingQueue<>();
    private Object locker = new Object();
    public MyTimer() {
        Thread t = new Thread(()->{
           while(true){
               try {
                   synchronized (locker) {
                       MyTask myTask = blockingQueue.take();
                       // 当前时间是否大于等于要执行任务的时间
                       if (System.currentTimeMillis() >= myTask.getTime()){
                           // 时间到了 执行任务
                           myTask.getRunnable().run();
                       }else {
                           // 时间没到,再把任务放回阻塞队列中
                           blockingQueue.put(myTask);
                           // 进行等待
                           locker.wait(myTask.getTime()-System.currentTimeMillis());
                       }
                   }
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
        t.start();
    }
    public void schedule(Runnable runnable, long time) throws InterruptedException {
        MyTask myTask = new MyTask(runnable,time);
        blockingQueue.put(myTask);
        synchronized (locker) {
            locker.notify();
        }
    }
}




上述为了解决"忙等"问题,使用wait和notify进行优化,而在优化过程因为synchronized加锁的范围不一样,又带来了新的问题. 因此多线程问题很复杂,加锁的范围,线程的切换都会影响程序的执行效果.


5. 总结

文章主要介绍了定时器的基本使用,以及自定义实现定时器,实现一个定时器并不难.但如果要想将定时器实现的更好,也不是一件容易的事. 毕竟多线程环境中,很容易出现各种意想不到的问题.

26c23245a3aa45aeb759de6a3964c906.gif

相关文章
|
8天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
28 9
|
11天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
8天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
11天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
26 3
|
10天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
11天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
22 1
|
11天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
44 1
C++ 多线程之初识多线程
|
26天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
19 3
|
26天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
16 2

热门文章

最新文章

  • 1
    高并发场景下,到底先更新缓存还是先更新数据库?
    67
  • 2
    Java面试题:解释Java NIO与BIO的区别,以及NIO的优势和应用场景。如何在高并发应用中实现NIO?
    75
  • 3
    Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
    68
  • 4
    Java面试题:如何实现一个线程安全的单例模式,并确保其在高并发环境下的内存管理效率?如何使用CyclicBarrier来实现一个多阶段的数据处理任务,确保所有阶段的数据一致性?
    64
  • 5
    Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
    55
  • 6
    Java面试题:假设你正在开发一个Java后端服务,该服务需要处理高并发的用户请求,并且对内存使用效率有严格的要求,在多线程环境下,如何确保共享资源的线程安全?
    71
  • 7
    在Java中实现高并发的数据访问控制
    42
  • 8
    使用Java构建一个高并发的网络服务
    32
  • 9
    微服务06----Eureka注册中心,微服务的两大服务,订单服务和用户服务,订单服务需要远程调用我们的用,户服务,消费者,如果环境改变,硬编码问题就会随之产生,为了应对高并发,我们可能会部署成一个集
    37
  • 10
    如何设计一个秒杀系统,(高并发高可用分布式集群)
    129