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高手!