【多线程-从零开始-玖】内核态,用户态,线程池的参数、使用方法详解

简介: 【多线程-从零开始-玖】内核态,用户态,线程池的参数、使用方法详解

1. 为什么会有线程池

“池”这种思想,本质上就是能提高程序的效率

最初引入线程,就是因为进程太重了,频繁创建、销毁进程,开销比较大

  • “大/小”是相对的,随着业务上对于性能要求越来越高,对应的,线程穿件/销毁的频次越来越多
  • 此时,线程创建和销毁的开销就变得比较明显,无法忽略不计了
    线程池就是解决上述问题的常见方案
  • 把线程提前从系统中申请好,放到一个地方,后面需要使用线程的时候直接从这个地方取,而不是从系统中重新申请
  • 线程用完之后,也是还回到刚才这个地方

2. 内核态和用户态

为啥从线程池里面取线程,比从系统申请来的更高效呢?

  • 内核态 & 用户态,操作系统中的概念
  • 操作系统=操作系统内核+操作系统配套的应用程序
  • 内核是操作系统的核心功能部分,负责完成一个操作系统的核心操作。向下要管理各种硬件资源(包括调用驱动程序,操控各种硬件设备);向上要给这些应用程序提供一个稳定的运行环境,给他们一些对应的 API 来完成相关的操作

2.1 场景构造

在银行里,你需要办理取款业务,就需要来到柜台前,给柜员说清楚你的需求,然后柜员再给你在后面的保险柜里面取钱(你是无法直接跨过柜台,自己取钱的)

  • 柜台内部——操作系统内核
  • 柜台外部——用户
    像我们平时执行的应用程序(CCtalk、画图板…),就是应用态的应用程序,而操作系统内核,提供了一些相关的系统 API 来给这些应用程序进行使用

比如执行一个 println 操作

  • 应用程序来到台前,给内核里面的人说:我现在要往控制台打印一个什么什么东西,你用空帮我弄一下呗
  • 然后内核里面的人就把数据拿到,开始操作显示器、控制台,完成这里的打印操作
    我们进行的很多操作,都是用户态的应用程序和操作系统内核(内核态)的逻辑相互配合,来完成工作

对应的,平时执行的很多代码逻辑,都是要用户态的代码和内核态的代码配合来完成的


应用程序有很多,这些应用程序都是由内核同一负责管理和服务,内核里的工作就可能非常繁忙,进而提交给内核的要做的任务可能是不可控的

比如说你要办一张银行卡

你来到了银行柜台前,但你没有身份证复印件,柜员说你有两个选择:

  1. 自己去旁边的自助复印机进行处理
  2. 等他帮你处理,但要稍等一会
  1. 如果选择自助复印,我就会立即去到复印机那里,立即开始复印,拿到复印件后立即回到窗口。整个过程是连贯的,是可控的效率就比较高
  2. 如果选择柜员帮忙,那么他就可能拿着身份证,跑到柜台后面,消失了,但什么时候能退回来,这就不知道了。因为他跑到后面后可能先得上个厕所、回个消息、玩下手机、吃点东西… 这整个过程是不可控的,因此效率比较低

  • 从系统创建线程,就相当与是让银行的人给我复印
  • 这样的逻辑就是调用系统 API,由系统内核执行一系列逻辑来完成这个过程
  • 直接从线程池里面来取,这就相当于是自助复印
  • 整个过程都是纯用户态代码,都是咱们自己控制的,整个过程更可控,效率更高

因此,我们通常认为,纯用户态操作,比经过内核的操作效率更高

3. 标准库的线程池

  • 标准库提供了类— ThreadPoolExecutor(构造方法提供了很多参数)

3.1 构造方法的参数

3.1.1 核心线程数和最大线程数

此线程池,可以支持“扩容”

某个线程初识情况下,可能有 M 个线程,实际使用中发现 M 不太够用,就会自动增加 M 的个数

因为 CPU 上的核心数量是有限的,所以线程不能无限扩容

  • 在 Java 标准库的线程池中,把里面的线程分为了两类
  • 核心线程
  • 最少有多少个线程
  • 始终存在于线程池内部
  • 非核心线程
  • 线程扩容的时候新增的
  • 繁忙的时候被创建出来,空闲了就会把这些线程真正的释放掉
  • { 核心线程数 + 非核心线程数 } m a x = 最大线程数 \{核心线程数+非核心线程数\}_{max}=最大线程数 {核心线程数+非核心线程数}max=最大线程数

3.1.2 非核心线程允许摸鱼的最大时间

非核心线程会在线程空闲空闲多久后会被销毁

比如:

公司招了个实习生,某一天,实习生没活干,但公司不会把他裁了,要是明天有活干呢?

第二天,实习生又没活干,裁吗?要是后天有活呢…

所以就要设一个最大时间,超过这个时间之后就裁掉

3.1.3 工作队列(阻塞队列)

  • 线程池的工作过程是典型的“生产者消费者模型”
  • 程序员工作的时候,通过形如“submit”这样的方法,把要执行的任务,设定到线程池里
  • 线程池内部的工作线程,负责执行这些任务
  • 此处就有一个阻塞队列

此处的队列可以让我们自行指定

  • 队列的容量—capcity
  • 队列的类型—基于链表?数组?优先级?…

通过 Runnable 给线程设定要执行的任务

  • Runnable 接口本身的含义就是一段可移植性的任务
  • Runnable 是基于任务的抽象表示

3.1.4 线程工厂

工厂设计模式

“工厂”指的是“工厂设计模式”,也是一种常见的设计模式

  • 是一种在创建类的实例时使用的设计模式
  • 由于构造方法有“坑”,通过这个设计模式来填坑

构造方法是一种特殊的方法

  • 必须和类名一样
  • 多个版本的构造方法必须构成“重载”(overload)实现

构造方法的局限性:

比如需要描述一个点(Point),有两种描述方法:

  1. 通过横纵坐标进行描述
  • 参数为 ( d o u b l e    x , d o u b l e    y ) (double\; x, double\; y) (doublex,doubley)
class Point{
  public Point(double x, double y) {}
}
  1. 通过极坐标进行描述(三角函数)
  • 参数为 ( d o u b l e    r , d o u b l e    a ) (double\; r, double\; a) (doubler,doublea)
  • x = r ∗ s i n α , y = r ∗ c o s α x=r*sinα,y=r*cosα x=rsinαy=rcosα
class Point {
  public Point(double r, double a){}
}
  • 但此时两个构造方法的参数个数和类型都是一样的,所以无法构成重载

因此,使用构造方法创建实例,就会存在上述局限性

为了解决上述问题,就引入了“工厂设计模式

  • 通过“普通方法”(通常是静态方法)完成对象构造和初始化的操作
class Point{
  public static Point makePointByXY(double x, double y) {
    Point p;
    p.setX(x);
    p.setY(y);
    return p;
  }
  
  public static Point makePointByRA(double r, double a) {
    Point p;
    p.setR(r);
    p.setA(a);
    return p;
  }
}
  • 这就是最简单的工厂设计模式的写法
  • 此处用来创建对象的 static 方法就称为“工厂方法”
  • 有的时候,工厂方法也会放到单独的类里实现,用来放工厂方法的类,称为“工厂类”
//工厂类
class PointFactory{
  public static Point makePointByXY(double x, double y) {
    Point p;
    p.setX(x);
    p.setY(y);
    return p;
  }
  
  public static Point makePointByRA(double r, double a) {
    Point p;
    p.setR(r);
    p.setA(a);
    return p;
  }
}

  • ThreadFactory 就是 Thread 类的工厂类,通过这个类,完成 Thread 实例创建和初始化操作
  • 此处的 ThreadFactory 可以针对线程池里的线程,进行批量的设置属性
  • 此处一般都不会进行调整,就是用标准库提供的默认值即可

3.1.5 拒绝策略

  • 最重要、最复杂的参数

如果线程池的任务队列满了,但还是要继续给这个队列添加任务,怎么办呢?

比如:你去向女神表白

  • 女神对你说:你是个好人(不见得是坏事),这样你可以趁早死心,趁早开始新的生活
  • 但如果女神只笑笑,也不说话,也不正面回答你,也不对你热情,也不对你冷淡,这才麻烦(很可能你就已经成为了备胎,你可能就在女神这里阻塞住)
    对于这两种情况,第一种肯定是更好,毕竟长痛不如短痛,伸头一刀缩头一刀,趁早让人死心
  • 所以,当队列满了,不要阻塞,而是要明确地拒绝
  • Java 标准库给出了四种不同的拒绝策略
拒绝策略类型 说明
ThreadPoolExecutor. AbortPolicy 默认拒绝策略,添加任务的时候,直接抛出异常
ThreadPoolExecutor.CallerRunsPolicy 拒绝执行,由调用 submit 的线程负责执行
ThreadPoolExecutor.DiscardOldestPolicy 把任务队列中最老的任务踢掉,然后执行新增加的任务
ThreadPoolExecutor.DiscardPolicy 把任务队列中,最新的任务踢掉
四种拒绝策略

情景介绍:

这周你行程安排满了,你欲哭无泪,结果,你们导员突然找到你,让你和她一起参加一个什么比赛,在这时:

  1. AbortPolicy,添加任务的时候,直接抛出异常
    你心里最后一跟稻草被压到了:“本来就事多,还要我做牛马,我受不了了啊“,悲伤之下,你进了医院,你们导员也就理所应当的取消了这次比赛,并且你这周的满课也上不了了
  2. CallerRunsPolicy,拒绝执行,由调用 submit 的线程负责执行
    你耐心给导员说:“导员,我周事实在太多了,完全抽不开身”,于是,你们善解人意的导员就理解了,他就自己一个人去参加了那个比赛
  3. DiscardOldestPolicy,把任务队列中最老的任务踢掉,然后执行新增加的任务
    你不想拒绝导员,你看了下行程表,划去了最后一个任务,将所有的行程往后移,然后你跟着导员一起去参加比赛去了
  4. DiscardPolicy,把任务队列中,最新的任务踢掉
    你给导员说去不了之后,导员说:“那我也不去了”,于是这个比赛就被踢掉了

3.2 线程池的使用

ThreadPoolExecutor 功能很强大,使用很麻烦

标准库对这个类进一步封装了一下,Executors 提供了一些工厂方法,可以更方便构造出线程池

  • newCachedThreadPool 设置了非常大的最大线程数,可以对线程池进行不停地扩容
  • newFixedThreadPool 把核心线程数和最大线程数设置成了一样的值,固定了数量,不会自动扩容
public class Demo2 {  
    public static void main(String[] args) {  
        ExecutorService service = Executors.newFixedThreadPool(4);  
        for (int i = 0; i < 100; i++) {  
            service.submit(() -> {  
                Thread current = Thread.currentThread(); 
                //这里的i报错了,因为出现了“变量捕获” 
                System.out.println("hello thread" + i + "," + current.getName());  
            });        
        }    
    }
}
  • submit 也可以使用 lambda 表达式
  • 如果想 i 不报错,就需要 i 是被 final 修饰的或者是没有修改的(发生了 [[03 多线程-线程的核心操作#^191375|变量捕获]])
  • 我们就可以在 lambda 外面重新定义一个变量 id,每次进入循环就创建一个等于 i 的变量,之后之后也不会修改
public class Demo2 {  
    public static void main(String[] args) {  
        ExecutorService service = Executors.newFixedThreadPool(4);  
        for (int i = 0; i < 100; i++) {  
        int id = i;
            service.submit(() -> {  
                Thread current = Thread.currentThread(); 
                //这里的i报错了,因为出现了“变量捕获” 
                System.out.println("hello thread" + id + "," + current.getName());  
            });        
        }    
    }
}
  • 虽然代码的 100 个任务都执行完毕了,但是整个进程并没有结束,因为此处线程池创建出来的线程都是“前台线程”,虽然 main 线程结束了,但是这些线程池里的前台线程仍然存在
  • 若想解决
  1. 将每次创建出来的线程设为“后台进程”
  2. 使用 shutdown 操作,把线程池里面所有的线程都终止掉
public class Demo2 {  
    public static void main(String[] args) throws InterruptedException {  
        ExecutorService service = Executors.newFixedThreadPool(4);  
        for (int i = 0; i < 100; i++) {  
            int id = i;  
            service.submit(() -> {  
                Thread current = Thread.currentThread();  
                System.out.println("hello thread" + id + "," + current.getName());  
            });        
        }        
        Thread.sleep(2000);  
        service.shutdown();  
        System.out.println("程序退出");  
    }
}
  • 使用 sleep 是为了保证所有线程都执行完了

使用线程池的时候,需要指定线程个数,那该如何指定呢?该指定多少呢?

实际开发中建议的做法,是通过实验的方式,找到一个合适的线程数的个数的值

  • 给线程池设置不同的线程数,分别进行性能测试,关注响应时间/消耗的资源指标,挑选一个比较合适的数值
  • 一台主机上,并不是只有一个程序
  • 你写的这个程序,也不是 100%的每个线程都跑满 CPU,线程工作过程中,可能会涉及到一些 IO 操作/阻塞操作主动放弃 CPU
  • 如果线程代码里都是算术运算,确实能跑满 CPU
  • 如果是包含了 sleep,wait,加锁,打印,网络通信,读写硬盘… 都会使线程主动放弃 CPU 一会(给其他线程提供更多资源)

4. 线程池的模拟实现

固定线程数目的线程池


import java.util.concurrent.ArrayBlockingQueue;  
import java.util.concurrent.BlockingQueue;  
  
class MyThreadPool {  
    private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);  
  
    //构造方法,n为创建的线程个数  
    public MyThreadPool(int n) {  
        //先创建 n 个线程  
        for (int i = 0; i < n; i++) {  
            Thread t = new Thread(() -> {  
                //循环的从队列中取出任务  
                while (true) {  
                    Runnable runnable = null;  
                    try {  
                        runnable = queue.take();  
                    } catch (InterruptedException e) {  
                        throw new RuntimeException(e);  
                    }                    
                    runnable.run();  
                }            
            });            
            t.start();  
        }    
    }  
    
    //添加任务  
    public void submit(Runnable runnable) {  
        try {  
            queue.put(runnable);  
        } catch (InterruptedException e) {  
            throw new RuntimeException(e);  
        }    
    }
}  
  
public class Demo3 {  
    public static void main(String[] args) {  
        MyThreadPool pool = new MyThreadPool(4);  
        for (int i = 0; i < 1000; i++) {  
            int id = i;  
            pool.submit(() -> {  
                System.out.println("执行任务" + id + "," + Thread.currentThread().getName()); 
            });        
        }    
    }
}


相关文章
|
11小时前
|
Java 程序员 编译器
【多线程-从零开始-叁】线程的核心操作
【多线程-从零开始-叁】线程的核心操作
8 0
|
5月前
|
Shell Python BI
targetcli内核态操作实战
工作中一旦遇到内核态的东西感觉操作非常困难,并且各种的权限的限制导致的操作非常困难,最终往往都是重启了事。比如进程出现了僵尸D状态,活着出现内核态数据残留,例如sysfs数据残留等
117 2
|
5月前
|
Unix Linux 调度
linux线程与进程的区别及线程的优势
linux线程与进程的区别及线程的优势
|
存储 Linux 调度
Linux系统编程5(线程概念详解)
Linux系统编程5(线程概念详解)
220 0
|
安全 Java
28a 线程&进程基础-快速上手
使用Java的Thread类来创建和管理程。线程的生命周期包括五个状态:新建、就绪、运行、阻塞和死步与互斥: 当多个程共享同一资源时,性Java提供了synchronized关. 线程安全的概念和正确的执行,不会出现数据竞争和错误情况发生。Java提供类概锁造成的一种阻塞状态。方法来避免死锁的发生程的工具类: Java提供了一些并发编的工具类,比如Semaphore、CountDownLatch、CyclicBarrier等来实现程之间的协作和控制概是,当有任务需要处理时接从线程池中取出线程使用,可以优化线程创建和销毁过程,减7. 分析线程和并在开发过程中,我们需要使用一些工具和方法来分析和解
96 0
|
存储 Python
Python语言如何在一个单独的线程中进行快速的IO操作
Python语言如何在一个单独的线程中进行快速的IO操作
119 0
Python语言如何在一个单独的线程中进行快速的IO操作
C++多线程 并行与并发 了解进程和线程 浅显的进行传参,调用
C++多线程 并行与并发 了解进程和线程 浅显的进行传参,调用
C++多线程 并行与并发 了解进程和线程 浅显的进行传参,调用
|
数据采集 算法 Java
库调多了 都忘了最基础的概念 - 进程 / 线程篇
库调多了 都忘了最基础的概念 - 进程 / 线程篇
114 0
库调多了 都忘了最基础的概念 - 进程 / 线程篇
|
算法 安全 Java
线程、进程、多线程、多进程、多任务,傻傻分不清?
可能学习嵌入式开发的读者都听说过这些专业名词,但又多少人理解了? 首先,从定义开始,先看一下教科书上 进程和线程定义
线程、进程、多线程、多进程、多任务,傻傻分不清?