Java多线程编程:实现并发处理的高效利器
作者:Stevedash
发表于:2023年8月13日 20点45分
来源:Java 多线程编程 | 菜鸟教程 (runoob.com)
在计算机领域,多线程编程是一项重要的技术,可以使程序同时执行多个任务,充分利用计算资源,提高系统的性能和响应能力。Java作为一门广泛应用的编程语言,提供了丰富的多线程编程支持,使得开发人员可以轻松实现并发处理。本篇博客将向您介绍Java多线程编程的基本概念、创建线程的方式、线程同步和线程通信等内容,同时还会涉及到线程的生命周期、线程的优先级和线程的常用方法。
多线程编程的优势
多线程编程在提高程序性能、资源利用率和响应速度方面具有明显的优势。通过充分利用多核处理器,可以在同一时刻处理多个任务,提高系统的整体吞吐量。此外,多线程还可以用于实现一些需要并发执行的场景,如并发请求处理、数据采集、实时计算等。
创建线程的方式
Java多线程编程有三种常见的方式来创建线程:
-
继承Thread类: 创建一个类继承
Thread
类,并重写run()
方法来定义线程要执行的任务。 -
实现Runnable接口: 创建一个实现
Runnable
接口的类,并实现run()
方法,然后通过Thread
类的构造方法创建线程对象。 -
实现Callable接口: 创建一个实现
Callable
接口的类,并实现call()
方法,可以获取线程执行后的返回值。
每种方式都有其特点,选择合适的方式取决于具体需求。使用继承Thread类的方式编写简单,但由于Java不支持多重继承,可能限制了其他类的继承。而实现Runnable或Callable接口的方式可以更灵活地管理线程。
线程的生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
下图显示了一个线程完整的生命周期。
线程的生命周期可以分为以下几个阶段:
- 新建状态(New): 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
- 就绪状态(Runnable): 当调用线程的
start()
方法后,线程进入就绪状态,就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度,等待获取CPU时间片执行。 - 运行状态(Running): 如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
- 阻塞状态(Blocked): **如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。**在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
- 死亡状态:(Terminated): 一个运行状态的线程完成任务或者其他终止条件发生时,又或者当线程的
run()
方法执行完毕,线程进入终止状态。
线程的优先级
每个线程都有一个优先级,用于指示线程在竞争CPU时间片时的优先顺序。线程的优先级分为1到10,其中1为最低优先级,10为最高优先级。可以使用setPriority()
方法设置线程的优先级,但并不能保证优先级高的线程一定会先执行。**默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。**具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
线程的常用方法
Java提供了一些常用的线程方法,用于管理线程的执行和状态:
-
start():
启动线程,使其进入就绪状态。 -
join():
等待线程执行完毕。 -
sleep():
使线程休眠一段时间。 -
yield():
让出CPU时间片,让其他线程执行。 -
isAlive():
判断线程是否还存活。
示例代码
以下是一个简单的Java程序,演示了如何创建并启动多个线程,并使用线程的优先级和常用方法:
public class MultiThreadDemo {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable("Thread 1"));
Thread thread2 = new Thread(new MyRunnable("Thread 2"));
thread1.setPriority(Thread.MAX_PRIORITY);
thread2.setPriority(Thread.MIN_PRIORITY);
thread1.start();
thread2.start();
try {
thread1.join(); // 等待thread1执行完毕
thread2.join(); // 等待thread2执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程结束");
}
//实现Runnable接口创建线程
static class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name) {
this.name = name;
}
//通过Callable接口和Future创建线程
static class MyCallable implements Callable<Integer> {
private String name;
public MyCallable(String name) {
this.name = name;
}
@Override
public Integer call() throws Exception{
//...
return Integer;
}
//继承Thread类创建线程 三选一
static class MyThread extends Thread{
private String name;
public MyCallable(String name) {
this.name = name;
}
}
//必须要重写run方法
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(name + ": " + i);
try {
Thread.sleep(1000); // 休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
下面是Thread 方法
下表列出了Thread类的一些重要方法:
序号 | 方法描述 |
---|---|
1 | public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
2 | public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
3 | public final void setName(String name) 改变线程名称,使之与参数 name 相同。 |
4 | public final void setPriority(int priority) 更改线程的优先级。 |
5 | public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。 |
6 | public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。 |
7 | public void interrupt() 中断线程。 |
8 | public final boolean isAlive() 测试线程是否处于活动状态。 |
上述方法是被 Thread 对象调用的,下面表格的方法是 Thread 类的静态方法。
序号 | 方法描述 |
---|---|
1 | public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。 |
2 | public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
3 | public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
4 | public static Thread currentThread() 返回对当前正在执行的线程对象的引用。 |
5 | public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。 |
重点了解一下SetDeamon()标记成守护线程或者用户线程
基本概念:
守护线程(Daemon Thread)是一种在后台运行的线程,它的存在不会阻止程序的终止。与之相对,用户线程(User Thread)是主要用于执行应用程序逻辑的线程,“如果所有的用户线程都执行完毕,JVM 就会退出,而不管守护线程是否还在运行。”
在 Java 中,我们可以通过 setDaemon(true)
方法将一个线程设置为守护线程。**默认情况下,线程都是用户线程,不会影响程序的终止。**如果想要使用守护线程,可以在创建线程后调用 setDaemon(true)
来设置它为守护线程。
守护线程的主要作用有以下几点:
- 后台任务执行: 守护线程通常用于执行一些不需要持续运行的后台任务,如垃圾回收(Garbage Collection)、内存管理等。通过将这些任务放在守护线程中,可以在主要任务执行完毕后,自动进行清理等工作,提高系统的整体性能和资源利用率。
- 资源管理: 守护线程可以用于监控和管理一些资源,如数据库连接池、网络连接等。当用户线程都结束时,守护线程可以负责关闭这些资源,防止资源泄露和浪费。
- 周期性任务: 守护线程还可以用于执行周期性的任务,如定时器任务。这些任务可以在“后台周期性地执行,而不影响主要业务逻辑的进行。”
PS(这很重要基本上就是守护线程的重点):守护线程在程序终止时会突然中断,因此不应该在它们的代码中进行需要确保完整性的操作。另外,守护线程不应该依赖于特定的执行顺序,因为它们的执行可能会受到系统资源调度的影响。
三种创建线程的方式对比
-
继承
Thread
类:- 创建线程的方式之一是继承
Thread
类,并重写run
方法来定义线程的任务逻辑。 - 优点:简单,不需要额外的类来实现接口。
- 缺点:由于 Java 不支持多继承,因此如果已经继承了其他类,则无法再使用这种方式创建线程。
- 示例代码:
class MyThread extends Thread { public void run() { // 线程执行的任务逻辑 } } public class ThreadExample { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); } }
- 创建线程的方式之一是继承
-
实现
Runnable
接口:- 另一种创建线程的方式是实现
Runnable
接口,并将实现类的实例传递给Thread
类的构造函数。 - 优点:可以避免继承单一父类的限制,同时更灵活,可以实现多个接口。
- 缺点:相对于继承
Thread
类,需要额外的类来实现接口。 - 示例代码:
class MyRunnable implements Runnable { public void run() { // 线程执行的任务逻辑 } } public class RunnableExample { public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.start(); } }
- 另一种创建线程的方式是实现
-
使用
Callable
和Future
:- 使用
Callable
接口可以创建一个带有返回结果的任务。Future
接口可以用于获取任务的执行结果。 - 优点:可以获取任务的返回结果,能够捕获任务抛出的异常。
- 示例代码:
import java.util.concurrent.*; class MyCallable implements Callable<Integer> { public Integer call() throws Exception { // 线程执行的任务逻辑 return 42; } } public class CallableExample { public static void main(String[] args) { ExecutorService executor = Executors.newSingleThreadExecutor(); Future<Integer> future = executor.submit(new MyCallable()); try { int result = future.get(); System.out.println("任务执行结果:" + result); } catch (Exception e) { e.printStackTrace(); } finally { executor.shutdown(); } } }
- 使用
三种创建线程方式对比小结:
- 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
- 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。
- 继承
Thread
类和实现Runnable
接口是最基本的方式,而使用Callable
和Future
可以获得更多的控制和返回结果的能力。
线程的几个主要概念
在多线程编程时,你需要了解以下几个概念:
- 线程同步
- 线程间通信
- 线程死锁
- 线程控制:挂起、停止和恢复
注意事项和进阶功能
-
多线程编程需要注意线程安全性和资源竞争问题,合理使用同步机制来保证数据的一致性。
-
Java提供了线程池、并发集合等工具类来简化多线程编程,提高效率和性能。
-
在处理复杂场景时,可以使用
Executor
框架、Future
接口等实现更高级的线程管理和任务调度。
总结
Java多线程编程是实现并发处理的有效手段,可以提高程序性能和响应能力。通过学习线程创建方式、线程同步、线程通信、线程的生命周期、线程的优先级和线程的常用方法,我们可以在应用程序中合理使用多线程来实现并发任务。
作者:Stevedash
发表于:2023年8月13日 20点45分
来源:Java 多线程编程 | 菜鸟教程 (runoob.com)
注:本文内容基于个人学习理解,如有错误或疏漏,欢迎指正。感谢阅读!如果觉得有帮助,请点赞和分享。