多线程案例(4)-线程池

简介: 多线程案例(4)-线程池

多线程案例四

四、线程池


线程池是什么

虽然创建线程 / 销毁线程 的开销

想象这么一个场景:

在学校附近新开了一家快递店,老板很精明,想到一个与众不同的办法来经营。店里没有雇人, 而是每次有业务来了,就现场找一名同学过来把快递送了,然后解雇同学。这个类比我们平时来一个任务,起一个线程进行处理的模式。

很快老板发现问题来了,每次招聘 + 解雇同学的成本还是非常高的。老板还是很善于变通的,知道了为什么大家都要雇人了,所以指定了一个指标,公司业务人员会扩张到 3 个人,但还是随着 业务逐步雇人。于是再有业务来了,老板就看,如果现在公司还没 3 个人,就雇一个人去送快 递,否则只是把业务放到一个本本上,等着 3 个快递人员空闲的时候去处理。这个就是我们要带出的线程池的模式。

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

标准库中的线程池

  • 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
  • 返回值类型为 ExecutorService
  • 通过 ExecutorService.submit 可以注册一个任务到线程池中.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello");
   }
});

上述代码第一行代码就是创建了一个线程池,池子里线程数目固定是10个。

这个操作,使用某个类的某个静态方法,直接构造出了一个对象来(相当于是把 new 操作,给隐藏到这样的方法后面了)

像这样的方法,就称为"工厂方法"

提供工厂方法的类,也就成为"工厂类"

此处这个代码就使用了"工厂模式"这种 设计模式

那么什么是工厂模式呢?

答:工厂模式,一句话表示,使用普通的方法来代替构造方法,创建对象。为啥要代替呢,因为构造方法有坑,他只能构造一种对象,如果要构造多种不同情况的对象,就难搞了。

例如我们需要使用笛卡尔坐标系来构造坐标,空间直角坐标系和极坐标系,那么此时我们的代码就会是:

class Point {
    //构造空间直角坐标系
    public Point(double x, double y) {
    }
    //构造极坐标系
    public Point(double r, double a) {
    }
}

显然此时我们虽然可以用代码写出并构造出这两个坐标系,但是他们是❌错误的,因为我们的构造方法只能构造一个对象。而普通方法是可以构造多个对象的

那么就会有人问了我们不是可以通过重载的方法来进行多个构造方法吗,此时我们来看一下重载的要求,重载要求的是:方法名相同,参数的个数或者类型不相同。

因此我们这里根本不可能进行重载调用。

普通方法构建笛卡尔坐标系代码:

class PointFactory {
    public static Point makePointByXY(double x,double y) {
    }
    public static Point makePointByRA(double r,double a) {
    }
}
public class ThreadDemo26 {
    public static void main(String[] args) {
        Point P = PointFactory.makePointByXY(10,20);
    }
}

普通方法,方法名字没有限制。因此有多种方式构造,就可以直接使用不同的方法名即可。此时,方法的参数是否要区分,已经不重要了

这里我们区分一下重载和重写的区别:

重载(overload):指一个类中可以有多个方法具有相同的名字,但这些方法的参数不同(参数的类型和个数不同)。分别在父类子类里也是可能构成重载的。

重写(override):在不同类中(指父类和子类)中,两个具有相同方法名和相同参数的方法,称作重写。 (如果是其他语言,重写方法不一定通过父类子类)

重写本质上就是用一个新的方法,代替旧的… 就得要求新的方法和旧的方法,名字/参数都得一摸一样

下面我们为大家带来几个线程池的使用举例:

运行程序之后发现,main线程结束了,但是整个进程没有结束。线程池中的线程都是 前台线程。此时会阻止进程结束。(前面定时器 Timer 也是同理)

下属代码为什么不能直接写 System.*out*.println("hello" + n); 呢?

答:因为变量捕获,这里的 i 是主线程里的局部变量(在主线程的栈上),随着主线程这里的代码块执行结束就销毁了。很可能主线程这里的 for 执行完了,当前 run 的任务在线程池里还没排到呢,此时 i 就销毁了

变量捕获(必须是不可修改的变量才能捕获 有final修饰的变量也可捕获):在定义 run 的时候,偷偷把 i 当前的值记住。后续执行run的时候,就创建一个也叫做 i 的局部变量,并且把这个值赋值过去

final:

final修饰类,表示该类是无法被任何其他类继承的,意味着此类在一个继承树中是一个叶子类,并且此类的设计已被认为很完美而不需要进行修改或扩展。

final修饰类中的方法,表示该类是无法被任何其他类继承的,不可以被重写;也就是把该方法锁定了,以防止继承类对其进行更改。

final修饰类中的变量,表示该变量一旦被初始化便不可改变。

此处要注意,当前是往线程池里放了1000个任务,1000个任务就是让这 10 个线程来平均分配一下,差不多一个线程执行100个任务,但是注意这里并非是严格的平均,可能有的多一个有的少一个,都很正常。(每个线程都执行完一个任务之后,再立即取下一个任务,由于每个任务执行时间都差不多,因此每个线程做的任务数量就差不多)

进一步的可以认为,这1000个任务,就在一个队列里排队,这10个线程,就依次来取队列中的任务,取一个就执行一个,执行完了之后再执行下一个。

因为线程调度是随机分配的,因此会出现顺序不一样的情况。

Executors 创建线程池的几种方式

  • newFixedThreadPool: 创建固定线程数(可能是多个)的线程池
  • newCachedThreadPool: 创建线程数目动态增长的线程池.
  • newSingleThreadExecutor: 创建只包含单个线程的线程池.
  • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer. 执行的时候不是由扫描线程自己执行,而是由单独的线程池来执行。

 

总结:实践中确定的线程数量,也很简单,通过测试/实验的方式

Executors 本质上是 ThreadPoolExecutor 类的封装.

ThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定. (后面再介绍)

实现线程池

  • 核心操作为 submit, 将任务加入线程池中 使用 Worker 类描述一个工作线程.
  • 使用 Runnable 描述一个任务.
  • 使用一个 BlockingQueue 组织所有的任务
  • 每个 worker 线程要做的事情: 不停的从 BlockingQueue 中取任务并执行.
  • 指定一下线程池中的最大线程数 maxWorkerCount; 当当前线程数超过这个最大值时, 就不再新增 线程了.
class Worker extends Thread {
    private LinkedBlockingQueue<Runnable> queue = null;
    public Worker(LinkedBlockingQueue<Runnable> queue) {
        super("worker");
        this.queue = queue;
   }
    @Override
    public void run() {
        // try 必须放在 while 外头, 或者 while 里头应该影响不大
        try {
            while (!Thread.interrupted()) {
                Runnable runnable = queue.take();
                runnable.run();
           }
       } catch (InterruptedException e) {
       }
   }
}
public class MyThreadPool {
    private int maxWorkerCount = 10;
    private LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue();
    public void submit(Runnable command) {
        if (workerList.size() < maxWorkerCount) {
            // 当前 worker 数不足, 就继续创建 worker
            Worker worker = new Worker(queue);
            worker.start();
       }
        // 将任务添加到任务队列中
        queue.put(command);
   }
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool();
        myThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("吃饭");
           }
       });
  Thread.sleep(1000);
   }
}

自己实现线程池版本二:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 晓星航
 * Date: 2023-08-01
 * Time: 15:16
 */
class MyThreadPoll {
    //此处不涉及 "时间",此处只有任务,就直接使用 Runnable 即可~~
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    //n 表示线程的数量
    public MyThreadPoll(int n) {
        //在这里创建线程
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(()->{
               while (true) {
                   try {
                       Runnable runnable = queue.take();
                       runnable.run();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
            });
            t.start();
        }
    }
    //注册任务给线程池
    public void submit(Runnable runnable) {
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadDemo27 {
    public static void main(String[] args) {
        MyThreadPoll pool = new MyThreadPoll(10);
        for (int i = 0; i < 1000; i++) {
            int n = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello" + n);
                }
            });
        }
    }
}

四个拒绝策略:

例如我们此时在学习,而有好朋友叫我们去打游戏

那么对应上面情况:

  1. 要学习的东西太多,我直接开摆,啥也不干了…
  2. 学习任务太多了,让朋友们自己去玩。(此时朋友们自己去打游戏了)
  3. 直接不学了,开玩!
  4. 拒绝去玩,继续学习。(至于朋友们有没有玩我们不必管)

感谢各位读者的阅读,本文章有任何错误都可以在评论区发表你们的意见,我会对文章进行改正的。如果本文章对你有帮助请动一动你们敏捷的小手点一点赞,你的每一次鼓励都是作者创作的动力哦!😘

目录
相关文章
|
5天前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
34 20
|
11天前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
1月前
|
监控 Kubernetes Java
阿里面试:5000qps访问一个500ms的接口,如何设计线程池的核心线程数、最大线程数? 需要多少台机器?
本文由40岁老架构师尼恩撰写,针对一线互联网企业的高频面试题“如何确定系统的最佳线程数”进行系统化梳理。文章详细介绍了线程池设计的三个核心步骤:理论预估、压测验证和监控调整,并结合实际案例(5000qps、500ms响应时间、4核8G机器)给出具体参数设置建议。此外,还提供了《尼恩Java面试宝典PDF》等资源,帮助读者提升技术能力,顺利通过大厂面试。关注【技术自由圈】公众号,回复“领电子书”获取更多学习资料。
|
1月前
|
存储 监控 Java
JAVA线程池有哪些队列? 以及它们的适用场景案例
不同的线程池队列有着各自的特点和适用场景,在实际使用线程池时,需要根据具体的业务需求、系统资源状况以及对任务执行顺序、响应时间等方面的要求,合理选择相应的队列来构建线程池,以实现高效的任务处理。
120 12
|
2月前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
79 1
|
3月前
|
Java
.如何根据 CPU 核心数设计线程池线程数量
IO 密集型:核心数*2 计算密集型: 核心数+1 为什么加 1?即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保 CPU 的时钟周期不会被浪费。
162 4
|
3月前
|
数据采集 Java Python
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
119 0
|
3月前
|
Java
线程池内部机制:线程的保活与回收策略
【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
165 2
|
3月前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
136 38
|
8天前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
38 17