java多线程第三十八章(第十章Java多线程编程)
在本章中,我们将介绍Java中的多线程编程。多线程是现代计算机应用程序中常见的重要概念之一,它可以提高程序的性能和响应能力。在本章中,我们将深入探讨Java多线程编程的概念、原理和实践。我们将学习如何创建和控制线程,以及如何处理线程间的同步和通信。
10.1 Java的线程概念和生命周期在开始介绍Java多线程编程之前,让我们先来了解一些基本概念。线程是程序执行的最小单位,它代表了一个独立的执行路径。在Java中,线程由Thread类表示。每个Java程序都至少有一个主线程,也可以创建多个额外的线程来执行并行任务。
在本节中,我们将学习线程的生命周期,它包括以下几个状态:
- 新建状态(New):线程被创建但尚未启动。
- 就绪状态(Runnable):线程可以开始执行,但尚未获得CPU时间片。
- 运行状态(Running):线程正在执行任务。
- 阻塞状态(Blocked):线程暂时停止执行,等待某个条件满足。
- 终止状态(Terminated):线程执行完成或因异常而终止。
在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多线程编程有所帮助。如有任何问题或疑惑,请随时提问。谢谢!
,
免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com