线程-基础知识

基础知识

OS 线程基础知识

:::info

① 所有语言层面的线程知识都是基于操作系统,所以本篇笔记会优先[简单讲述]{.red}操作系统中线程

② 了解操作系统中的线程之后才会开始介绍 Java 线程、C# 协程中的相关知识

注:最好对操作系统中的线程基础知识有所了解

:::

什么是线程?

  • 线程定义:
    • OS 定义:
      • 线程是拥有 [独立的程序计数器、寄存器、栈空间]{.red}
      • 并且共享进程提供的 [代码段、数据段]{.red} 等资源的 [处理器基本单元]{.red}
    • 通俗定义:[线程是由一系列指令组成的不具有存储空间的串行控制流,负责将每条指令交付给处理器执行]{.red}
  • 线程模型:寄存器 + 程序计数器 + 占空间
  • 线程状态
    • 新生态:线程刚被操作系统创建还没有启动的时候的状态
    • 就绪态:线程已经获取到所有启动所需要的资源的时候的状态
    • 运行态:线程获取到处理器的使用权的时候的状态
    • 等待态(阻塞态):[线程等待 IO 操作返回数据时的状态]{.red}
    • 终止态:线程执行完所以任务之后的状态
  • 线程上下文切换:
  • 并行与并发:

为什么使用线程?

  • [核心:多个线程能够提高进程的工作效率]{.red}

    • 多线程从两方面提升效率
      • [提升进程对于用户的 响应时间:多线程的诞生就是为了改善响应时间]{.red}
      • [提升进程对于计算任务的 执行速度:多核处理器出现之后才能够提高进程的执行速度]{.red}
    • 提升工作效率的两个前提:
      1. [线程执行的任务主要是 IO 密集型而不是 CPU 密集型]{.red}
        • 解释 CPU 密集型:
          • [如果线程执行的是 CPU 密集型任务,那么处理器就会长时间处于满负荷运载的状态]{.aqua}
          • [如果存在多线程并发的情况,那么处理器就必须花费时间执行上下文切换,浪费执行计算的时间]{.aqua}
        • 解释 IO 密集型:
          • [如果线程执行的 IO 密集型任务,那么线程因为 IO 过程而阻塞时,处理器也会相应的空闲下来]{.orange}
          • [避免处理器长时间处于空闲,就可切换上下文让其他等待的线程执行,让处理器可以重新工作]{.orange}
        • 总结:多线程对于 IO 密集型任务可以很好地 [改善进程的响应时间]{.red},当前线程阻塞了可以换其他线程
      2. [计算机最好是多核处理器而不是单核处理器]{.red}
        • [单核处理器下的多线程只能实现并发,只能够提高进程的响应时间,而无法提高进程的运算速度]{.aqua}
          • 每个计算核只能够运行一个线程,那么需要执行其他的线程就必须上下文切换
          • 线程无论执行的是 CPU 还是 IO 密集型都无法从实质上提升进程的执行速度的,毕竟只有并发
        • [多核处理器下的多线程可以同时实现并行和并发,既可以提高响应时间也可以提供运算速度]{.orange}
          • 多个线程之间可以完全相互不干扰的并行执行,处理器不需要对并行的线程执行上下文切换
          • 不过依然会对单个处理器上并发的线程执行上下文切换,避免处理器的长时间等待
    • 总结:[不能够非常笼统地说多线程就是能够提高进程的工作效率,具体情况需要具体分析,使用不当反而造成性能下降]{.red}
      1. [单核处理器下的多线程是为了改善进程的响应时间,而无法提高进程的执行速度,毕竟始终只有一个线程在运行]{.purple}
      2. [多核处理器的出现是为了更好地支持多线程,只要多个线程能够并行执行,显然就可以提高执行速度,在并行的基础上实现并发,又可以改善响应时间]{.purple}
  • [多个线程可以共享同一个进程提供的所有资源,多个进程之间则是独立的不可共享的]{.pink}

    • 优点:多线程通信和协作时可以随意使用相同资源,避免重复创建相同的资源
    • 缺点:多线程使用共享资源时需要确保线程安全
  • [线程的创建和销毁的成本相对于进程非常低,减轻操作系统的负担]{.pink}

    • 细节:即使线程的创建和开销成本很低,但是创建或者销毁大量的线程也是非常消耗资源的
  • Amdahl(阿姆达尔定律)

线程如何实现?

线程实现

  • 前言:
    • OS 层面的线程:几乎绝大多数操作系统都是支持多线程的
    • 语言层面的线程:每种语言实现线程的方式各不相同
      • 内核级线程代表(线程):Java、C\C++
      • 用户级线程代表(协程/纤程):C#、Python、Golang
  • 现状:
    • 线程的创建和销毁对于日益增加的并发量来说依然是笔不小的开销,即使有线程池、多路复用等方法的改善
    • 如今大多数新兴语言,诸如 Python、Golang 都是采用的协程而不是传统的线程,从而支持更高的并发量
内核级线程
  • 定义:
  • 特点:
  • 图示:
用户级线程
  • 定义:
  • 特点:
  • 图示
混合级线程
  • 定义:
  • 特点:
  • 图示:

线程调度

:::primary

每种算法的详细内容也放在操作系统的笔记中,这里要是每种算法都详细讲述实在是太多了

:::

  • 前言:
    • [内核级线程调度和进程调度基本类似]{.red}
    • [用户级线程的调度算法完全取决于程序员编写的逻辑]{.red}
  • 两类调度方式
    • 协同式调度:先来先服务算法
    • 抢占式调度:时间片轮转法
    • 最短作业优先、优先权调度、多级队列调度既可以是协同式的,也可以是抢占式的

线程通信?线程同步?

:::primary

① 之所以将标题取为疑问的方式,是因为网络上关于这一块内容存在非常多滥竽充数和胡乱的解释

② 首先明确通信和同步是两种完全不同的行为,但是却又有一定的联系,接下来将会提到

③ 通信和同步的具体方式将会放在之后来讲,这里受限于篇幅不在此展开

:::

  • 前言:

    • [只有进程间存在通信的概念,线程间通信只是一种伪术语]{.red}
      • [多个进程之间是相互隔离的,没有办法直接交互或者共享数据,所以才需要通信]{.pink}
      • [多个线程之间本身就是共享同一个进程的所有资源,相互之间可以直接交互数据,没有所谓通信的概念]{.pink}
      • [多个线程之间天生共享进程的资源,可以认为多线程默认通信方式就是共享内存]{.pink}
    • [只要进程或者线程采用共享内存的方式通信,就会存在线程安全问题,就需要采用同步手段解决]{.red}
    • [进程之间通常才会考虑通信方式,线程之间通常只考虑同步方式]{.red}
  • 进程间通信:① 管道 ② 消息队列 ③ 共享内存 ④ 套接字 ⑤ 信号 ⑥ 信号量(常见的六种)

    • [本地进程通信:共享内存、信号、管道]{.red}
    • [分布式进程通信:消息队列、套接字、RPC、Stream]{.aqua}
    • 细节:
      • 管道:可以细分为匿名管道和命名管道两种类型
      • 信号量:本质是同步手段而不是进程间的通信手段
  • 线程间同步:

    • 前提:[线程间通信是基于共享内存的方式进行通信的,所以不再考虑通信的方式]{.red}
      1. Java 提供的 sleep()、wait()、join()等方法严格意义上不能够算作线程通信
      2. 毕竟线程之间没有办法交互数据,也没有办法互相通知对方完成什么事情,只能够是一种协作方式吧
    • 竞争条件:[多线程并发访问共享资源的结果和访问的顺序有关的情况称为竞争条件]{.red}
    • 临界区:[线程使用共享资源的代码块]{.red}
    • 方式:
      • [硬件方式:屏蔽中断、TSL、Swap、CAS、LL/SC]{.red}
        • TSL(Test And Set):x86 指令集中的命令是 XCHG
        • CAS(Compare And Swap)
        • LL/SC:Load-Linked/Store-Conditional
      • [软件方式:锁变量、严格轮换法、Peterson、信号量(包括互斥量)]{.red}
      • [编译器层面:管程]{.red}
    • 细节:上述同步方案都是基于操作系统或者编译器层面的,语言层面自行实现的锁不在此列出

线程应用

  • 应用场景:

    • [Web 开发中]{.orange}大多数情况都是不会用到多线程技术的,这些工作已经由各种各种的中间件(Tomcat、Netty)为你完成
    • [中间件开发中]{.aqua},诸如服务器开发才会使用到多线程的知识,而且是非常重要的
  • 服务器开发

番外

线程与进程

线程与协程

:::primary

什么是协程?

协程的概念,为什么要用协程,以及协程的使用

什么是协程?

:::

  • [银弹:协程 + 异步 IO]{.rainbow}
什么是协程?
为什么要使用协程?
协程比线程好在哪里?

Java 线程基础知识

线程实现

创建线程

:::info

① 需要具有 Lambda 表达式和匿名内部类的相关知识

② 采用匿名内部类或者 Lambda 表达式实现的线程在使用外界变量时,那个变量必须是不可变类型的

:::

extends Thread
  • 实现过程
    • [继承 Thread 类]{.red}
      • [extends 关键字实现继承]{.pink}
      • [匿名内部类实现继承]{.pink}
    • 重写 run 方法
      • 线程启动后会自动调用 run 方法中的逻辑
      • 线程执行完成 run 方法之后就会自行结束
// 前提: 线程是不可以被多次启动的
public static void main(String[] args){
// 1. 采用匿名内部类的方式继承 Thread 类
new Thread("Thread-0"){
@Override
public void run(){
log.debug("Hello, Thread!");
}
};
}
//============================
//2 采用 extends 关键字的方式继承 Thread
class MyThread extends Thread{
@Override
public void run(){
log.debug("Hello, Thread!");
}
}
  • 优点:

    • [可以给线程类任意添加成员变量]{.red}
    • [可以直接使用 this 关键字获取当前正在执行的线程]{.red}
  • 缺点:

    • [线程继承 Thread 类之后无法继承其他类]{.green}
    • [线程和任务存在耦合,同一个类派生的线程对象都只能够执行相同的任务,没有办法执行不同的任务]{.green}
  • 线程安全相关:

    • 核心:[如果构造函数没有引入外界的成员变量,那么继承实现的线程就是线程安全的]{.red}

    • 解释:

      1. [传入多个线程对象的构造函数的中的变量可能是相同的,所以构造函数引入外界成员变量有线程安全问题]{.blue}
      2. [没有传入外界变量时,每个线程对象都拥有独立的成员变量,彼此之间不会共享变量,没有共享就不会造成线程安全性问题]{.blue}
    • 图示:

      创建
implements Runnable
  • 前提:[Runnable 实现方式并不是创建线程类,而是创建线程执行的任务类]{.red}

    • 任务类中主要编写线程需要执行的逻辑
    • 线程类主要负责线程自身的资源调度等行为
    • 线程类采用组合的方式使用我们编写的任务类
    // 成员变量
    private Runnable target;
    // 构造方法
    public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    // 如果线程对象的构造方法中没有传入任务类, 那么就会执行线程默认的 run 方法
    // 如果线程对象的构造方法中传入了任务类, 那么显然执行的就是 target 对象的 run 方法
    @Override
    public void run() {
    if (target != null) {
    target.run();
    }
    }
  • 实现过程

    • [实现 Runnable 接口]{.red}
      • implements 关键字实现
      • Lambda 表达式 / 匿名内部类实现
    • 重写 run 方法
    • 创建 Thread 对象,并将 Runnable 实例传入构造方法中
    // 前提: 线程是不可以被多次启动的
    public static void main(String[] args{
    // 1、采用 Lambda 表达式实现的
    new Thread(()->{
    log.debug("Hello, Lambda Thread!");
    }, "Thread-0");

    // 2、线程这里执行的就是任务对象中的 run 方法
    new Thread(new MyRunnalbe());
    }
    //=================================
    // 2、采用 implements 关键字实现的
    class MyRunnable implements Runnable{
    @Override
    public void run(){
    log.debug("Hello, Thread!");
    }
    }
  • 优点:

    • [任务类可以继续继承或者实现其他类或者接口]{.red}
    • [线程和任务解耦,每个线程对象可以执行相同的任务也可以执行不同的任务]{.red}
  • 缺点:

    • [无法直接为每个线程添加成员变量]{.green}

    • [任务类只能够调用 Thread.current 方法来判断当前运行的线程,而无法调用 this 来判断运行的线程]{.green}

      Runnable 只是 Thread 执行的任务而不是线程,显然不能够依靠 this 关键字知道当前正在运行的线程

  • 线程安全:

    • 核心:

      • [执行相同任务的线程会共享任务中的变量,从而容易导致线程安全问题]{.red}
      • [执行不同任务的线程如果在构造函数中引入外界成员变量,也会导致线程安全问题]{.red}
    • 图示

      创建线程-2

:::warning

① 线程安全的相关话题将会在之后的多线程提到

② 总结来说就是继承实现的线程不容易出现线程安全问题,接口实现的线程容易造成线程安全

:::

implements Callable< T >
  • 前提:Callable 实现方式也不是创建线程类,而是创建线程执行的任务类

  • 实现过程

    • [实现 Callable 接口,确定返回值类型]{.red}
      • implements 实现
      • Lambda 表达式 / 匿名内部类实现
    • [重写 call 方法,提供返回值]{.red}
    • [创建 FutureTask 对象,并将 Callable 实例传入对象的构造函数中,确定返回值类型]{.red}
      • FutureTask 对象让调用线程阻塞等待线程执行结束,并且获取返回值
    • [创建 Thread 对象,并将 FutureTask 实例传入构造函数中]{.red}
    // 前提: 线程是不可以被多次启动的
    public static void main(String[] args) throws ExecutionException, InterruptedException
    {
    // 1、Lambda 表达式实现:
    FutureTask<Boolean> task = new FutureTask<>(()->{
    log.debug("Hello");
    Thread.sleep(1000);
    return true;
    });
    new Thread(task, "Thread-3").start();
    // 阻塞等待结果
    log.debug("{}", task.get());

    // 2.2、创建 Callable 实例
    MyRunnable runnable = new MyRunnable();
    // 2.3、创建 FutureTask 对象
    FutureTask<Boolean> task = new FutureTask(runnable);
    // 2.4、创建线程
    new Thread(task);
    // 2.5、接收返回结果
    task.get();

    }
    //=====================
    // 2、implements 实现
    class MyRunnable implements Callable<Boolean>{
    @Override
    public Boolean call() throws Exception{
    System.out.println("Hello");
    return true;
    }
    }
  • 细节:[Callable 接口和 Runnable 接口本质是一种实现方式]{.blue}

    • Thread 实际上是没有能够接受 FutureTask 或者 Callable 类型的构造方法

      public Thread() {
      init(null, null, "Thread-" + nextThreadNum(), 0);
      }
      public Thread(Runnable target) {
      init(null, target, "Thread-" + nextThreadNum(), 0);
      }
    • 之所以 FutureTask 能够作为参数传递,那只能够是因为它实现了 Runnable 接口

      public class FutureTask<V> implements RunnableFuture<V> {
      ...
      }
    • 所以这两种方式其实本质是相同的

启动线程

  • start() 方法

    • 作用:[线程对象调用 start 方法后就会让虚拟机向操作系统发出请求,创建新的线程]{.pink}

    • 细节:

      • [线程对象不可以重复调用 start 方法,否则会抛出异常]{.red}
      • [线程对象只有调用 start 方法才会创建线程,只有这个方法才会调用 start0 方法启动线程]{.red}
    • 源码:

      // 本地方法, 虚拟机会向操作系统申请创建新的线程
      private native void start0();

      public synchronized void start() {
      ...
      // 标记线程是否成功启动
      boolean started = false;
      try {
      //====================
      // 调用本地方法 start0() 启动线程
      start0();
      // 线程成功启动之后会将标志置为 true
      started = true;
      //====================
      } finally {
      try {
      // 如果线程已经启动了, 却再次调用 start 方法会抛出异常
      if (!started) {
      group.threadStartFailed(this);
      }
      } catch (Throwable ignore) {

      }
      }
      }
  • run() 方法

    • 作用:[线程启动之后默认调用的方法,执行方法内的代码]{.pink}

    • 细节:[调用者可以执行该方法中的代码,但是无法启动线程]{.red}

    • 源码:

      @Override
      public void run() {
      // 内部根本没有调用 start0 方法,所以也就不会真正意义上的创建新的线程
      if (target != null) {
      target.run();
      }
      }
  • 核心区别:[start 能够启动线程而 run 方法是显然不可以的]{.red}

线程类型

:::info

① 线程类型仅分为两种类型:用户线程和守护线程

② 进程类型可以根据特点而分为多种类型:CPU 密集型、IO 密集型、独立进程、协作进程、僵尸进程、孤儿进程

③ 两者的分类是完全不同的,不要搞混淆了

:::

  • 用户线程:

    • 定义:默认创建的所有线程都是用户线程
    • 特点:[虚拟机会等待所有用户线程都结束之后才会停止运行]{.red}
  • 守护线程

    • 定义:为用户线程提供服务的被称为守护线程
    • 特点:[虚拟机不会等待所有守护线程结束才停止,只要所有用户线程结束虚拟机就停止]{.red}
  • 测试

    • 测试用户线程:
    @Slf4j(topic = "c.daemon")
    public class ThreadDaemon
    {
    public static void main(String[] args)
    {
    Thread user = new Thread(() ->{
    while (true)
    {
    // 空循环
    }
    });

    user.start();
    // 测试结果:主线程都结束了,但是虚拟机依然没有停止
    log.debug("主线程结束...");

    }
    }
    • 测试守护线程
    @Slf4j(topic = "c.daemon")
    public class ThreadDaemon
    {
    public static void main(String[] args)
    {
    Thread daemon = new Thread(() ->{
    while (true)
    {

    }
    });

    daemon.setDaemon(true);
    daemon.start();
    // 测试结果:虚拟机在主线程停止之后立刻就停止了
    log.debug("主线程结束...");

    }
    }

线程状态

Java-线程状态
  • 前提:

    • [OS 给线程定义了五种基本状态]{.red}
    • [Java 在 OS 的基础上重新给线程定义了六种基本状态]{.red}
  • 基本状态:

    • NEW(初始态):[线程对象没有调用 start 方法启动时的状态]{.green}
    • RUNNABLE(运行态):[包含线程准备运行、线程正在运行、线程被 IO 阻塞三种状态]{.blue}
      • 虚拟机无法区分线程是处于运行态还是处于就绪态,所以将这两种都划分为 Runnable
      • 虚拟机也不知道被 IO 阻塞的线程是否可以运行,所以也认为是 Runnable
    • TIMED_WAITING(有限等待态):[线程因为方法调用而陷入的有限期等待,一定时间后会由虚拟机唤醒]{.orange}
    • WAITING(无限等待态):[线程因为方法调用而陷入的无限期等待]{.pink}
    • BLOCKED(阻塞态):[线程因为无法获取到锁而陷入的阻塞状态]{.aqua}
    • TERMINATED(终止态):[线程执行完成 run 方法之后的状态]{.gray}
  • 状态转换

    • [NEW => RUNNABLE:仅有 start 方法可以实现]{.red}
    • [RUNNABLE => WAITING:wait、join、park]{.red}
      • WAITING => RUNNABLE:notify / notifyAll,等待的线程结束、unpark
    • [RUNNABLE => TIMED_WAITING:wait、join、park、sleep]{.red}
      • TIMED_WAITING => RUNNABLE:限定时间结束之后,虚拟机主动唤醒线程
    • [RUNNABLE => BLOCKED:线程竞争锁失败]{.red}
      • BLOCKED => RUNNABLE:线程竞争锁成功并且开始运行
      • 常见锁:synchronized、ReentrantLock、ReentrantReadWriteLock
    • [RUNNABLE => TERMINATED:线程执行结束]{.red}
  • 测试线程状态

    • 代码
    // 测试线程的六种状态
    @Slf4j(topic = "c.ThreadState")
    public class ThreadState
    {
    public static void main(String[] args) throws InterruptedException
    {
    // NEW 状态: 没有调用 start 方法
    Thread t1 = new Thread(()->{log.debug("初始态...");});

    // RUNNABLE 状态: 调用 start 方法后线程开始运行的状态
    Thread t2 = new Thread(() -> {
    while (true){

    }
    });

    // TERMINATED 状态: 线程先于主线程结束, 就能够看到结束态
    Thread t3 = new Thread(() -> {log.debug(""); });

    // TIMED_WAITING 状态: 线程陷入休眠, 一段时间后想来\
    Thread t4 = new Thread(() -> {
    try {
    TimeUnit.SECONDS.sleep(1000);
    }
    catch (InterruptedException e) {
    e.printStackTrace();
    }
    });

    // WAITING 状态: 等待其余线程结束的时候可以看到等待状态
    Thread t5 = new Thread(() ->
    {
    synchronized (ThreadState.class){
    try {
    t4.join();
    }
    catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    });

    // BLOCKED 状态: 获取陷入等待状态的线程持有的锁, 就能够看到阻塞态
    Thread t6 = new Thread(() -> {
    synchronized (ThreadState.class){

    }
    });

    // 启动所有线程
    t2.start();
    t3.start();
    t4.start();
    t5.start();
    t6.start();
    // 主线程休眠
    TimeUnit.SECONDS.sleep(1);

    // 打印状态
    log.debug("t1 state: {}", t1.getState());
    log.debug("t2 state: {}", t2.getState());
    log.debug("t3 state: {}", t3.getState());
    log.debug("t4 state: {}", t4.getState());
    log.debug("t5 state: {}", t5.getState());
    log.debug("t6 state: {}", t6.getState());
    }
    }
    • 结果
    测试线程状态

线程优先级

线程调度

  • 核心:
    • [Java 线程采用的是内核级线程实现的,内核级线程通常都是采用抢占式调用]{.red}
    • [抢占式调用的执行时间由 OS 控制而不受程序员的控制,所以 Java 线程几乎不可控]{.red}
  • 细节:[线程的优先级和 Yield 方法几乎都只能向 OS 建议优先调度某些线程,而没有办法强行控制]{.red}

线程协作

:::primary

线程协作的常用方法也会在之后的笔记中讲述

:::

线程同步

:::primary

线程同步的相关内容在多线程基础中讲述,需要理解线程安全之后才能够理解为什么需要线程同步

:::

Author: Fuyusakaiori
Link: http://example.com/2021/10/19/juc/线程基础/基础知识/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.