javaEE:多线程,线程池

多线程-线程池

线程池的作用是提前创建好线程.提高程序在面对大量并发任务时的效率和性能

为什么要使用线程池

只有一个原因:创建一个线程的开销非常大

创建一个线程时,需要进行很多操作:

  1. 分配内存
  2. 与操作系统互动
  3. 线程销毁也需要资源
    但当我们使用线程池时:
    线程池会预先创建好一批线程,当有任务时,直接取出一个线程执行,而完成后不销毁线程,直接将线程重新放入线程池中,当再有类型的线程需求时,可以直接取出
    这样的好处很明显:
  4. 提升性能:减少创建/销毁线程的开销
    理所应当的,毕竟线程池是创建几个线程后就不断复用这些线程
  5. 限制并发量:线程池可以规定最大线程数.这样可以防止线程创建过多导致的系统崩溃

另外java中还有进程池,但在普通编程环境下线程池的效果远不如进程池好.因此我们此处忽略进程池的相关知识

java中的线程池

java的线程池由java.util.concurrent包中的Executor框架提供

  • Executor:任务执行器(接口)
  • ExecutorService:增强版的Executor
  • Executors:工具类,用来创建线程池
  • ThreadPoolExecutor:真正的线程池实现

ThreadPollExecutor关键参数解释

java 复制代码
ThreadPoolExecutor(
    corePoolSize,
    maximumPoolSize,
    keepAliveTime,
    BlockingQueue<Runnable> blockingQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler
)
参数 意义
corePoolSize 核心线程数
maximumPoolSize 最大线程数
keepAliveTime 非核心线程存活时间
blockingQueue 任务阻塞队列
threadFactory创建线程工厂
handler 队列满时的拒绝逻辑
corePoolSize|核心线程数 和 maximumPoolSize|最大线程数

首先,需要知道什么是核心线程和非核心线程

  • 核心线程:不会随便释放掉的线程(类似于公司中的正式员工)
  • 非核心线程:在任务比较少的时候会自动释放的线程(类似与公司中的实习生)
    因此
    corePoolSize这个参数就是:你需要多少个核心线程
    maximumPoolSize这个参数是:你一共需要多少个核心线程和非核心线程(这个值不能小于核心线程数)
    那么如何设置核心线程数和最大核心线程数呢?
    对于不同类型的程序,我们需要灵活设置
  • 对于CPU密集型:算术运算,逻辑判断等
    由于对CPU消耗很大,需要更大的核心线程数
  • 对于I/O密集型:文件操作,网络操作等
    由于对CPU消耗很小,故不需要大量的核心线程
    最合理的做法是:
    通过一次次性能测试,找到一个合适的值
keepAliveTime |非核心线程存活时间

如果一个非核心线程的空闲时间超过了设定的存活时间,那么这个非核心线程将会被销毁

例如:如果设定时间是100.存在一个非核心线程在100的时间内仍然没有工作,那么这个线程就会销毁

blockingQueue|任务阻塞队列

由于一个线程池需要处理多个任务,因此需要存放运行的任务

submit的功能就是把要执行的任务(Runnable对象)给放到队列中

threadFactory | 线程工厂

通过线程工厂,可以做到不使用'new'或者构造方法来初始化对象

通过工厂方法最大的好处就是没有重载限制:比如使用相同的构造名但可以使用不同的参数

java 复制代码
public class Point {
//    //无法重载,会报错
//    public Point(double x, double y) {//使用坐标构建点
//
//    }
//    public Point(double r, double a) {//使用极坐标构建点
//
//    }
    //使用工厂方法
    public static Point buildPointByXY(double x, double y) {//使用坐标构建点
        Point p = new Point();
        p.setX(x);
        p.setY(y);
        return p;
    }
    public static Point buildPointByRA(double r, double a) {//使用极坐标构建点
        Point p = new Point();
        p.setR(r);
        p.setA(a);
        return p;
    }
}
handler|队列满时的拒绝逻辑

在阻塞队列中,队列满时的拒绝逻辑就是阻塞.但在线程池中,队列满时,阻塞并不一定是最好的应对方法.因此,我们需要有其它方式来处理

线程池中hander是RejectedExecutionHandler.它是一个接口,用来定义队列满时的拒绝逻辑.通过使用这个接口,就能做到自定义线程池的拒绝逻辑

  • 线程池自带的拒绝逻辑
    1. ThreadPoolExecutor.AbortPolicy
      当队列满时直接抛出异常
      适用于需要立即知道任务被拒绝访问的场景
    2. ThreadPoolExecutor.CallerRunsPolicy
      当队列满时让调用者来执行
      适用于对任务处理实时性能要求不高的场景
    3. ThreadPoolExecutor.DiscardPolicy
      当队列满时直接丢弃任务中最新的任务
    4. ThreadPoolExecutor.DiscardOldesPolicy
      当队列满时直接丢弃任务中最老的任务

简单线程池的模拟实现:只实现定义线程数

java 复制代码
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class MyThreadPoolExecutor {
    private final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();//定义一个阻塞队列
    public MyThreadPoolExecutor(int nThread) {//根据参数n创建n个线程
        for (int i = 0; i < nThread; i++) {
            Thread t = new Thread(() -> {
                while(true) {
                    Runnable runnable = null;
                    try {
                        runnable = queue.take();//如果没有使用到的线程会自动阻塞
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    runnable.run();//运行runnable内容
                }
            });
            t.start();//开始执行线程
        }
    }

    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
    public static void main(String[] args) throws InterruptedException {
        MyThreadPoolExecutor myThreadPoolExecutor = new MyThreadPoolExecutor(5);
        for (int i = 0; i < 1000; i++) {//假如有100个任务
            int id = i;
            myThreadPoolExecutor.submit(() -> {
                System.out.println("线程:" + Thread.currentThread().getName() + "正在执行第" + id + "个任务");
            });
        }
    }
}

观看这个模拟的线程池可以发现线程池使用了阻塞队列.在一个线程中通过不断传入runnable接口来执行特定的任务

运行这个模拟程序后,我们会发现返回的结果如下

bash 复制代码
线程:Thread-0正在执行第979个任务
线程:Thread-0正在执行第997个任务
线程:Thread-4正在执行第976个任务
线程:Thread-4正在执行第999个任务
线程:Thread-0正在执行第998个任务
线程:Thread-1正在执行第996个任务
线程:Thread-2正在执行第992个任务
线程:Thread-3正在执行第989个任务

观察这个结果我们发现线程的执行是不是线程0,1,2,3这样依次执行的,而且最后一个任务也不一定是最后一个完成的

定时器

在执行多线程任务,我们肯定会有一个需求.就是我希望某些任务先执行,某些任务后执行.那么应该怎么做呢?总不能在每个线程执行前加个sleep吧

此时,定时器这个东西就可以解决这个问题

  • Timer.schedule
    Timer类中有一个schedule方法.这个方法可以做到多线程中定时执行任务
    语法格式public void schedule(TimerTask task, long delay);.
    其中TimerTask 可以简单理解为一个专为Timer类写的一个Runnable接口.它和runnable的唯一区别是TimerTsak多了有关时间控制的内容.
    long delay 就是你要过多久才执行这个线程执行这个线程.
实例展示
java 复制代码
import java.util.Timer;
import java.util.TimerTask;

public class TestTimer {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 2");
            }
        },2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 1");
            }
        },1000);
        System.out.println("end");
        //timer.cancel();//关闭所有进程
    }
}

运行后结果

bash 复制代码
end
hello 1
hello 2

可以发现打印"end"的进程在代码中明明是排在最后的,但执行时代码却是最先执行完的.而"hello 1"在其次,"hello 2"在最后

实际上是在线程执行的一开始end就被打印了.1000毫秒后,hello 1 打印. 2000毫秒后, hello 2 打印.

注意到代码中有一个cancel()方法.可能很容易被理解为在执行完所有线程后关闭这个总的进程.但你如果把这个方法的注释去掉,会发现执行后的结果变成了这样

bash 复制代码
end

进程已结束,退出代码0

会发现只打印了一个end.这是因为方法的逻辑是立即关闭所有线程.不会等延时线程执行完再关闭进程

定时器原理--手写一个MyTimer

定时器最大的作用是能在多个任务中,指定每个任务延迟多久后再开始运行.因此需要做到让时间最小的任务先进行运行

为了找到任务中最先执行的任务,我们可以使用小根堆

java 复制代码
import java.util.PriorityQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class MyTimerTask implements Comparable<MyTimerTask> {
    private Runnable runnable;//要执行的任务
    private long time;//执行时间

    public long getTime() {
        return time;
    }
    public void run() {
        runnable.run();
    }
    public MyTimerTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.time - o.time);
    }
}
public class MyTimer {
    Lock locker = new ReentrantLock();
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    public MyTimer() {
        Thread t = new Thread(()-> {
            try {
                while (true) {
                    synchronized (locker) {
                        while (queue.isEmpty()) {
                            locker.wait();
                        }
                        MyTimerTask task = queue.peek();
                        long currentTime = System.currentTimeMillis();//系统时间
                        if (task.getTime() <= currentTime) {//判断时间
                            task.run();
                            queue.poll();
                        } else {
                            //超时等待
                            locker.wait(task.getTime() - currentTime);
                        }
                    }
                }
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t.start();
    }
    public void schedule(Runnable task, Long delay) {
        synchronized (locker) {
            queue.offer(new MyTimerTask(task, delay));
            locker.notify();
        }
    }
}
相关推荐
艾莉丝努力练剑15 小时前
【Linux进程间通信:共享内存】为什么共享内存的 key 值由用户设置
java·linux·运维·服务器·开发语言·数据库·mysql
jwn99915 小时前
【JavaEE】Spring Web MVC
前端·spring·java-ee
星辰_mya15 小时前
并发容器全家桶:选择正确的“交通工具”
java·开发语言·面试
w1225h15 小时前
Tomcat10下载安装教程
java
NikoAI编程15 小时前
AI实战第一课:从项目配置到功能开发的完整流程
java·ai编程
啦啦啦_999915 小时前
6. AI面试题之 MCP
java
飞天小猪啊15 小时前
Mybatis
java·spring·mybatis
Memory_荒年15 小时前
分布式锁:当你的“锁”从部门会议室升级到公司全球预订系统
java·后端
yuyuxun115 小时前
基于JSP购物网站系统的设计与实现 毕业设计-附源码03645
java·开发语言·python·django·flask·课程设计·pygame
几分醉意.15 小时前
先发制人:用 Bright Data 抢先捕捉 TikTok 爆款内容(附实战案例)
java·大数据·人工智能