Java 线程
上次修改时间:2024 年 7 月 13 日
在本文中,我们将介绍 Java 线程,并列出它们的优点和缺点。我们还将提到在使用线程时会出现哪些问题。我们将在几个示例中展示如何创建简单的线程。
Java 线程是程序中一个轻量级的执行单元。它允许程序表面上同时执行多个任务。Java 虚拟机允许应用程序同时运行多个执行线程。
每个 Java 程序至少有一个线程,称为主线程。当程序启动时,Java 虚拟机创建此线程,并调用 main 方法。
使用 Java 线程的优点是:
- 提高响应速度:程序可以在后台(单独的线程中)执行长时间运行的任务时,保持用户界面具有响应性。
- 高效的资源利用:线程共享主进程的内存,这使得它们比创建全新的进程更轻量。
Java 线程的常见用例包括:
- 在后台执行长时间运行的任务(例如,下载文件、网络通信)。
- 更新图形用户界面而不阻塞主线程。
- 在服务器应用程序中处理多个客户端请求。
由于线程并发运行,代码执行的顺序是不可预测的。 当线程读取和写入相同的变量时,可能会导致并发问题。为了避免这种情况,应尽可能减少线程之间共享的属性,并使用同步技术。
多线程问题
多线程程序本质上比单线程程序更复杂。这可能会导致难以识别和重现的错误。
常见的并发问题包括:
- 竞争条件:当多个线程在没有适当同步的情况下访问和修改共享数据时,可能会导致意外和不一致的结果。
- 死锁:当两个或多个线程都在等待彼此持有的资源时,就会发生死锁,导致它们无限期地冻结。
- 饥饿:当一个线程不断被具有更高优先级的其他线程抢占时,它可能永远没有机会运行,从而有效地被剥夺了资源。
同步
线程同步是控制多个线程对共享资源访问的能力。它确保一次只有一个线程可以访问代码的关键部分或共享资源。
同步技术包括:
- 同步代码块:使用同步代码块或方法来控制对共享数据的访问。一次只有一个线程可以进入一个同步代码块,从而确保数据一致性。
- 并发集合:在处理共享数据结构时,使用线程安全的集合,例如 ConcurrentHashMap 或 BlockingQueue,而不是传统的集合。
- 信号量和互斥锁:为了更精细的控制,可以考虑使用信号量或互斥锁来管理对有限资源的访问,例如文件句柄或网络连接。
请注意,通过同步对共享资源的访问来确保线程安全可能会引入开销。与单线程方法相比,这会减慢我们的程序。
创建线程
在 Java 中创建线程有两种主要方法:a) 扩展 Thread
类或 b) 实现 Runnable
接口。
class Worker implements Runnable { @Override public void run() { System.out.println("worker is running"); } } void main() { System.out.println("main thread started"); var myRunnable = new Worker(); var thread = new Thread(myRunnable); thread.start(); System.out.println("main thread ended"); }
在这里,我们创建一个实现 Runnable
接口的 Worker
类。Runnable
接口只有一个方法,run
,它定义了线程要执行的代码。线程通过 start
方法启动。
在第二个示例中,Worker
类扩展了 Thread
类。
class Worker extends Thread { @Override public void run() { System.out.println("worker is running"); } } void main() { System.out.println("main thread started"); var myRunnable = new Worker(); var thread = new Thread(myRunnable); thread.start(); System.out.println("main thread ended"); }
在该示例中,我们从 Thread
类扩展并覆盖 run
方法。run
方法定义了线程要执行的代码。
Thread.sleep
Thread.sleep(ms)
方法用于暂停当前线程的执行,暂停时间以毫秒为单位。
class Task implements Runnable { private int delay; private String name; public Task(String name, int delay) { this.name = name; this.delay = delay; } @Override public void run() { try { System.out.printf("starting task %s%n", this.name); Thread.sleep(delay); System.out.printf("finishing task %s%n", this.name); } catch (InterruptedException e) { e.printStackTrace(); } } } void main() { var task1 = new Task("Task 2000", 2000); var t1 = new Thread(task1); t1.start(); var task2 = new Task("Task 1000", 1000); var t2 = new Thread(task2); t2.start(); var task3 = new Task("Task 500", 500); var t3 = new Thread(task3); t3.start(); System.out.println("tasks launched"); }
实现 Task
接口的类会休眠指定的毫秒数。我们在主应用程序线程中启动三个线程。它们分别休眠 2 秒、1 秒和 500 毫秒。
$ java Main.java tasks launched starting task Task 1000 starting task Task 2000 starting task Task 500 finishing task Task 500 finishing task Task 1000 finishing task Task 2000
首先,主程序线程完成。这三个线程以不可预测的顺序启动。它们稍后根据其休眠值完成。
Thread join 方法
Thread
的 join
方法用于同步线程的执行。
该方法的使用场景
- 在主线程中继续执行之前,等待后台线程完成任务。
- 确保在主线程中使用之前,线程中的初始化任务已完成。
- 同步线程之间对共享资源的访问。
class Worker extends Thread { private int delay; private String msg; public Worker(int delay, String msg) { this.delay = delay; this.msg = msg; } public void run() { try { Thread.sleep(delay); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(msg); } } void main() { var w1 = new Worker(2000, "Hello there"); var w2 = new Worker(1000, "New mail received"); var w3 = new Worker(500, "Notes taken"); // start three threads w1.start(); w2.start(); w3.start(); // wait for threads to end try { // join is a blocker method which waits for a thread to complete. // the w1.join() causes the current (main) thread to pause execution // until w1's thread terminates. w1.join(); w2.join(); w3.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("finished tasks"); }
在该程序中,我们启动了三个额外的线程。使用 join
方法,我们暂停主线程的执行,暂停时间为这三个线程的持续时间。
$ java Main.java Notes taken New mail received Hello there finished tasks
在三个线程结束其任务后,才会显示 finished tasks。
SwingWorker
SwingWorker 专门用于在与 Swing 事件分派线程 (EDT) 分开的单独线程中运行长时间运行的任务。它简化了在后台执行长时间运行的任务,同时保持 EDT 的响应性。
长时间运行的任务被放置在 doInBackground
方法中。此方法在单独的线程上运行,从而使 EDT 可以处理 UI 更新。
package com.zetcode; import javax.swing.GroupLayout; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.SwingWorker; import java.awt.EventQueue; class MyWorker extends SwingWorker<Void, Void> { @Override protected Void doInBackground() throws Exception { // Simulate a time-consuming task Thread.sleep(3000); return null; } @Override protected void done() { System.out.println("task done"); } } public class ButtonTaskEx extends JFrame { public ButtonTaskEx() { initUI(); } private void initUI() { var taskButton = new JButton("Task"); taskButton.addActionListener((event) -> { var worker = new MyWorker(); worker.execute(); }); // taskButton.addActionListener((event) -> { // try { // Thread.sleep(3000); // System.out.println("task done"); // } catch (InterruptedException e) { // throw new RuntimeException(e); // } // }); createLayout(taskButton); setTitle("Task button"); setSize(500, 450); setLocationRelativeTo(null); setDefaultCloseOperation(EXIT_ON_CLOSE); } private void createLayout(JComponent... arg) { var pane = getContentPane(); var gl = new GroupLayout(pane); pane.setLayout(gl); gl.setAutoCreateContainerGaps(true); gl.setHorizontalGroup(gl.createSequentialGroup() .addComponent(arg[0]) ); gl.setVerticalGroup(gl.createSequentialGroup() .addComponent(arg[0]) ); } public static void main(String[] args) { EventQueue.invokeLater(() -> { var ex = new ButtonTaskEx(); ex.setVisible(true); }); } }
在该程序中,我们有一个按钮,可以启动一个 3 秒的后台任务。如果我们将任务放置在 SwingWorker
中,则应用程序将保持响应。否则,应用程序将在任务持续时间内冻结。
来源
在本文中,我们定义了 Java 线程,并提供了一些基本的代码示例。
作者
列出所有Java教程。