当前位置: 首页 > news >正文

Java大师成长计划之第8天:Java线程基础

📢 友情提示:

本文由银河易创AI(https://ai.eaigx.com)平台gpt-4o-mini模型辅助创作完成,旨在提供灵感参考与技术分享,文中关键数据、代码与结论建议通过官方渠道验证。

在今天的学习中,我们将继续探索Java编程语言的重要特性之一——线程。线程是Java实现并发编程的核心,能够让你的应用程序在执行同时可以进行多项任务,提高应用程序的性能和响应能力。本文将详细介绍Java线程的创建方式与生命周期。

一、线程创建

在Java中,线程的创建是实现并发编程的第一步,主要有两种常用方式:继承Thread实现Runnable接口。我们将详细讨论这两种方式,并分析它们各自的优缺点。

1. 继承Thread类

使用继承Thread类的方式来创建线程,首先需要创建一个类并让它继承Thread类。在这个新的类中,我们需要重写run()方法。这个方法包含的是线程的执行代码。一旦创建了这个新的线程类的实例,就可以调用start()方法来启动线程,从而调用run()方法。

示例代码:

class MyThread extends Thread {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + " - Count: " + i);try {Thread.sleep(500); // 模拟线程执行过程中休眠} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {MyThread thread1 = new MyThread();MyThread thread2 = new MyThread();thread1.start(); // 启动线程1thread2.start(); // 启动线程2}
}

优点和缺点

优点

  • 简单直观,特别适合于快速创建和测试线程。

缺点

  • Java是单继承的,也就是说,一个类只能继承一个类。如果一个类已经继承了其他类,就不能再继承Thread类。这限制了程序的灵活性。

2. 实现Runnable接口

实现Runnable接口是Java中创建线程的另一个常用方法。在这种方式下,我们创建一个实现了Runnable接口的类,并实现其run()方法。随后,在主线程中,我们可以实例化这个Runnable对象,并将其作为参数传递给Thread类的构造函数,然后调用start()方法来启动线程。

示例代码:

class MyRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + " - Count: " + i);try {Thread.sleep(500); // 模拟线程执行过程中休眠} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();Thread thread1 = new Thread(myRunnable);Thread thread2 = new Thread(myRunnable);thread1.start(); // 启动线程1thread2.start(); // 启动线程2}
}

优点和缺点

优点

  • 允许多个线程共享同一Runnable实例。这样不同的线程可以共享相同的数据,这在某些情况下是非常有用的。
  • 解决了Java单继承的问题,增强了代码的灵活性。

缺点

  • 需要多写一些代码以创建线程实例,不过这对于大多数开发者而言并不会造成太大麻烦。

3. Callable与Future接口

除了Runnable接口外,还有一个更为强大的接口是Callable。与Runnable不同,Callable可以返回一个结果并且可以抛出异常。要使用Callable,我们需将其与ExecutorService结合使用。

示例代码:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;class MyCallable implements Callable<String> {@Overridepublic String call() throws Exception {for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + " - Count: " + i);Thread.sleep(500); // 模拟线程执行过程中休眠}return "Task Completed!";}public static void main(String[] args) {ExecutorService executor = Executors.newFixedThreadPool(2);Future<String> futureResult = executor.submit(new MyCallable());try {System.out.println("Result from Callable: " + futureResult.get()); // 获取可返回的结果} catch (InterruptedException | ExecutionException e) {e.printStackTrace();} finally {executor.shutdown(); // 关闭线程池}}
}

优点和缺点

优点

  • 支持返回值和异常处理,适合需要计算结果的场景。
  • 通过ExecutorService可以更好地管理线程池,避免线程创建和销毁的开销。

缺点

  • 相较于用Runnable创建线程,Callable的用法略显复杂。

线程的创建方式有多种,多种方式各有优劣。在具体情况中,我们需要根据实际要求来选择合适的线程创建方式。对于简单任务,可以使用Thread类或Runnable接口;而在需要共享数据或获取返回值的复杂场景下,使用Callable接口将是更优的选择。掌握这些基本的线程创建方式,为你深入学习Java的并发编程打下坚实的基础。

二、线程的生命周期

Java中的线程在其生命周期中会经历多个状态,这些状态反映了线程的运行情况及其与系统资源的互动。理解线程的生命周期对于编写高效、稳定的多线程程序至关重要。本节将详细阐述线程的各个状态及其状态之间的转换。

1. 线程状态概述

Java线程的生命周期主要包括以下几种状态:

  • **新建状态 (New)**:线程刚被创建但尚未启动。
  • **就绪状态 (Runnable)**:线程已经启动,等待CPU调度执行。
  • **运行状态 (Running)**:线程获得CPU时间片并正在执行。
  • **阻塞状态 (Blocked)**:线程等待获取锁,无法继续执行。
  • **等待状态 (Waiting)**:线程在等待某个条件发生,无法继续执行。
  • **超时等待状态 (Timed Waiting)**:线程在等待但设置了时间限制,如果超时则会返回就绪状态。
  • **死亡状态 (Terminated)**:线程的执行结束,无论是正常结束还是由于异常终止。

2. 各状态详细解释

2.1 新建状态 (New)

定义:当线程对象被创建时,处于新建状态。此时,线程对象的生命周期刚刚开始,但尚未执行。示例

Thread myThread = new Thread(new MyRunnable());
// 此时 myThread 处于新建状态

2.2 就绪状态 (Runnable)

定义:调用start()方法后,线程进入就绪状态。在这个状态中,线程已准备好执行,但尚未获得CPU的调度。这意味着操作系统的线程调度器将在线程可运行时将其调度到运行状态。示例

myThread.start(); // 线程进入就绪状态

2.3 运行状态 (Running)

定义:当线程获得CPU的执行权时,它处于运行状态。在运行状态中,线程执行run()方法中的代码。

状态转换

  • 从就绪状态到运行状态:当线程获得CPU时间片时,状态转换为运行状态。
  • 线程的状态转变是由调度器控制的,无法在代码中直接控制。

2.4 阻塞状态 (Blocked)

定义:线程试图获取已被其他线程占用的锁时,将进入阻塞状态。在这一状态下,线程无法继续执行,等待其它线程释放锁。

状态转换:当一个线程持有锁,而另一个线程试图获取该锁时,第二个线程将被阻塞,状态转换为阻塞状态。示例

class MyLock {synchronized void lockedMethod() {// ... 需要同步的代码}
}MyLock lock = new MyLock();
Thread thread1 = new Thread(() -> lock.lockedMethod());
Thread thread2 = new Thread(() -> lock.lockedMethod());

在上述示例中,thread1在持有lock的锁时,thread2若尝试访问lockedMethod(),将进入阻塞状态。

2.5 等待状态 (Waiting)

定义:线程在某些条件下主动放弃资源并进入等待状态,典型情况下是调用Object.wait()Thread.join()LockSupport.park()等方法。当条件满足时,其他线程可以通知它重新变为就绪状态。

状态转换

  • 线程会进入等待状态,例如,调用wait()方法后。
  • 线程将等待其他线程的通知或者被中断。

示例

synchronized (lock) {lock.wait(); // 当前线程将进入等待状态
}

2.6 超时等待状态 (Timed Waiting)

定义:线程在等待某个条件的情况下也可以设置超时,例如调用Thread.sleep(millis)Object.wait(timeout)。超时等待状态与等待状态的不同在于,超时等待状态会在指定的时间后自动返回到就绪状态。

状态转换:超时等待状态可以在等待时间到达后自动转变为就绪状态。示例

synchronized (lock) {lock.wait(1000); // 在超时时间内等待,超时后变为就绪状态
}

2.7 死亡状态 (Terminated)

定义:线程的执行完成后,无论是正常结束还是由于异常终止,都会进入死亡状态。此时,该线程的资源会被回收,无法再次启动。

状态转换

  • 线程的run()方法正常执行完毕或因未处理的异常结束。

示例

public void run() {// 线程操作// 线程完成后,将进入死亡状态
}

3. 小结

Java线程的生命周期由多个状态组成,理解这些状态及其之间的转换对于开发高效的并发程序至关重要。线程可以在不同的状态之间自由切换,程序员需要根据需要合理设计线程的使用策略,包括在何时让线程进入等待、阻塞或运行状态,以便优化应用的性能和响应能力。在多线程编程中,能够灵活控制线程的各种状态是一项重要的技能。掌握线程生命周期的相关知识,可以帮助你更好地应对各种并发场景,写出高效的并发应用程序。

三、线程状态转换示意图

线程状态转换示意图为理解Java线程的生命周期提供了一个直观的视角。通过这个图,我们可以清晰地看到不同状态之间的转换关系,以及在某些条件下线程如何在这些状态之间切换。在这一部分中,我们不仅会展示状态图,还会详细解释每一条状态转换的含义和触发条件。

1. 线程状态图概览

下面是一个Java线程状态转换的示意图:

     +------------+|    New     |+------------+|| start()v+------------+        [Thread Scheduler]|  Runnable  | <----------------++------------+                  ||                        ||                        ||                [lock acquisition failed]|                        ||  [run() method calls] |v                        |+------------+                  ||   Running  |                  |+------------+                  ||                        ||                        ||                    [Thread Yield / Sleep]|                        |v                        |[Thread completes /       [Thread is blocked]Thread interrupt]              ||                        v|                    +------------+| <----- Often blocked ➔|  Blocked   |[lock is acquired]        +------------+|                        ||                        ||              [lock released] |+-------------------------------+|[wait() / join() / notify() / time out]|+------------+             +------------+|  Waiting   | ---------> | Timed Waiting |+------------+             +------------+|                           | [Notify / interrupted]          [Timeout expired]|                           |+---------------------------+|+------------+|  Terminated |+------------+

2. 各状态及转换的具体说明

2.1 新建状态 (New)

  • 描述:线程对象被创建,但尚未启动。
  • 如何激活:通过创建一个Thread对象来初始化,例如:Thread thread = new Thread(myRunnable);
  • 转换:调用thread.start()方法后,线程进入就绪状态。

2.2 就绪状态 (Runnable)

  • 描述:线程已启动,等待操作系统的调度。
  • 如何激活:调用start()方法。
  • 转换:线程会被调度器选择并进入运行状态;也可能由于系统资源不足而被延迟执行。

2.3 运行状态 (Running)

  • 描述:线程正在执行代码。
  • 转换触发
    • 线程调度:当线程获得CPU时间片时,它会进入运行状态。
    • 操作结束:调用Thread.sleep()、Thread.yield()等方法,或其他原因导致线程的运行结束时,会回到就绪状态。
    • 线程完成run()方法执行完成或者由于异常抛出,状态转换为死亡状态。

2.4 阻塞状态 (Blocked)

  • 描述:线程正在等待获取已经被其他线程占用的锁。
  • 如何激活:调用一个需要锁的方法(如synchronized修饰的方法),而该锁被其它线程持有。
  • 转换触发
    • 获得锁:当锁释放,当前线程获得锁后,状态将转换为就绪状态。

2.5 等待状态 (Waiting)

  • 描述:线程处于等待状态,等待其他线程完成某些操作后被唤醒。
  • 如何激活:调用Object.wait()Thread.join()LockSupport.park()
  • 转换触发
    • 唤醒:通过其他线程调用notify()notifyAll(),或者线程被中断时,状态将转为就绪状态。

2.6 超时等待状态 (Timed Waiting)

  • 描述:线程在等待某个条件的情况下设置了超时时间。
  • 如何激活:调用方法如Thread.sleep(millis)Object.wait(timeout)等。
  • 转换触发
    • 超时结束或唤醒:超时结束后,线程将返回就绪状态,或者在接收到通知后也会转为就绪状态。

2.7 死亡状态 (Terminated)

  • 描述:线程的执行已经结束,无法再被重新启动。
  • 如何激活:在run()方法正常完成时,或者抛出未捕获异常导致线程中止。
  • 不可转换:线程一旦进入死亡状态,无法再转换回其他状态。

3. 小结

通过理解线程的状态转换及其相关的转换条件,我们可以写出更有效且可靠的多线程程序。合理调度线程、处理锁和监控线程状态是性能优化的关键。上面的状态图为我们提供了一个清晰的视角,帮助开发者在多线程编程中作出更好的设计和决策。了解并掌握线程状态的转换,将使你能够有效管理并发行为,充分利用Java的多线程特性,提升应用程序的吞吐量和响应速度。

四、总结

Java线程是一个复杂但极其重要的主题。通过适当的线程创建和管理方法,我们可以实现高效的并发程序。掌握线程的生命周期以及状态转换有助于我们更好地理解Java的多线程编程。同时,这也是成为Java大师不可或缺的一部分。

第8天的学习到此结束,期待明天的内容!继续努力,成为真正的Java高手!

http://www.xdnf.cn/news/235927.html

相关文章:

  • 树状结构转换工具类
  • 沙箱逃逸-通过题解了解沙箱逃逸
  • Flow Matching 是什么?
  • 如何做表征对齐?
  • Kettle下载安装教程
  • C# 异步详解
  • 探索MySQL InnoDB:事务、日志与锁的奥秘
  • 从实列中学习linux shell5: 利用shell 脚本 检测硬盘空间容量,当使用量达到80%的时候 发送邮件
  • MCP 自定义python实现server服务,支持离线调用和远程接口访问形式
  • 【IP101】图像处理基础:从零开始学习颜色操作(RGB、灰度化、二值化、HSV变换)
  • Kaamel白皮书:OpenAI 在安全方向的实践
  • Vulkan 学习(16)---- 使用 VertexBuffer
  • Python魔法函数深度解析
  • 关于epoch、batch_size等参数含义,及optimizer.step()的含义及数学过程
  • pinia实现数据持久化插件pinia-plugin-persist-uni
  • 10、属性和数据处理---c++17
  • 突破SQL注入字符转义的实战指南:绕过技巧与防御策略
  • 《Ultralytics HUB:开启AI视觉新时代的密钥》
  • Stack--Queue 栈和队列
  • 前端基础之《Vue(13)—重要API》
  • Dify Agent节点的信息收集策略示例
  • 【效率提升】Vibe Coding时代如何正确使用输入法:自定义短语实现Prompt快捷输入
  • windows系统 压力测试技术
  • Github开通第三方平台OAuth登录及Java对接步骤
  • ES使用之查询方式
  • 空域伦理与AI自主边界的系统建构
  • 《冰雪传奇点卡版》:第二大陆介绍!
  • Java 手写jdbc访问数据库
  • 代理脚本——爬虫
  • 【MySQL】索引特性