神、头像
线程的基本概念/创建/执行封面图

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

4. 线程(TCB)

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

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

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

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

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

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

4.2 进程与线程的关系

4.2.1 包含关系

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

4.2.2 进程与线程的区别

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

重点记忆:

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

4.2.3 线程与 PCB、TCB

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

入门阶段可以这样理解:

  1. 一个进程通常对应一个 PCB。
  2. 一个线程通常对应一个 TCB。
  3. 一个进程可以包含多个线程,因此一个进程可以关联多个 TCB。

4.2.4 进程与线程的内存关系

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

因此:

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

4.2.5 线程安全问题

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

5. 创建线程的方式

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

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

  1. 继承 Thread 类。
  2. 实现 Runnable 接口。
  3. 使用匿名内部类继承 Thread。
  4. 使用匿名内部类实现 Runnable。
  5. 使用 Lambda 表达式。

5.1 Thread 类

5.1.1 Thread 类的作用

  1. Thread 是 Java 标准库提供的线程类,可以通过创建 Thread 对象来表示一个线程。
  2. 创建子类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 实现步骤

  1. 创建 MyThread 子类并继承 Thread。
  2. 重写 run() 方法。
  3. 创建 MyThread 实例对象。
  4. 调用 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 实现步骤

  1. 创建 MyRunnable 类并实现 Runnable 接口。
  2. 重写 run() 方法。
  3. 创建 MyRunnable 实例对象。
  4. 将实例对象传入 Thread 构造方法。
  5. 调用 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 进程中。

五种创建线程的方式:

  1. 继承 Thread 类(Demo1)。
  2. 实现 Runnable 接口(Demo2)。
  3. 使用匿名内部类继承 Thread(Demo3)。
  4. 使用匿名内部类实现 Runnable(Demo4)。
  5. 使用 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是
Demo5Lambda 表达式是

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 本节重点总结

  1. 一个进程可以包含多个线程。
  2. Java 程序启动后,主线程执行 main() 方法。
  3. 调用 Thread.start() 可以启动新线程,但不会自动创建新的 Java 进程。
  4. 五种线程创建方式的区别在于任务的定义方式。
  5. 判断线程属于哪个进程,不能只看采用了哪种创建方式。
    一句话记忆:Demo1~Demo5 都是在一个 Java 进程中,由主线程和新线程分别执行各自的任务。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2205_76017107/article/details/169169490

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--