【多线程】定时器和线程池

目录

1.定时器

1.1 标准库中的定时器

1.2 实现定时器

1.3 完成代码

2.线程池

2.1 标准库中的线程池

2.2 实现线程池

3. 保证线程安全的思路

4.对比线程和进程

4.1 线程的优点

 4.2进程与线程的区别


1.定时器

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

比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连。比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除). 类似于这样的场景就需要用到定时器。

1.1 标准库中的定时器

标准库中提供了一个Timer 类。Timer 类中的核心方法为 schedule。

schedule 包含两个参数 . 第一个参数指定即将要执行的任务代码 , 第二个参数指定多长时间之后
执行 ( 单位为毫秒 )。
Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("hello");
   }
}, 3000);

1.2 实现定时器

定时器的构成:
(1)一个带优先级的阻塞队列.
因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的 . 使用带 优先级的队列就可以高效的把这个 delay 最小的任务找出来。
(2) 队列中的每个元素是一个 Task 对象
(3)Task 中带有一个时间属性 , 队首元素就是即将执行的任务
(4)同时有一个 worker 线程一直扫描队首元素 , 看队首元素是否需要执行

 1) Timer 类提供的核心接口为 schedule, 用于注册一个任务, 并指定这个任务多长时间后执行。

class MyTimer {
    public void schedule(Runnable command, long after) {
 // 
   }
}

2)Task 类用于描述一个任务(作为 Timer 的内部类).。里面包含一个 Runnable 对象和一个

time(毫秒时间戳)。这个对象需要放到 优先队列 中. 因此需要实现 Comparable 接口

/创建一个类表示一个任务
class MyTask  implements Comparable<MyTask> {
    //任务具体要什么
    private Runnable runnable;
    //任务具体开始执行时间,保存任务直接的毫秒时间戳
    private long time;
    //after 是一个时间间隔,不是绝对的时间戳
    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);
    }
}

3)Timer 实例中, 通过 PriorityBlockingQueue 来组织若干个 Task 对象,通过 schedule 来往队列中插入一个个 Task 对象。

 private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<MyTask>();

    public void schedule(Runnable runnable,long delay){
        MyTask task = new MyTask(runnable,delay);
        queue.put(task);

        }
    }

4)Timer 类中存在一个 worker 线程, 一直不停的扫描队首元素, 看看是否能执行这个任务。

class Timer {
 // ... 前面的代码不变
    
    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();
    }
}

 但是当前这个代码中存在一个严重的问题, 就是 while (true) 转的太快了, 造成了无意义的 CPU 浪费。

比如第一个任务设定的是 1 min 之后执行某个逻辑. 但是这里的 while (true) 会导致每秒钟访问队 首元素几万次. 而当前距离任务执行的时间还有很久。所以需要引入wait

5) 引入一个 loker 对象, 借助该对象的 wait / notify 来解决 while (true) 的忙等问题。
class Timer {
    // 存在的意义是避免 worker 线程出现忙等的情况
    private Object loker = new Object(); 
}

修改Worker run 方法, 引入 wait, 等待一定的时间。

public MyTimer() {
        Thread t = new Thread(() ->{
            while (true) {
                try {
                    //先取出队首元素
                    MyTask task = queue.take();
                    //比较当前的时间和任务开始执行的时间,看看任务时间到了没有
                    long curTime = System.currentTimeMillis();
                    if (curTime < task.getTime()) {
                        //时间还没到,把任务放回队列里
                        queue.put(task);
                        //指定等待时间,任务的开始时间减当前时间
                        synchronized (loker) {
                            loker.wait(task.getTime() - curTime);
                        }
                    } else {
                        //时间到了,开始执行任务
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });
        t.start();
    }

6) 修改 Timer schedule 方法, 每次有新任务到来的时候唤醒一下 worker 线程。 (因为新插入的任务可能 是需要马上执行的)。

ublic void schedule(Runnable runnable,long delay){
        MyTask task = new MyTask(runnable,delay);
        queue.put(task);

        //每次任务插入成功之后, 都唤醒一下扫描线程, 让线程重新检查一下队首的任务看是否时间到要执行
        synchronized (loker) {
            loker.notify();
        }
    }

1.3 完成代码


import java.util.concurrent.PriorityBlockingQueue;

//创建一个类表示一个任务
class MyTask  implements Comparable<MyTask> {
    //任务具体要什么
    private Runnable runnable;
    //任务具体开始执行时间,保存任务直接的毫秒时间戳
    private long time;
    //after 是一个时间间隔,不是绝对的时间戳
    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<MyTask>();

    public void schedule(Runnable runnable,long delay){
        MyTask task = new MyTask(runnable,delay);
        queue.put(task);

        //每次任务插入成功之后, 都唤醒一下扫描线程, 让线程重新检查一下队首的任务看是否时间到要执行
        synchronized (loker) {
            loker.notify();
        }
    }

    private Object loker = new Object();

    public MyTimer() {
        Thread t = new Thread(() ->{
            while (true) {
                try {
                    //先取出队首元素
                    MyTask task = queue.take();
                    //比较当前的时间和任务开始执行的时间,看看任务时间到了没有
                    long curTime = System.currentTimeMillis();
                    if (curTime < task.getTime()) {
                        //时间还没到,把任务放回队列里
                        queue.put(task);
                        //指定等待时间,任务的开始时间减当前时间
                        synchronized (loker) {
                            loker.wait(task.getTime() - curTime);
                        }
                    } else {
                        //时间到了,开始执行任务
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });
        t.start();
    }
}


/**
 * 定时器存放任务
 */
public class Demo2 {

    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("开始执行任务");
            }
        },3000);
        System.out.println("main");
    }
}

执行效果

2.线程池

线程池就是一个池子里有多个线程。用到线程就直接从池子里面拿,不用频繁启动线程。线程池最大的好处就是减少每次启动、销毁线程的损耗。

2.1 标准库中的线程池

使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池。
返回值类型为 ExecutorService
通过 ExecutorService.submit 可以注册一个任务到线程池中。
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello");
   }
});
Executors 创建线程池的几种方式
newCachedThreadPool: 创建线程数目动态增长的线程池 .
newSingleThreadExecutor: 创建只包含单个线程的线程池 .
newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令, 是进阶版 Timer。

 Executors 本质上是 ThreadPoolExecutor 类的封装。

Executors 本质上是 ThreadPoolExecutor 类的封装。

2.2 实现线程池

核心操作为 submit, 将任务加入线程池中
使用 Worker 类描述一个工作线程 . 使用 Runnable 描述一个任务。
使用一个 BlockingQueue 组织所有的任务。
每个 worker 线程要做的事情 : 不停的从 BlockingQueue 中取任务并执行。
指定一下线程池中的最大线程数 maxWorkerCount; 当当前线程数超过这个最大值时 , 就不再新增
线程了。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool {
    // 1. 描述一个任务. 直接使用 Runnable, 不需要额外创建类了.
    // 2. 使用一个数据结构来组织若干个任务.
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    // 3. 描述一个线程, 工作线程的功能就是从任务队列中取任务并执行.
    static class Worker extends Thread {
        // 当前线程池中有若干个 Worker 线程 这些 线程内部 都持有了上述的任务队列.
        private BlockingQueue<Runnable> queue = null;

        public Worker(BlockingQueue<Runnable> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            // 就需要能够拿到上面的队列
            while (true) {
                try {
                    // 循环的去获取任务队列中的任务.
                    // 这里如果队列为空, 就直接阻塞. 如果队列非空, 就获取到里面的内容~~
                    Runnable runnable = queue.take();
                    // 获取到之后, 就执行任务.
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    // 4. 创建一个数据结构来组织若干个线程.
    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);
        }
    }

    // 5. 创建一个方法, 能够允许程序猿来放任务到线程池中.
    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 threadpool");
                }
            });
        }
    }
}

3. 保证线程安全的思路

1. 使用没有共享资源的模型
2. 适用共享资源只读,不写的模型
    2.1. 不需要写共享资源的模型
    2.2. 使用不可变对象
3. 直面线程安全(重点)
   3.1. 保证原子性
   3.2. 保证顺序性
   3.3. 保证可见性

4.对比线程和进程

4.1 线程的优点

1. 创建一个新线程的代价要比创建一个新进程小得多
2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3. 线程占用的资源要比进程少很多
4. 能充分利用多处理器的可并行数量
5. 在等待慢速 I/O 操作结束的同时,程序可执行其他的计算任务
6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7. I/O 密集型应用,为了提高性能,将 I/O 操作重叠。线程可以同时等待不同的 I/O 操作。

 4.2进程与线程的区别

1. 进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位。
2. 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
3. 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
4. 线程的创建、切换及终止效率更高。

猜你喜欢

转载自blog.csdn.net/m0_60494863/article/details/125266703