Java 多线程核心要点全解析:从基础到实战的深度探索

190 阅读11分钟

前言:

在 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 包中的工具类。