欢迎关注我的公众号:观知小阁。包含各种类的文章,内容更丰富,更新及时且不迷路。

了解与理解多线程协作的原理与技巧

在Java多线程编程中,仅仅理解如何创建和启动线程是远远不够的。真正的挑战在于如何让多个线程高效协作,共同完成复杂任务。

今天,我们来了解Java中线程协作的关键机制,包括wait/notify、挂起和继续执行、结束和谦让等操作。

1. 等待(wait)和通知(notify)机制

在多线程环境中,线程间的协作至关重要。JDK提供了wait()和notify()方法来实现这种协作,这两个方法定义在Object类中,意味着任何Java对象都可以作为线程协作的工具。

1.1 基本工作原理

当线程调用一个对象的wait()方法后,它会进入等待状态,直到其他线程调用该对象的notify()或notifyAll()方法。这个过程涉及两个关键队列:等待队列和锁竞争队列。

// 简单的wait/notify示例
public class WaitNotifyExample {
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread waitingThread = new Thread(() -> {
            synchronized(lock) {
                try {
                    System.out.println("线程进入等待状态...");
                    lock.wait();
                    System.out.println("线程被唤醒并继续执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        waitingThread.start();

        Thread.sleep(1000); // 主线程休眠1秒

        synchronized(lock) {
            lock.notify(); // 唤醒等待的线程
        }
    }
}

1.2 wait()和notify()的执行条件

关键点在于:这两个方法都必须在同步环境(synchronized方法或代码块)中调用。线程在调用wait()方法前必须获得对象锁,而wait()方法执行后会释放这个锁,使其他线程有机会获得锁并执行通知操作。

与Thread.sleep()不同,wait()方法会释放目标对象的锁,而sleep()则不会释放任何锁资源。

1.3 生产者 - 消费者模式实战

wait/notify机制最典型的应用就是 生产者 - 消费者模式:

public class ProducerConsumerExample {
    private static final List<String> list = new ArrayList<>();
    private static final Object lock = new Object();

    // 生产者线程
    static class Producer extends Thread {
        public void run() {
            while (true) {
                synchronized(lock) {
                    // 如果列表中有数据,就等待消费者消费
                    if (list.size() > 0) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    // 生产数据
                    list.add("数据:" + System.currentTimeMillis());
                    System.out.println("生产数据: " + list.get(0));
                    lock.notify(); // 通知消费者
                }
            }
        }
    }

    // 消费者线程
    static class Consumer extends Thread {
        public void run() {
            while (true) {
                synchronized(lock) {
                    // 如果列表中没有数据,就等待生产者生产
                    if (list.size() == 0) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    // 消费数据
                    System.out.println("消费数据: " + list.remove(0));
                    lock.notify(); // 通知生产者
                }
            }
        }
    }
}

在这个模式中,当条件不满足时,线程会主动等待,并在条件满足时被通知继续执行,从而高效地协调生产者和消费者的工作节奏。

2. 挂起(suspend)和继续执行(resume)的隐患

Thread类曾提供了suspend()和resume()方法用于线程挂起和恢复,但这些方法已被标记为过时,不推荐使用。

2.1 为什么被弃用?

主要问题在于suspend()方法在挂起线程时不会释放它持有的锁。如果在一个同步代码块中挂起线程,其他需要访问该同步资源的线程会被阻塞,极易导致死锁发生。

// 有问题的suspend/resume示例
public class SuspendResumeExample {
    static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized(lock) {
                System.out.println("t1获得锁");
                Thread.currentThread().suspend(); // 挂起但不释放锁
                System.out.println("t1继续执行");
            }
        });

        t1.start();
        Thread.sleep(100); // 确保t1先执行

        Thread t2 = new Thread(() -> {
            synchronized(lock) { // 这个调用将一直被阻塞,因为t1持有了锁但没有释放
                System.out.println("t2获得锁");
            }
        });

        t2.start();
        t1.resume(); // 尝试恢复t1
    }
}

在上面的例子中,t2线程会一直阻塞,因为t1在挂起时没有释放lock的锁,即使后续调用了resume(),也可能由于时机问题导致系统无法正常继续执行。

2.2 安全的替代方案

相比使用suspend()和resume(),更安全的做法是使用wait/notify机制或条件变量:

// 使用wait/notify替代suspend/resume
public class SafeSuspendResume {
    private static final Object lock = new Object();
    private static boolean suspended = false;

    public static void main(String[] args) {
        Thread worker = new Thread(() -> {
            while (true) {
                synchronized(lock) {
                    while (suspended) {
                        try {
                            lock.wait(); // 等待直到被通知
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            break;
                        }
                    }
                }

                // 执行实际工作
                System.out.println("工作线程运行中...");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        });

        worker.start();

        // 暂停工作线程
        synchronized(lock) {
            suspended = true;
        }

        // 恢复工作线程
        synchronized(lock) {
            suspended = false;
            lock.notifyAll();
        }
    }
}

这种方式避免了长期持有锁的问题,更加安全可靠。

3. 等待线程结束(join)和谦让(yield)

3.1 join:让线程有序结束

当一个线程需要等待另一个线程执行完毕时,可以使用join()方法。这在线程间有依赖关系的场景中非常有用。

public class JoinExample {
    public static void main(String[] args) throws InterruptedException {
        Thread worker1 = new Thread(() -> {
            System.out.println("工作者1开始工作");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("工作者1完成工作");
        });

        Thread worker2 = new Thread(() -> {
            System.out.println("工作者2开始工作");
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("工作者2完成工作");
        });

        worker1.start();
        worker2.start();

        // 主线程等待worker1和worker2完成
        worker1.join();
        worker2.join();

        System.out.println("所有工作者已完成任务,主线程继续执行");
    }
}

join()方法会使当前线程进入等待状态,直到目标线程执行完毕。还可以使用join(long millis)设置最大等待时间,避免无限期等待。

3.2 yield:线程的谦让行为

Thread.yield()是一个静态方法,它提示调度器当前线程愿意让出CPU使用权,但这只是一个提示,不保证调度器一定会采纳。

public class YieldExample {
    static class Worker extends Thread {
        private final String name;

        public Worker(String name) {
            this.name = name;
        }

        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(name + " 执行步骤 " + i);

                // 在特定条件下让出CPU
                if (i == 2) {
                    System.out.println(name + " 让出CPU");
                    Thread.yield();
                }
            }
        }
    }

    public static void main(String[] args) {
        new Worker("线程A").start();
        new Worker("线程B").start();
    }
}

yield()方法适用于优先级相等或更高的线程间协调,它可以让出CPU资源,给其他线程执行机会,但不改变锁的状态。

4. 线程状态与协作要点

理解Java线程的各种状态对正确使用协作机制至关重要:

  • NEW:线程创建但尚未启动

  • RUNNABLE:线程正在JVM中执行

  • BLOCKED:线程被阻塞等待监视器锁

  • WAITING:线程无限期等待另一个线程的操作

  • TIMED_WAITING:线程有限期等待

  • TERMINATED:线程已退出执行

图片

wait()方法会使线程进入WAITING或TIMED_WAITING状态,而notify()和notifyAll()则可以将线程从等待状态移回锁竞争队列。

5. 实战建议与常见陷阱

5.1 正确使用wait/notify的模式

为避免丢失通知或无限等待问题,推荐使用以下模式:

public class CorrectWaitNotify {
    private final Object lock = new Object();
    private boolean condition = false;

    public void awaitCondition() throws InterruptedException {
        synchronized(lock) {
            // 始终在循环中检查条件
            while (!condition) {
                lock.wait();
            }
            // 条件满足后执行相应操作
            System.out.println("条件满足,继续执行");
        }
    }

    public void satisfyCondition() {
        synchronized(lock) {
            condition = true;
            lock.notifyAll(); // 优先使用notifyAll而非notify
        }
    }
}

关键点:

  • 总是在循环中检查条件,避免虚假唤醒

  • 优先使用notifyAll(),除非确实只需要唤醒一个线程且性能考虑极为重要

  • 确保在条件改变后发出通知

6. 总结

Java线程协作机制是一把双刃剑,正确使用可以大幅提升程序性能和解耦设计,而误用则会导致性能问题甚至死锁。以下是关键要点总结:

机制

优点

缺点

适用场景

wait/notify

释放锁资源,高效协作

需在同步环境中使用

生产者-消费者模式

join

简单直观保证执行顺序

会阻塞当前线程

线程间依赖关系强

yield

提示性谦让,避免饥饿

效果不确定

平衡同等优先级线程

记住核心原则:多线程编程的首要目标是正确性,其次才是性能。在复杂场景中,考虑使用java.util.concurrent包中的高级工具类,它们封装了这些底层机制,提供了更安全、高效的并发解决方案。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐