JavaEE——No.2 多线程案例

JavaEE传送门

JavaEE

JavaEE——No.2 线程安全问题

JavaEE——No.1 多线程案例



多线程案例

1. 定时器

定时器, 类似于一个 "闹钟". 代码中的定时器, 通常都是设定 “多长时间之后, 执行某个动作”.

例如: 客户端发送请求之后, 就需要等待服务器响应. 如果服务器一直不响应, 客户端也不能一直死等下去. 客户端经常会设置一个超时时间, 这时就可以使用定时器来实现.

标准库中的定时器

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

  • schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后

    执行 (单位为毫秒).

public class Test {
    public static void main(String[] args) {
        //标准库的计时器
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("7:00了,该起床了");
            }
        },3000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("9:00了,该起床了");
            }
        },5000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("8:00了,该起床了");
            }
        },4000);

        System.out.println("闹钟响了!!");
    }
}

这时我们运行代码, 我们发现, 按照时间执行完上述任务之后, 进程并没有退出. Timer 内部需要一组线程来执行注册的任务, 而这里的线程是 前台线程, 会影响进程退出.


定时器的实现

  • schedule 的第一个参数是一个任务, 我们需要能够描述这个任务. (任务包含两方面信息, 需要执行的工作, 与他开始执行的时间)
class MyTask implements Comparable<MyTask>{
    //要执行的任务
    private Runnable runnable;
    //什么时间来执行任务(一个时间戳)
    private long time;

    public MyTask(Runnable runnable, long dalay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + dalay;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public long getTime() {
        return time;
    }

    //实现 Comparable 接口, 便于下文 优先级队列的比较
    @Override
    public int compareTo(MyTask o) {
        return (int)(this.time - o.time);
    }
}
  • MyTimer 管理多个任务, 使用优先级阻塞队列 (保证每次取出等待的时间最短的任务, 与线程安全)
class MyTimer {
    private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public void schedule(Runnable runnable, long after) throws InterruptedException {
        MyTask myTask = new MyTask(runnable, after);
		queue.put(MyTask);
    }
}
  • 从队列中去元素, 创建一个单独的扫描线程, 让这个线程不停的来检查队首元素, 时间是否到了, 如果到了则执行该任务
class MyTimer {
    private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public MyTimer() {
        // 一个扫描线程
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    //取出队首元素
                    MyTask task = queue.take();
                    //当前时间
                    long curTime = System.currentTimeMillis();
                    //假设当前时间是 大于等于 任务设定时间,就要执行任务了
                    if(curTime >= task.getTime()) {
                        task.getRunnable().run();
                    }else {
                        //没到时间, 再将 task 放回阻塞队列
                        queue.put(task);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

    public void schedule(Runnable runnable, long after) throws InterruptedException {
        MyTask myTask = new MyTask(runnable, after);
        queue.put(myTask);

    }
}
  • # 注意 # 当前的代码中存在一个严重的问题, while(true) 转的太快了, 造成了无意义的 CPU 浪费

    例如: 如果我们设定了一个任务, 这个任务是 8:00 开始, 现在的时间是 7:30, 在这半个小时里 while (true) 会每秒钟访问队首元素几万次. CPU 不断的在工作, 这里的等待是无意义的.

class MyTimer {
    private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    // 定义一个 locker 实例
    Object locker = new Object();
    public MyTimer() {
        // 一个扫描线程
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    synchronized (locker) {
                        MyTask task = queue.take();
                        long curTime = System.currentTimeMillis();

                        if (curTime >= task.getTime()) {
                            task.getRunnable().run();
                        } else {
                            queue.put(task);
                            //时间未到, 就等待 wait, 等待多久, 任务时间 - 当前时间
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

    public void schedule(Runnable runnable, long after) throws InterruptedException {
        MyTask myTask = new MyTask(runnable, after);
        queue.put(myTask);
        //放入一个任务之后, 唤醒 wait 更新需要等待的时间
        synchronized (locker) {
            locker.notify();
        }
    }
}

完整代码 + 测试用例
//表示一个任务
class MyTask implements Comparable<MyTask>{
    //要执行的任务
    private Runnable runnable;
    //什么时间来执行任务(一个时间戳)
    private long time;

    public MyTask(Runnable runnable, long dalay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + dalay;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        return (int)(this.time - o.time);
    }
}

class MyTimer {
    private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    Object locker = new Object();
    public MyTimer() {
        // 一个扫描线程
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    synchronized (locker) {
                        //取出队首元素
                        MyTask task = queue.take();
                        //当前时间
                        long curTime = System.currentTimeMillis();
                        //假设当前时间是 大于等于 任务设定时间,就要执行任务了
                        if (curTime >= task.getTime()) {
                            task.getRunnable().run();
                        } else {
                            //没到时间, 再将 task 放回阻塞队列
                            queue.put(task);
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

    public void schedule(Runnable runnable, long after) throws InterruptedException {
        MyTask myTask = new MyTask(runnable, after);
        queue.put(myTask);
        synchronized (locker) {
            locker.notify();
        }
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("7:00了 该起床了");
            }
        },3000);

        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("9:00了 该起床了");
            }
        },5000);

        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("8:00了 该起床了");
            }
        },4000);

        System.out.println("闹钟响了!!");
    }
}

运行结果展示


2. 工厂模式

构造方法存在一定局限性, 为了绕开局限, 就引入了工厂模式

  • 构造实例, 最主要的就是使用构造方法 new .
  • 在 new 的过程中,就需要调用构造方法, 有时希望提供多种构造实例的方式
  • 就需要重载构造方法, 来实现不同的版本的对象来创建

例如: 我们想通过两个函数分别求点的坐标

此处这两个版本的构造方法, 无法构成重载.

<img src="C:/Users/gu%27jiu/AppData/Roaming/Typora/typora-user-images/image-20220924184037037.png" alt="image-20220924184037037" style="zoom:80%;" /

这时我们就可以用普通方法, 代替构造方法. (使用普通方法, 在里面分别构造出 Point 对象, 再通过一些其他手段来进行设置)

public static Point makePointXY(double x, double y) {
    Point p = new Point();
    p.setX(x);
    p.setY(y);
    return p;
}

public static Point makePointRA(double r, double a) {
    Point p = new Point();
    p.setR(r);
    p.setA(a);
    return p;
}

我们将这种方法称为工厂方法.


3. 线程池

线程诞生的目的, 是因为进程的创建/ 销毁开销过大.

但如果线程创建的速率进一步频繁了. 此时线程创建/ 销毁的开销仍不能忽略, 这时我们就可以使用线程池来进一步优化这里的速度.

在一个池子中, 创建好许多线程. 当需要执行任务的时候, 就不需要重新创建线程了, 只需要直接从池子里取一个现成的线程, 直接使用. 用完, 也不必释放线程, 而是直接还回到线程池里.

线程池最大的好处就是减少每次启动、销毁线程的损耗

标准库中的线程池

  • 创建线程池, 没有显式 new , 而是通过另外的 Executors 类的静态方法 newCachedThreadPool 来完成, 这个方法就是工厂方法.

    在 Java 中, 线程池的本体叫做 ThreadPoolExecutor .他的构造方法写起来非常麻烦, 为了简化构造,标准库就提供了一系列的工厂方法:

    • newFixedThreadPool: 创建固定线程数的线程池
    • newCachedThreadPool: 创建线程数目动态增长的线程池.
    • newSingleThreadExecutor: 创建只包含单个线程的线程池.
    • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
ExecutorService pool = Executors.newCachedThreadPool();
  • 使用 .submit( ) 添加任务, 里面只有一个Runnable 参数
public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("一个任务");
            }
        });
    }

线程池的实现

一个线程池可以同时提交 N 个任务, 对应的线程之中有 M 个线程来负责完成这 N 个任务

  • 这时我们就需要使用一个阻塞队列, 把每个被提交的任务, 都放在阻塞队列中. 再搞 M 个线程来取队列元素.
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
  • 核心操作为 submit, 将任务加入线程池中
public void submit(Runnable runnable) throws InterruptedException {
    queue.put(runnable);
}
  • 在构造方法中, 创建 M 个线程, 负责完成工作
public MyThreadPool(int m) {
    //在构造方法中, 创建 M 个线程, 负责完成工作
    for (int i = 0; i < m; i++) {
        Thread t = new Thread(() -> {
            while(true) {
                try {
                    Runnable runnable = queue.take();
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

完整代码 + 测试用例
class MyThreadPool {
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    public MyThreadPool(int m) {
        //在构造方法中, 创建 M 个线程, 负责完成工作
        for (int i = 0; i < m; i++) {
            Thread t = new Thread(() -> {
                while(true) {
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(10);
        for (int i = 0; i < 100; i++) {
            int taskId = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    //变量捕获只能捕获一个final类型变量, 或者是一个实际final变量
                    System.out.println("执行当前任务 " + taskId + "  当前线程: " + Thread.currentThread().getName());
                }
            });
        }
    }
}

运行结果展示


线程池的本体 ThreadPoolExecutor 的构造方法.


🌷(( ◞•̀д•́)◞⚔◟(•̀д•́◟ ))🌷

以上就是今天要讲的内容了,希望对大家有所帮助,如果有问题欢迎评论指出,会积极改正!!
在这里插入图片描述
加粗样式

这里是Gujiu吖!!感谢你看到这里🌬
祝今天的你也
开心满怀,笑容常在。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值