欢迎光临
我们一直在努力

线程的基本概念/创建/执行

4. 线程(TCB)

4.1 为什么有了进程还需要线程?

进程是资源分配的基本单位。创建和管理进程需要一定的系统开销。当客户端频繁向服务器发送请求时,如果每个任务都采用独立进程处理,就可能产生较大的资源开销。

  • 为了降低开销,可以在一个进程中创建多个线程,让这些线程共享所属进程的资源,分别执行不同的任务。

  • 进程: 主要负责资源的分配与管理。

  • 线程: 使用所属进程的资源,负责执行具体任务,是 CPU 调度的基本单位。

  • 线程(轻量级进程): 相比进程,线程不需要独立拥有一套完整的进程资源,因此创建和切换的开销通常较小。
    注意: 进程结束后,操作系统会按照相应机制回收其资源。线程共享的是所属进程当前拥有的资源,并不是进程结束后继续使用遗留的内存。

4.2 进程与线程的关系

4.2.1 包含关系

  • 一个操作系统上可以运行多个进程,每个进程又可以包含一个或多个线程。
  • 一个进程可以包含多个线程。
  • 进程与线程是包含关系。
  • 每个进程都有自己的独立虚拟地址空间,同一进程中的多个线程共享该进程的地址空间及相应资源。
  • 4.2.2 进程与线程的区别

    对比内容进程线程
    主要作用 管理和拥有资源 执行任务
    基本单位 资源分配的基本单位 CPU 调度的基本单位
    内存空间 拥有独立的虚拟地址空间 同一进程中的线程共享地址空间
    资源开销 创建和管理开销通常较大 创建和管理开销通常较小

    重点记忆:

    • 进程是资源分配的基本单位。
    • 线程是 CPU 调度的基本单位。
    • 一个进程可以包含多个线程。
    • 同一进程中的多个线程共享进程的资源。

    4.2.3 线程与 PCB、TCB

    • PCB(进程控制块): 用于记录和管理进程相关的信息。
    • TCB(线程控制块): 用于记录和管理线程相关的信息。

    入门阶段可以这样理解:

  • 一个进程通常对应一个 PCB。
  • 一个线程通常对应一个 TCB。
  • 一个进程可以包含多个线程,因此一个进程可以关联多个 TCB。
  • 4.2.4 进程与线程的内存关系

    每个进程拥有独立的虚拟地址空间。同一个进程中的多个线程共享该进程的地址空间和部分资源。

    因此:

    • 不同进程之间通常不能直接访问彼此的普通内存。
    • 同一进程中的线程可以访问共享数据。
    • 一个线程修改共享数据,可能影响同一进程中的其他线程。
    • 进程终止后,其资源通常会被操作系统回收。

    4.2.5 线程安全问题

    • 线程之间共享内存,因此多个线程同时操作同一份数据时,可能相互影响。
      例如:
    • 多个线程同时修改同一个变量,如果没有适当的控制,就可能产生数据不一致的问题。(类似于不可重复读,前后两次读取到的数据不一致)
    • 线程执行过程中如果出现未捕获的异常,通常会导致对应线程终止,但不意味着整个 JVM 一定终止。因此,需要妥善处理线程中的异常,并注意多个线程共享数据时可能产生的问题。

    5. 创建线程的方式

    Java 标准库提供了 Thread 类来执行线程,并提供了 Runnable 接口来描述线程需要执行的任务。

    常见的创建线程的方式包括:

  • 继承 Thread 类。
  • 实现 Runnable 接口。
  • 使用匿名内部类继承 Thread。
  • 使用匿名内部类实现 Runnable。
  • 使用 Lambda 表达式。
  • 5.1 Thread 类

    5.1.1 Thread 类的作用

  • Thread 是 Java 标准库提供的线程类,可以通过创建 Thread 对象来表示一个线程。
  • 创建子类MyThread子类继承 Thread 类并重写 run() 方法,可以定义线程需要执行的任务。
    • 继承 Thread 类: 创建一个 Thread 子类。
    • 重写 run() 方法: 编写线程要执行的具体逻辑。
    • 创建线程对象: 创建子类实例。
    • 调用 start() 方法: 启动新线程,使其执行 run() 方法中的任务。

    5.1.2 run()与 start() 的区别

    方法作用
    run() 定义线程需要执行的任务;直接调用时只是普通的方法调用
    start() 启动新线程,使任务在新线程中执行

    重点:

    t.start(); // 启动新线程
    t.run(); // 普通方法调用,不会启动新线程

    5.1.3 sleep() 方法

    Thread.sleep() 用于让当前正在执行的线程休眠指定时间,主动进入阻塞状态。

    写在哪个线程内部就让哪个线程进入休眠状态。

    例如:

    Thread.sleep(1000); // 当前线程休眠 1000 毫秒

    • 调用该方法时,需要处理 InterruptedException 异常。
    • 可以使用 try-catch 捕获异常,也可以在允许的情况下通过 throws 声明异常。
    • 需要注意:Thread.sleep() 是静态方法,调用它时休眠的是当前执行该语句的线程,而不是根据变量 t 指定休眠哪个线程。

    5.1.4 主线程

    • Java 程序执行 main() 方法时,会运行主线程。
    • 创建并启动其他线程后,主线程和新线程可以分别执行各自的任务。
    • 多个线程的执行顺序不确定,由操作系统的线程调度机制等因素决定。

    5.1.5 JVM 中的其他线程

    • 程序运行时,除了自己创建的线程,例如主线程和通过 Thread 创建的线程,JVM 还可能创建其他线程,用于垃圾回收,网络通信等工作。

    5.2 Runnable 接口

    5.2.1 Runnable 接口的作用

    • Runnable 是 Java 提供的线程任务接口,其中定义了 run() 方法。
    • 通过实现 Runnable 接口并重写 run() 方法,可以描述线程需要执行的任务
    • 实现 Runnable 接口本身不会直接启动线程,还需要将实现类了接口的实例对象作为参数传入 Thread 的构造方法,再调用 start()。

    5.2.2 通过Thread类 与 Runnable接口创建进程的区别

    对比内容Thread 类Runnable 接口
    实现方式 子类 继承Thread 类 子类实现Runnable 接口
    任务编写位置 子类重写 run() 方法 子类实现接口中的 run() 方法
    线程对象 子类实例本身就是线程对象 需要将实现了Runnable接口的子类实例化对象传给 Thread
    启动方式 t.start() t. start()

    5.2.3 两种方式的理解

    第一种:继承 Thread 类

    • 子类 MyThread 重写 run() 方法,描述线程要执行的任务。
    • 创建 MyThread 实例对象后,可以将其赋值给 Thread 类型的变量(向上转型)。

    Thread t = new MyThread();

    • 再调用 start() 启动线程。

    第二种:实现 Runnable 接口

    • MyRunnable 实现 Runnable 接口,重写 run() 方法,描述线程要执行的任务。
    • 然后创建 Thread 实例,将 MyRunnable 实例作为参数传入。
    • 最后调用 start() 启动线程。

    6. 创建线程的五种代码写法

    6.1 继承 Thread 类

    • 子类实现Thread类,重写run( )方法,方法中是新线程要执行的任务,实例化子类对象,调用 start( )方法执行 run()方法的语句。
    • 这里的子类对象就是要执行的线程。
    • 而实现Runnable接口的MyRunnable子类是要执行的实例化对象,需要把子类MyRunnable当作参数传给Thread

    6.1.1 实现步骤

  • 创建 MyThread 子类并继承 Thread。
  • 重写 run() 方法。
  • 创建 MyThread 实例对象。
  • 调用 start() 启动线程。
  • 6.1.2 完整代码

    package thread;
    // 创建 Thread 的子类
    class MyThread extends Thread {
    // 重写 run 方法,定义新线程需要执行的任务
    @Override
    public void run() {
    while (true) {
    System.out.println("hello thread");

    try {
    // 当前线程休眠 1000 毫秒
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    }
    }
    }
    public class Demo1 {
    public static void main(String[] args) {
    // 创建线程对象,此时只是完成准备工作
    Thread t = new MyThread();//向上转型
    // 启动新线程
    t.start();

    // 主线程执行自己的任务
    while (true) {
    System.out.println("hello world");
    try {
    //当前线程休眠 1000 毫秒
    Thread.sleep(1000);
    //判断哪个线程休眠,不要看 sleep()前面写了谁,而要看是哪一个线程正在执行这行代码。
    //这里即使写成了t.sleep(1000);也是主线程休眠,因为这是在主线程里面的代码。
    //Thread.sleep(1000);写在哪个线程的代码块中哪个线程休眠
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }

    6.1.3 重点理解

    • MyThread 描述新线程需要执行的任务。
    • t.start() 启动新线程。
    • main() 方法中的循环由主线程执行。
    • t.run() 不会创建新线程。
    • 主线程和新线程的执行顺序不确定。
    • Thread.sleep(1000);写在哪个线程的代码块中哪个线程休眠

    6.2 实现 Runnable 接口

    6.2.1 实现步骤

  • 创建 MyRunnable 类并实现 Runnable 接口。
  • 重写 run() 方法。
  • 创建 MyRunnable 实例对象。
  • 将实例对象传入 Thread 构造方法。
  • 调用 start() 启动线程。
  • 6.2.2 完整代码

    package thread;
    // 实现 Runnable 接口,描述线程要执行的任务
    class MyRunnable implements Runnable {
    @Override
    public void run() {
    while (true) {
    System.out.println("hello thread");
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    }
    }
    }
    public class Demo2 {

    public static void main(String[] args) {
    // 创建 Runnable 实现类的实例
    MyRunnable runnable = new MyRunnable();
    // 将任务对象传入 Thread
    Thread t = new Thread(runnable);
    // 启动新线程
    t.start();

    // 主线程执行自己的任务
    while (true) {
    System.out.println("hello main");
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    }
    }
    }

    6.2.3 重点理解

    • MyRunnable 描述线程要执行的任务,是任务对象。
    • Thread 对象用于启动线程。
    • new Thread(runnable) 将任务对象传给线程对象。
    • t.start() 启动新线程。

    6.3 使用匿名内部类继承 Thread

    6.3.1 实现步骤

    • 直接在 main() 方法中创建 Thread 的匿名内部类对象,并重写 run() 方法。
    • 上面6.1的简便写法都是一样的理解,只是添加了一个匿名内部类。

    6.3.2 完整代码

    package thread;
    public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
    // 创建 Thread 的匿名内部类对象
    Thread t = new Thread() {
    @Override
    public void run() {
    while (true) {
    System.out.println("hello thread");
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    }
    }
    };
    // 启动新线程
    t.start();

    // 主线程执行自己的任务
    while (true) {
    System.out.println("hello main");
    Thread.sleep(1000);
    }
    }
    }

    6.4 使用匿名内部类实现 Runnable

    6.4.1 实现步骤

    • 直接创建 Runnable 的匿名内部类对象,重写 run() 方法,再将任务对象传给 Thread。
    • 上面6.2的简便写法都是一样的理解,只是添加了一个匿名内部类。

    6.4.2 完整代码

    package thread;
    public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
    // 创建 Runnable 的匿名内部类对象
    Runnable runnable = new Runnable() {
    @Override
    public void run() {
    while (true) {
    System.out.println("hello thread");
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    }
    }
    };

    // 将 Runnable 任务对象传给 Thread
    Thread t = new Thread(runnable);
    // 启动新线程
    t.start();

    // 主线程执行自己的任务
    while (true) {
    System.out.println("hello main");
    Thread.sleep(1000);
    }
    }
    }

    6.5 使用 Lambda 表达式

    6.5.1 实现步骤

    • 使用 Lambda 表达式描述 Runnable 任务,可以省略匿名内部类的部分代码。

    6.5.2 完整代码

    package thread;
    public class Demo5 {
    public static void main(String[] args) {
    //新线程定义任务代码块
    Thread t = new Thread(() -> {
    while (true) {
    System.out.println("hello thread");
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    }
    });
    //启动新线程
    t.start();

    //主线程执行自己的任务
    while (true) {
    System.out.println("hello main");
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    }
    }
    }

    6.7五种线程创建方式的进程关系

    6.7.1 五种方式是否运行在同一个进程?

    是的,五种方式创建的新线程,都与 main() 方法的主线程运行在同一个 Java 进程中。

    五种创建线程的方式:

  • 继承 Thread 类(Demo1)。
  • 实现 Runnable 接口(Demo2)。
  • 使用匿名内部类继承 Thread(Demo3)。
  • 使用匿名内部类实现 Runnable(Demo4)。
  • 使用 Lambda 表达式创建线程(Demo5)。
    它们的区别在于如何定义线程执行的任务,而不是创建不同的进程。
  • 6.7.2 五种方式的运行关系

    Java 进程(JVM)
    │
    ├── 主线程(main)
    │ ├── 执行 main() 方法
    │ ├── 输出 hello main / hello world
    │ └── Thread.sleep(1000)
    │
    └── 新线程(Thread)
    ├── 执行 run() 方法中的任务
    ├── 输出 hello thread
    └── Thread.sleep(1000)

    6.7.3 五种方式的对比

    代码创建线程的方式是否与主线程属于同一进程
    Demo1 继承 Thread 类 是
    Demo2 实现 Runnable 接口 是
    Demo3 匿名内部类继承 Thread 是
    Demo4 匿名内部类实现 Runnable 是
    Demo5 Lambda 表达式 是

    6.7.4 为什么属于同一个进程?

    • 以 Demo5 为例:

    Thread t = new Thread(() -> {
    // 新线程执行的任务
    System.out.println("hello thread");
    });
    t.start(); // 启动新线程

    • 程序启动时,JVM 运行在 Java 进程中,主线程执行 main() 方法。
    • 调用 t.start() 后,JVM 启动新线程执行任务,但不会因此启动一个新的 Java 进程。

    因此:

    • main() 方法由主线程执行。
    • Lambda 表达式中的任务由新线程执行。
    • 两个线程属于同一个 Java 进程。
    • 两个线程共享所属进程的地址空间及相应资源,但各自拥有独立的调用栈和执行状态。

    6.7.5 本节重点总结

  • 一个进程可以包含多个线程。
  • Java 程序启动后,主线程执行 main() 方法。
  • 调用 Thread.start() 可以启动新线程,但不会自动创建新的 Java 进程。
  • 五种线程创建方式的区别在于任务的定义方式。
  • 判断线程属于哪个进程,不能只看采用了哪种创建方式。
    一句话记忆:Demo1~Demo5 都是在一个 Java 进程中,由主线程和新线程分别执行各自的任务。
  • 赞(0)
    未经允许不得转载:171主机测评 » 线程的基本概念/创建/执行
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址