跳转至

Java Thread Waiting:深入理解与实践

简介

在Java多线程编程中,线程等待(Thread Waiting)是一个至关重要的概念。它允许线程暂停执行,等待特定条件满足后再继续执行。这一机制在解决线程间的同步和协调问题时发挥着关键作用,能够有效避免竞争条件和死锁等常见问题,从而提升程序的稳定性和性能。本文将详细介绍Java线程等待的基础概念、使用方法、常见实践以及最佳实践,帮助读者全面掌握这一技术。

目录

  1. 基础概念
    • 什么是线程等待
    • 线程状态与等待
  2. 使用方法
    • Object类的等待方法
    • Condition接口的使用
  3. 常见实践
    • 生产者 - 消费者模型
    • 线程池中的线程等待
  4. 最佳实践
    • 避免死锁
    • 合理设置等待时间
    • 使用CountDownLatchCyclicBarrier
  5. 小结
  6. 参考资料

基础概念

什么是线程等待

线程等待是指一个线程暂停其执行,进入等待状态,直到满足特定条件。这通常发生在多个线程需要共享资源或协调执行顺序的场景中。例如,一个线程可能需要等待另一个线程完成某个任务后才能继续执行。

线程状态与等待

在Java中,线程有多种状态,如NEW(新建)、RUNNABLE(可运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(计时等待)和TERMINATED(终止)。当线程调用等待方法时,它会从RUNNABLE状态转换到WAITINGTIMED_WAITING状态。处于WAITING状态的线程会无限期等待,直到被其他线程唤醒;而处于TIMED_WAITING状态的线程会等待指定的时间,时间结束后如果没有被唤醒也会继续执行。

使用方法

Object类的等待方法

Object类提供了三个用于线程等待的方法:wait()wait(long timeout)wait(long timeout, int nanos)

public class ObjectWaitExample {
    public static void main(String[] args) {
        Object lock = new Object();

        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Thread 1 is waiting...");
                    lock.wait();
                    System.out.println("Thread 1 has been awakened.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                try {
                    Thread.sleep(2000);
                    System.out.println("Thread 2 is notifying...");
                    lock.notify();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

在上述代码中,thread1调用lock.wait()进入等待状态,thread2在睡眠2秒后调用lock.notify()唤醒thread1。需要注意的是,wait()方法必须在synchronized块中调用,否则会抛出IllegalMonitorStateException

Condition接口的使用

Condition接口是Java 5.0引入的,它提供了比Object类等待方法更灵活的线程等待控制。ConditionLock配合使用,一个Lock可以创建多个Condition实例。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        Thread thread1 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("Thread 1 is waiting...");
                condition.await();
                System.out.println("Thread 1 has been awakened.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        Thread thread2 = new Thread(() -> {
            lock.lock();
            try {
                Thread.sleep(2000);
                System.out.println("Thread 2 is notifying...");
                condition.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        thread1.start();
        thread2.start();
    }
}

在这个例子中,thread1调用condition.await()进入等待状态,thread2在睡眠2秒后调用condition.signal()唤醒thread1Condition接口提供了更丰富的方法,如signalAll()可以唤醒所有等待的线程。

常见实践

生产者 - 消费者模型

生产者 - 消费者模型是线程等待的经典应用场景。生产者线程生成数据并放入缓冲区,消费者线程从缓冲区取出数据进行处理。当缓冲区满时,生产者线程需要等待;当缓冲区空时,消费者线程需要等待。

import java.util.LinkedList;
import java.util.Queue;

public class ProducerConsumerExample {
    private static final int MAX_SIZE = 5;
    private static Queue<Integer> queue = new LinkedList<>();

    public static void main(String[] args) {
        Thread producerThread = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                synchronized (queue) {
                    while (queue.size() == MAX_SIZE) {
                        try {
                            System.out.println("Producer is waiting...");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.add(i);
                    System.out.println("Producer produced: " + i);
                    queue.notify();
                }
            }
        });

        Thread consumerThread = new Thread(() -> {
            while (true) {
                synchronized (queue) {
                    while (queue.isEmpty()) {
                        try {
                            System.out.println("Consumer is waiting...");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    int item = queue.poll();
                    System.out.println("Consumer consumed: " + item);
                    queue.notify();
                }
            }
        });

        producerThread.start();
        consumerThread.start();
    }
}

在这个例子中,生产者线程和消费者线程通过synchronized块和wait()notify()方法实现了缓冲区的同步访问。

线程池中的线程等待

线程池中的线程在任务队列没有任务时会进入等待状态,直到有新的任务提交。以下是一个简单的线程池示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        executorService.submit(() -> {
            System.out.println("Task 1 is running.");
        });

        executorService.submit(() -> {
            System.out.println("Task 2 is running.");
        });

        executorService.shutdown();
    }
}

在上述代码中,线程池中的线程在执行完任务后会等待新的任务提交,直到线程池关闭。

最佳实践

避免死锁

死锁是多线程编程中常见的问题,当两个或多个线程相互等待对方释放资源时就会发生死锁。为了避免死锁,应遵循以下原则: - 尽量减少锁的使用范围。 - 按照相同的顺序获取锁。 - 设置合理的锁获取超时时间。

合理设置等待时间

在使用wait(long timeout)Condition.await(long timeout, TimeUnit unit)等方法时,应根据实际情况合理设置等待时间。如果等待时间过短,可能导致线程频繁唤醒,增加系统开销;如果等待时间过长,可能导致程序响应缓慢。

使用CountDownLatchCyclicBarrier

CountDownLatchCyclicBarrier是Java并发包中提供的同步工具类,它们可以更方便地实现线程间的等待和同步。CountDownLatch用于让一个或多个线程等待其他线程完成一组操作,CyclicBarrier用于让一组线程相互等待,直到所有线程都到达某个屏障点。

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) {
        int numThreads = 3;
        CountDownLatch latch = new CountDownLatch(numThreads);

        for (int i = 0; i < numThreads; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep((long) (Math.random() * 2000));
                    System.out.println(Thread.currentThread().getName() + " has finished.");
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }

        try {
            System.out.println("Main thread is waiting...");
            latch.await();
            System.out.println("All threads have finished.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int numThreads = 3;
        CyclicBarrier barrier = new CyclicBarrier(numThreads, () -> {
            System.out.println("All threads have reached the barrier.");
        });

        for (int i = 0; i < numThreads; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep((long) (Math.random() * 2000));
                    System.out.println(Thread.currentThread().getName() + " is waiting at the barrier.");
                    barrier.await();
                    System.out.println(Thread.currentThread().getName() + " has passed the barrier.");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

小结

本文详细介绍了Java线程等待的基础概念、使用方法、常见实践以及最佳实践。通过合理运用线程等待机制,能够有效解决多线程编程中的同步和协调问题,提升程序的性能和稳定性。在实际开发中,应根据具体需求选择合适的等待方法,并遵循最佳实践原则,避免出现死锁等问题。

参考资料