【JavaEE】多线程(5)

发布于:2024-12-06 ⋅ 阅读:(27) ⋅ 点赞:(0)

一、线程池

1.1 什么是线程池

线程池是一种管理和复用线程的机制,线程池会提前创建好一定数量线程存储在池中,当有任务要执行时,就从线程池中取出一个线程来执行任务,任务执行结束后,就把该线程放回线程池中供其他线程使用

1.2 为什么要用线程池

我们来看看线程池的优势

  • 降低资源消耗:线程池可以复用已创建的线程,显著减少了创建和销毁线程的系统开销;按照以前的做法,每有一个任务都要创建一个线程,任务结束后销毁线程,线程池则可以循环复用
  • 提高响应速度:线程池中的线程都是提前创建好的,随需随取,任务完成后放归线程池,不需要等待线程创建和销毁线程

1.3 ThreadPoolExecutor

ThreadPoolExecutor 是线程池的具体实现——一个类

ThreadPoolExecutor类中的构造方法

所以我们来看最后一个构造方法:

【定义】 corePoolSize:核心线程数,maximumPoolSize:最大线程数

【解释】

  • 标准库中将线程分为核心线程非核心线程,maximumPoolSize = 核心线程数+非核心线程数
  • 一个线程池刚被创建的时候,里面的线程数就是核心线程数这么多(假设核心线程数为4,最大线程数为8)
  • 线程池会提供一个 submit 方法,其他线程来向线程池中添加任务,如果任务较少,4个线程能够处理的过来,那么线程池就只有这4个线程工作了;如果任务较多,线程池就会自动创建出新的线程来支撑更多的任务,创建的总线程数不能超过最大线程数

【定义】keepAliveTime:允许非核心线程空闲的最大时间,unit:keepAliveTime 的单位

【解释】当非核心线程没有执行任务的时间超过 keepAliveTime,就会被回收,单位表:


【定义】BlockingQueue<Runnable> workQueue:线程池中存放任务的阻塞队列

【解释】后续线程池中的工作线程就会消费这个队列


【定义】ThreadFactory threadFactory:用来创建线程的工厂类

【解释】这里涉及到一种设计模式——工厂模式

工厂模式主要用来解决构造方法创建对象的相关问题,比如:

上述代码中,一个是直角坐标的方式创建一个点对象,一个是极坐标的方式创建一个点对象,但是这两个构造方法无法构成重载,导致编译报错

解决方法如下:

class Point {
    //直角坐标
    public static Point pointByXY (double x, double y) {
        Point p = new Point();
        p.setX(x);
        p.setY(y);
        return p;
    }
    
    //极坐标
    public static Point pointByRA (double r, double a) {
        Point p = new Point();
        p.setR(r);
        p.setA(a);
        return p;
    }
}

上述两个方法就是工厂方法,Point就是一个工厂类

其实如果语法层面上不要求构造方法名字必须和类名一样,就不会有上述模式了

threadFactory 创建线程,主要是为了批量的给要创建的线程设置一些属性,将线程的属性提前初始化好(比如名字)


【定义】RejectedExecutionHandler handler:这是一个枚举类型,代表拒绝策略

【解释】当任务队列满了,仍然要继续添加任务时,线程池要采用的拒绝策略

  • ThreadPoolExecutor.AbortPolicy:被拒绝的任务的处理程序,抛出一个 RejectedExecutionException(让程序员快速直到任务处理不过来了)

  • ThreadPoolExecutor.CallerRunsPolicy:由添加任务的线程来执行这个任务

  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃最老的任务,让新任务去队列中排队

  • ThreadPoolExecutor.DiscardPolicy:丢弃这个任务,按照原来的节奏执行

1.4 标准库中的线程池

Executors 是一个工具类,它提供了一系列静态方法来创建不同类型的线程池,这些线程池都是基于 ThreadPoolExecutor 或其他相关类的封装

以上就是对线程池的构造方法的描述,其实标准库也知道ThreadPoolExecutor使用起来很费劲(构造方法的参数过多)于是标准库自己提供了几个工厂类

可以看一下这些方法的源码:

后面就不展示了,可以自己去看,基本都是基于 ThreadPoolExecutor 的封装


1.5 自己实现线程池

class MyThreadPool {
    private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);

    //初始化
    public MyThreadPool (int n) {
        //
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                try { 
                    while (true) {
                        //线程刚创建和线程执行完所有任务后都会在这里阻塞
                        Runnable runnable = queue.take();
                        runnable.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    }

    //提交任务
    void submit (Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
}

public class Demo {

    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(10);
        for (int i = 0; i < 10000; i++) {
            int id = i;
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello "+ id + ", " + Thread.currentThread().getName());
                }
            });
        }
    }
}

代码在执行 main 方法里的创建 MyThreadPool 对象时就已经创建好了10个线程,此时这10个线程都因为阻塞队列为空而处于阻塞状态

向线程池提交10000个任务后,这10个线程就会开始消费阻塞队列中的任务

⬆️共打印10000条

二、定时器

2.1 什么时定时器

定时器类似于一个闹钟,指定一个任务,指定一个时间,该任务不会立即执行,而是等指定的时间过去后再去执行

比如验证码的时效性就可以理解为有定时器

2.2 标准库中的定时器

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

TimerTask 是一个抽象类,本质上实现了Runnable接口,把它当成Runnable即可;delay表示多长时间之后执行

public class Demo {
    public static void main(String[] args) {
        Timer timer = new Timer();

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        }, 2000);
    }
}

2s后打印"hello"

程序没有结束,因为Timer内部包含一个前台线程,阻塞等待下一个任务,所以阻塞了进程的结束

2.3 模拟实现定时器

实现定时器需要完成2个功能:

  • 能够延时执行任务/指定时间执行任务
  • 能够管理多个任务

1. 定义一个类表示一个任务:

class MyTimerTask {
    //两个属性
    private Runnable runnable; //任务
    private long time; //这是一个绝对时间,表示什么时候执行任务

    //初始化两个属性
    public MyTimerTask (Runnable runnable, Long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }

    //执行任务
    public void run() {
        runnable.run();
    }

    //获取任务执行的绝对时间
    public long getTime () {
        return time;
    }

}

2. 定义一个优先级队列来管理任务

class MyTimer {
    PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    public void schedule (Runnable runnable, long delay) {
        MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
        queue.offer(myTimerTask);
    }
}

让任务按照各自的执行时间作为优先级的标准,队首元素就是最早要执行的任务,线程只需要关注队首元素即可(因为时间最早要执行的任务还没执行,其他任务肯定时间还没到)

【引入比较规则】

为了以执行时间作为优先级标准,应该在 MyTimeTask 类中引入比较规则

3. 管理好任务后,需要有一个线程来执行这些任务

写在 MyTimer 的构造方法里

    public MyTimer () {
        Thread t = new Thread(() -> {
            while (true) {
                if (queue.size() == 0) {
                        continue;
                }

                //取出任务
                MyTimerTask task = queue.peek();

                //判断时间
                long curTime = System.currentTimeMillis();
                if(curTime >= task.getTime()) {
                    task.run();
                    queue.poll();
                } else {
                    //时间还没到
                    continue;
                }
            }
        });
    }

当队列为空时,代码会一直进行循环,什么也不干,当队列不为空时,代码会取出一个任务,判断时间有没有到,如果到了,就执行,没到就再入循环

【线程不安全】

上述代码中存在线程不安全问题

主线程中,schedule方法内存在入队操作;t线程中,存在出队操作;两个线程针对操作同一个队列就会引发线程不安全的问题

比如:t线程检查当前队首元素到时间了可以执行,执行完后要将该队首元素取出,但是再取出之前,主线程又向队列中添加了一个新的任务,新任务的时间又正好早于要取出的任务,那么此时新任务就成了队首元素,这时就将新入队的任务在还没执行的情况下就出队了

基于上述原因要进行加锁:

【忙等】

代码中还存在一处不合理的地方:在t线程中,判断任务的执行时间还没到就要再入循环,在下一轮循环又要把判断队列为空、取出队首元素、计算时间这些操作再做一遍,使得代码在持续消耗CPU,但又没有执行任务,所以要在等待的时间里,然线程阻塞,释放CPU资源

这里使用 wait()


🙉本篇文章到此结束


网站公告

今日签到

点亮在社区的每一天
去签到