前言:
在 Java 编程的广袤天地中,多线程犹如一颗璀璨而又复杂的明珠,散发着独特的魅力与挑战。它是实现高效并发处理、提升系统性能的关键利器,但同时也隐藏着诸多陷阱与微妙之处。从多线程的创建方式,到线程的优雅停止,再到线程间通信与同步机制的奥秘,每一个环节都如同精密齿轮,相互协作又相互制约,共同构建起 Java 多线程编程的精彩世界。在本次深入的探索之旅中,我们将逐一揭开这些神秘面纱,让您全面而透彻地理解 Java 多线程的精髓,无论是初涉多线程领域的新手,还是渴望进一步提升多线程编程技能的资深开发者,都能在这场知识盛宴中收获满满,为您在 Java 开发之路上披荆斩棘、构建强大而高效的应用程序奠定坚实的基础。
1.说说Java中实现多线程有几种方法
在Java中实现多线程主要可以通过以下几种方式:
- 继承Thread类:
- 创建一个新的类来继承java.lang.Thread类,并重写run()方法。该方法定义了新线程开始执行的操作。
- 创建该类的实例对象后,调用其start()方法来启动新的线程。
示例代码如下:
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running.");
}
}
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); // 启动线程
}
}
- 实现Runnable接口:
- 创建一个实现了java.lang.Runnable接口的类,并实现run()方法。
- 将这个类的实例作为参数传递给Thread类的构造器来创建线程对象。
- 调用线程对象的start()方法启动线程。
- 这种方式的好处是避免了由于Java单继承特性导致的问题,可以实现多继承(实际上是指实现多个接口)。
示例代码如下:
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread via Runnable is running.");
}
}
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动线程
}
}
- 使用Executor框架:
- Java 5引入了java.util.concurrent包,其中包含了Executor接口及其子接口ExecutorService。
- ExecutorService提供了管理一组线程池的方法,可以更灵活地管理和控制线程的生命周期。
- 使用Executors类中的静态方法如newFixedThreadPool()、newSingleThreadExecutor()等来创建不同的线程池。
示例代码如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5); // 创建固定大小的线程池
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread("" + i);
executor.execute(worker); // 执行任务
}
executor.shutdown(); // 关闭线程池
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
class WorkerThread implements Runnable {
private String command;
public WorkerThread(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start. Command = " + command);
processCommand();
System.out.println(Thread.currentThread().getName() + " End.");
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
-
实现Callable接口与FutureTask:
- Callable接口类似于Runnable,但是Callable的任务可以返回结果和抛出异常。
- 结合FutureTask类,可以将Callable任务的结果封装起来,并通过ExecutorService提交任务并获取结果。
示例代码如下:
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Integer> future = executor.submit(new Callable<Integer>() {
public Integer call() {
return 123;
}
});
System.out.println("future.get() = " + future.get()); // 输出结果
executor.shutdown();
}
}
2.如何停止一个正在运行的线程
在Java中,停止一个正在运行的线程并不是一件简单的事情,因为直接终止线程可能会导致资源泄露或数据不一致等问题。Java提供了一些机制来优雅地停止线程,但没有提供直接的“杀死”线程的方法(如Thread.stop()方法已经被废弃)。下面是一些推荐的做法:
- 使用标志位:
- 最常见也是最推荐的方式是在线程内部设置一个标志位,通过检查这个标志位来决定是否继续执行循环或其他操作。
- 在需要停止线程的地方,外部代码可以改变这个标志位的值,线程会定期检查这个标志位,并根据其值决定是否退出。
示例代码如下:
public class MyThread extends Thread {
private volatile boolean stop = false;
public void run() {
while (!stop) {
// 模拟一些工作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Working...");
}
System.out.println("Thread stopped.");
}
public void requestStop() {
stop = true;
}
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
// 让主线程等待一段时间后请求停止子线程
Thread.sleep(5000);
thread.requestStop();
}
}
- 中断线程:
- 使用Thread.interrupt()方法可以中断一个线程。当调用线程的interrupt()方法时,如果该线程正处于阻塞状态(例如在调用sleep()、wait()等方法时),则会抛出InterruptedException异常。
- 如果线程不在阻塞状态下被中断,则可以通过检查Thread.isInterrupted()方法来判断线程是否已被中断。
示例代码如下:
public class MyThread extends Thread {
public void run() {
while (!Thread.currentThread().isInterrupted()) {
// 模拟一些工作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 清除中断状态
Thread.currentThread().interrupt();
break; // 退出循环
}
System.out.println("Working...");
}
System.out.println("Thread interrupted and stopped.");
}
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
// 让主线程等待一段时间后中断子线程
Thread.sleep(5000);
thread.interrupt();
}
}
- 使用ExecutorService:
- 如果使用的是ExecutorService来管理线程,可以调用shutdown()方法来平滑关闭线程池,或者调用shutdownNow()方法来立即尝试停止所有活动任务。
- shutdown()方法不会立即停止所有活动任务,而是不允许接受新的任务,并等待所有已提交的任务完成。
- shutdownNow()方法会尝试停止所有活动任务,并返回尚未开始的任务列表。
总之,优雅地停止线程通常涉及到良好的编程习惯,比如使用标志位、处理中断以及正确管理资源释放等。直接“杀死”线程不是一种好的做法,因为它可能导致数据丢失或资源泄露。
3.notify()和notifyAll()有什么区别?
在Java中,notify() 和 notifyAll() 方法都是用于唤醒等待在某个对象监视器上的线程,但它们的行为和应用场景有所不同。这两个方法都属于 Object 类,因此每个对象都可以调用这些方法。
notify()
- 作用:唤醒一个正在等待该对象监视器的单个线程。
- 行为:从所有等待该对象监视器的线程中随机选择一个线程,并将其从等待队列中移出,使其有机会重新获得对象的锁并继续执行。
- 应用场景:当你希望只有一个线程继续执行时,通常是在生产者-消费者模式中只有一个消费者或生产者需要被唤醒时使用。
notifyAll()
- 作用:唤醒所有正在等待该对象监视器的线程。
- 行为:将所有等待该对象监视器的线程从等待队列中移出,使它们都有机会重新竞争对象的锁。最终只有一个线程能够获得锁并继续执行,其他线程会重新进入等待状态。
- 应用场景:当你希望唤醒所有等待的线程,让它们都有机会重新竞争锁并继续执行时使用。这在某些复杂的同步场景中非常有用,例如在条件变量变化时需要通知多个等待的线程。
示例代码
使用 notify()
public class ProducerConsumerExample {
private int buffer = 0;
private final Object lock = new Object();
public void produce() {
synchronized (lock) {
while (buffer != 0) {
try {
System.out.println("Buffer full, producer waits.");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
buffer++;
System.out.println("Produced 1 item, buffer now has " + buffer);
lock.notify(); // 唤醒一个等待的消费者
}
}
public void consume() {
synchronized (lock) {
while (buffer == 0) {
try {
System.out.println("Buffer empty, consumer waits.");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
buffer--;
System.out.println("Consumed 1 item, buffer now has " + buffer);
lock.notify(); // 唤醒一个等待的生产者
}
}
}
使用 notifyAll()
public class MultiProducerConsumerExample {
private int buffer = 0;
private final Object lock = new Object();
public void produce() {
synchronized (lock) {
while (buffer != 0) {
try {
System.out.println("Buffer full, producer waits.");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
buffer++;
System.out.println("Produced 1 item, buffer now has " + buffer);
lock.notifyAll(); // 唤醒所有等待的消费者
}
}
public void consume() {
synchronized (lock) {
while (buffer == 0) {
try {
System.out.println("Buffer empty, consumer waits.");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
buffer--;
System.out.println("Consumed 1 item, buffer now has " + buffer);
lock.notifyAll(); // 唤醒所有等待的生产者
}
}
}
总结
- notify():只唤醒一个等待的线程,适用于只需要唤醒一个线程的情况。
- notifyAll():唤醒所有等待的线程,适用于需要所有等待的线程都有机会重新竞争锁的情况。
选择合适的唤醒方法取决于你的具体需求和场景。如果你不确定应该使用哪个方法,通常建议使用 notifyAll(),以确保所有等待的线程都能有机会继续执行。
4.sleep()和wait() 有什么区别?
在Java中,sleep() 和 wait() 都是用于线程暂停执行的方法,但它们之间存在一些重要的区别。这些区别主要涉及它们的工作原理、使用场景以及对锁的影响。
Thread.sleep(long millis)
- 作用:让当前正在执行的线程暂停指定的时间(毫秒),然后恢复执行。
- 影响锁:sleep() 不会释放任何锁。也就是说,即使线程在调用 sleep() 方法时持有某个对象的锁,它也不会释放这个锁。
- 异常:sleep() 可能会抛出 InterruptedException 异常,如果另一个线程中断了当前线程。
- 使用场景:通常用于让线程暂停一段时间,以便其他线程有机会执行,或者用于模拟延迟。
Object.wait(long millis)
- 作用:让当前正在执行的线程等待,直到其他线程调用同一个对象的 notify() 或 notifyAll() 方法,或者指定的时间(毫秒)过去。
- 影响锁:wait() 必须在同步块(synchronized block)或同步方法(synchronized method)中调用,且会释放当前对象的锁。当线程从 wait() 返回时,它必须重新获得该对象的锁。
- 异常:wait() 也可能抛出 InterruptedException 异常,如果另一个线程中断了当前线程。
- 使用场景:通常用于线程间的协调,例如在生产者-消费者模式中,生产者线程会在缓冲区满时调用 wait(),等待消费者线程消费数据后再继续生产。
主要区别总结
- 锁的影响:
- sleep() 不会释放任何锁。
- wait() 会释放当前对象的锁。
- 调用位置:
- sleep() 可以在任何地方调用。
- wait() 必须在同步块或同步方法中调用。
- 唤醒方式:
- sleep() 是定时的,时间到了自然醒来。
- wait() 需要通过 notify() 或 notifyAll() 唤醒,或者等待指定时间过去。
- 使用场景:
- sleep() 通常用于简单的延时操作。
- wait() 通常用于线程间的协调和通信。
示例代码
使用 Thread.sleep()
public class SleepExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
System.out.println("Thread starts sleeping.");
Thread.sleep(2000); // 暂停2秒
System.out.println("Thread wakes up after 2 seconds.");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
使用 Object.wait()
public class WaitNotifyExample {
private final Object lock = new Object();
public void waitMethod() {
synchronized (lock) {
try {
System.out.println("Thread starts waiting.");
lock.wait(2000); // 等待2秒或被唤醒
System.out.println("Thread wakes up after 2 seconds or being notified.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void notifyMethod() {
synchronized (lock) {
System.out.println("Notifying waiting thread.");
lock.notify(); // 唤醒一个等待的线程
}
}
public static void main(String[] args) {
WaitNotifyExample example = new WaitNotifyExample();
Thread waiter = new Thread(() -> example.waitMethod());
Thread notifier = new Thread(() -> {
try {
Thread.sleep(1000); // 等待1秒后唤醒
example.notifyMethod();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
waiter.start();
notifier.start();
}
}
5.volatile 是什么?可以保证有序性吗?
在Java中,volatile 是一个关键字,用于修饰变量,确保变量的可见性和禁止指令重排序。下面是关于 volatile 的详细解释:
1. 可见性
- 可见性:当一个线程修改了 volatile 变量的值时,其他线程能够立即看到这个修改后的值。这是因为在读取 volatile 变量时,总是从主内存中读取,而不是从线程的工作内存中读取;而在写入 volatile 变量时,总是写入到主内存中,而不是仅写入到线程的工作内存中。
示例:
public class VolatileExample {
private volatile boolean flag = false;
public void writer() {
flag = true; // 写操作
}
public void reader() {
while (!flag) { // 读操作
// 等待 flag 变为 true
}
System.out.println("Flag is true!");
}
}
2. 禁止指令重排序
- 禁止指令重排序:volatile 变量的读写操作具有一定的内存屏障(Memory Barrier)效果,可以防止编译器和处理器进行某些类型的指令重排序。这意味着在写入 volatile 变量之前发生的操作不会被重排序到写入之后,同样,在读取 volatile 变量之后发生的操作不会被重排序到读取之前。
public class VolatileOrderingExample {
private volatile int a = 0;
private int b = 0;
public void writer() {
b = 1; // 非 volatile 变量
a = 1; // volatile 变量
}
public void reader() {
if (a == 1) { // 读取 volatile 变量
// 由于 a 是 volatile 的,这里保证 b 已经被赋值为 1
System.out.println("b = " + b);
}
}
}
3. 是否保证有序性
- 有序性:虽然 volatile 可以防止某些特定的指令重排序,但它并不能完全保证所有的操作都具有严格的顺序性。特别是对于复杂的多线程同步问题,volatile 可能不足以确保所有操作的有序性。
示例:
public class VolatileOrderingLimitation {
private volatile int a = 0;
private int b = 0;
public void writer() {
a = 1; // volatile 变量
b = 1; // 非 volatile 变量
}
public void reader() {
if (a == 1) { // 读取 volatile 变量
// 这里不能保证 b 已经被赋值为 1
System.out.println("b = " + b);
}
}
}
总结
- 可见性:volatile 确保了变量的可见性,即一个线程对 volatile 变量的修改对其他线程是立即可见的。
- 禁止指令重排序:volatile 可以防止某些特定的指令重排序,确保在写入 volatile 变量之前的操作不会被重排序到写入之后,读取 volatile 变量之后的操作不会被重排序到读取之前。
- 有序性:虽然 volatile 提供了一定程度的有序性保障,但它并不能完全保证所有操作的严格顺序性。对于复杂的多线程同步问题,通常需要使用更强大的同步机制,如 synchronized 关键字或 java.util.concurrent 包中的工具类。