java多线程第三十八章(第十章Java多线程编程)

第十章:java多线程编程

在本章中,我们将介绍Java中的多线程编程。多线程是现代计算机应用程序中常见的重要概念之一,它可以提高程序的性能和响应能力。在本章中,我们将深入探讨Java多线程编程的概念、原理和实践。我们将学习如何创建和控制线程,以及如何处理线程间的同步和通信。

10.1 Java的线程概念和生命周期

在开始介绍Java多线程编程之前,让我们先来了解一些基本概念。线程是程序执行的最小单位,它代表了一个独立的执行路径。在Java中,线程由Thread类表示。每个Java程序都至少有一个主线程,也可以创建多个额外的线程来执行并行任务。

在本节中,我们将学习线程的生命周期,它包括以下几个状态:

  • 新建状态(New):线程被创建但尚未启动。
  • 就绪状态(Runnable):线程可以开始执行,但尚未获得CPU时间片。
  • 运行状态(Running):线程正在执行任务。
  • 阻塞状态(Blocked):线程暂时停止执行,等待某个条件满足。
  • 终止状态(Terminated):线程执行完成或因异常而终止。
10.2 Java的线程创建方式

在Java中,有两种常见的方式来创建线程:继承Thread类和实现Runnable接口。

10.2.1 继承Thread类

继承Thread类是创建线程的一种方式。为了创建一个新的线程,我们可以定义一个类,并继承自Thread类。然后重写Thread类的run()方法,在该方法中定义线程要执行的任务。

下面是一个简单的示例代码:

public class MyThread extends Thread { @Override public void run() { // 线程要执行的任务 System.out.println("Hello from MyThread!"); } }

创建并启动线程的代码如下所示:

public class Main { public static void main(String[] args) { // 创建线程实例 MyThread thread = new MyThread(); // 启动线程 thread.start(); } }

在上面的代码中,我们创建了一个名为MyThread的线程类,并重写了其run()方法。然后,在主线程中创建MyThread的实例,并调用start()方法来启动线程。启动线程后,线程将会执行其run()方法中定义的任务。

10.2.2 实现Runnable接口

另一种创建线程的方式是实现Runnable接口。与继承Thread类不同,实现Runnable接口将线程的任务与线程类本身分离开来,更符合面向对象的设计原则。

下面是一个使用实现Runnable接口创建线程的示例代码:

public class MyRunnable implements Runnable { @Override public void run() { // 线程要执行的任务 System.out.println("Hello from MyRunnable!"); } }

创建并启动线程的代码如下所示:

public class Main { public static void main(String[] args) { // 创建线程实例 MyRunnable runnable = new MyRunnable(); // 创建线程对象 Thread thread = new Thread(runnable); // 启动线程 thread.start(); } }

在上面的代码中,我们创建了一个名为MyRunnable的类,并实现了Runnable接口。然后,在主线程中创建了一个Thread对象,并将MyRunnable的实例作为参数传递给Thread的构造函数。最后,调用线程的start()方法来启动线程。

无论是继承Thread类还是实现Runnable接口,最终都会创建一个新的线程,并在该线程中执行定义的任务。

10.3 Java的线程控制方法

Java提供了一些方法来控制线程的执行,包括start()、run()、sleep()、join()和yield()等。

10.3.1 start()方法

start()方法用于启动线程,并让线程进入就绪状态,等待CPU调度执行。当调用start()方法时,系统会为线程分配必要的资源,并在稍后的时间点自动调用线程的run()方法。

10.3.2 run()方法

run()方法是线程的主体,包含线程要执行的任务代码。当线程启动后,系统会自动调用线程的run()方法,并在该方法中执行定义的任务。

需要注意的是,我们不应直接调用线程的run()方法来启动线程,而是应该通过调用start()方法来启动线程。

10.3.3 sleep()方法

sleep()方法使线程暂停执行一段时间,让出CPU时间片给其他线程执行。它接受一个以毫秒为单位的参数,表示线程暂停的时间。

下面是一个使用sleep()方法的示例代码:

public class Main { public static void main(String[] args) { System.out.println("Thread 1 started."); try { // 线程1暂停执行500毫秒 Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread 1 finished."); } }

在上面的代码中,线程1在执行到Thread.sleep(500)时,暂停执行500毫秒,然后继续执行。

10.3.4 join()方法

join()方法用于等待一个线程的完成。当一个线程调用其他线程的join()方法时,它将会被阻塞,直到被调用的线程执行完成后才继续执行。

下面是一个使用join()方法的示例代码:

public class Main { public static void main(String[] args) { Thread thread1 = new Thread(() -> { System.out.println("Thread 1 started."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread 1 finished."); }); Thread thread2 = new Thread(() -> { System.out.println("Thread 2 started."); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread 2 finished."); }); // 启动线程1 thread1.start(); // 等待线程1执行完成后再启动线程2 try { thread1.join(); } catch (InterruptedException e) { e.printStackTrace(); } // 启动线程2 thread2.start(); } }

在上面的代码中,我们创建了两个线程:线程1和线程2。在主线程中,首先启动线程1,然后调用线程1的join()方法,等待线程1执行完成后再启动线程2。这样可以确保线程2在线程1执行完成后才开始执行。

10.3.5 yield()方法

yield()方法是一种线程让步的机制,它使当前线程暂停执行,让出CPU时间片给其他具有相同优先级的线程。通过调用yield()方法,可以实现线程之间的合理调度,提高系统的整体性能。

需要注意的是,yield()方法只是提供了一种提示,不保证当前线程会立即让出CPU时间片,而是依赖于系统的具体实现。

10.4 Java的线程同步机制

在并发编程中,多个线程可能同时访问共享的资源,如果没有适当的同步机制,可能会导致数据不一致或并发错误。Java提供了一些机制来实现线程的同步,包括synchronized关键字、wait/notify机制和锁(Lock)等。

10.4.1 synchronized关键字

synchronized关键字用于保护共享资源,确保在同一时间只有一个线程可以访问被保护的代码块或方法。

同步代码块

我们可以使用synchronized关键字来创建同步代码块,如下所示:

public class Counter { private int count = 0; public void increment() { synchronized (this) { count ; } } }

在上面的代码中,使用synchronized关键字将代码块count 标记为同步代码块,其中的参数this表示当前对象,也就是锁定的资源。这样,每次只有一个线程可以进入该代码块,确保了对count变量的安全访问。

同步方法

我们也可以使用synchronized关键字来创建同步方法,如下所示:

public class Counter { private int count = 0; public synchronized void increment() { count ; } }

在上面的代码中,使用synchronized关键字修饰了方法increment(),这样整个方法体都被视为同步代码块,只有一个线程可以同时执行该方法。

无论是同步代码块还是同步方法,都是通过获取对象的锁来实现线程的同步。当一个线程进入同步代码块或同步方法时,它会尝试获取锁。如果锁已经被其他线程占用,那么当前线程将被阻塞,直到获取到锁才能继续执行。

10.4.2 wait/notify机制

wait/notify机制是基于对象的等待/通知机制,用于实现线程间的协作和通信。它允许一个线程暂停执行,直到满足某个条件才继续执行,同时允许其他线程发出通知,以唤醒等待的线程。

wait()方法

wait()方法使当前线程进入等待状态,释放对象的锁,并等待其他线程调用相同对象的notify()或notifyAll()方法来唤醒它。

wait()方法可以有两种形式:

  • wait(): 使当前线程无限期地等待,直到其他线程唤醒它。
  • wait(long timeout): 使当前线程等待一定的时间,如果在指定的时间内没有被唤醒,将自动苏醒。

wait()方法必须在同步代码块或同步方法中调用,否则会抛出IllegalMonitorStateException异常。

notify()和notifyAll()方法

notify()方法用于唤醒正在等待对象锁的某个线程,而notifyAll()方法用于唤醒所有正在等待对象锁的线程。

notify()方法只会唤醒等待队列中的一个线程,而notifyAll()方法会唤醒所有等待的线程。被唤醒的线程将重新进入对象锁的竞争。

notify()和notifyAll()方法也必须在同步代码块或同步方法中调用,否则同样会抛出IllegalMonitorStateException异常。

下面是一个使用wait/notify机制的示例代码:

public class Message { private String content; private boolean empty = true; public synchronized String read() { while (empty) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } empty = true; notifyAll(); return content; } public synchronized void write(String message) { while (!empty) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } empty = false; this.content = message; notifyAll(); } }

在上面的示例代码中,我们创建了一个Message类,其中包含一个content字段表示消息内容,和一个empty字段表示消息是否为空。read()方法用于读取消息,write()方法用于写入消息。

在read()方法中,使用while循环判断消息是否为空,如果为空,则调用wait()方法进入等待状态,释放对象锁。等待期间,其他线程可以调用write()方法写入消息并唤醒等待的线程。当被唤醒后,再次检查消息是否为空,如果不为空,则返回消息内容。

在write()方法中,同样使用while循环判断消息是否为空,如果不为空,则调用wait()方法进入等待状态,释放对象锁。等待期间,其他线程可以调用read()方法读取消息并唤醒等待的线程。当被唤醒后,再次检查消息是否为空,如果为空,则写入消息内容。

需要注意的是,在使用wait()方法时,要将其放在循环中,并且检查等待条件。这是为了防止虚假唤醒(spurious wake-up),即线程在没有收到通知的情况下被唤醒。通过将wait()方法放在循环中,可以在唤醒后再次检查等待条件,确保只在满足条件时才继续执行。

10.4.3 Lock和Condition

除了使用synchronized关键字和wait/notify机制外,Java还提供了Lock和Condition接口来实现线程的同步和协作。

lock接口提供了与synchronized关键字相似的功能,用于保护临界区代码。相比于synchronized关键字,Lock提供了更灵活的锁定机制,例如可重入锁、公平锁、读写锁等。

Condition接口则提供了比wait/notify更强大的线程等待和通知机制。一个Lock对象可以关联多个Condition对象,每个Condition对象可以控制线程的等待和唤醒。

使用Lock和Condition的示例代码如下:

import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Message { private String content; private boolean empty = true; private Lock lock = new ReentrantLock(); private Condition notEmpty = lock.newCondition(); private Condition notFull = lock.newCondition(); public void read() { lock.lock(); try { while (empty) { try { notEmpty.await(); } catch (InterruptedException e) { e.printStackTrace(); } } empty = true; notFull.signalAll(); System.out.println("Read: " content); } finally { lock.unlock(); } } public void write(String message) { lock.lock(); try { while (!empty) { try { notFull.await(); } catch (InterruptedException e) { e.printStackTrace(); } } empty = false; this.content = message; notEmpty.signalAll(); System.out.println("Write: " message); } finally { lock.unlock(); } } }

在上面的代码中,我们首先创建了一个Lock对象和两个Condition对象,分别用于控制非空和非满的条件。

在read()方法中,首先调用lock()方法获取锁,然后使用while循环判断消息是否为空。如果为空,调用await()方法进入等待状态,并释放锁。等待期间,其他线程可以调用write()方法写入消息并唤醒等待的线程。当被唤醒后,再次检查消息是否为空,如果不为空,则输出消息内容。

在write()方法中,同样首先调用lock()方法获取锁,然后使用while循环判断消息是否为空。如果不为空,调用await()方法进入等待状态,并释放锁。等待期间,其他线程可以调用read()方法读取消息并唤醒等待的线程。当被唤醒后,再次检查消息是否为空,如果为空,则写入消息内容。

最后,需要在适当的地方调用lock.unlock()方法释放锁,以确保资源的正确释放。

使用Lock和Condition相比于synchronized关键字和wait/notify机制更加灵活,可以更精确地控制线程的等待和唤醒,并提供更多的同步和并发控制选项。

以上是关于Java多线程编程的基本内容和常用技术。通过合理地使用线程同步和协作机制,可以编写出安全、高效的多线程程序。然而,在实际开发中,还有更多复杂的多线程场景和问题需要进一步研究和解决。深入学习和理解多线程编程的原理和技术,将有助于编写更健壮、高性能的并发应用。

希望本篇博客对您理解和掌握Java多线程编程有所帮助。如有任何问题或疑惑,请随时提问。谢谢!

java多线程第三十八章(第十章Java多线程编程)(1)

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页